mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Move create-token command into cli.py, refs #1855
This commit is contained in:
parent
9cc1a7c4c8
commit
e95b490d88
2 changed files with 123 additions and 129 deletions
124
datasette/cli.py
124
datasette/cli.py
|
|
@ -7,10 +7,11 @@ from click_default_group import DefaultGroup
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
from runpy import run_module
|
||||||
import shutil
|
import shutil
|
||||||
from subprocess import call
|
from subprocess import call
|
||||||
import sys
|
import sys
|
||||||
from runpy import run_module
|
import time
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from .app import (
|
from .app import (
|
||||||
OBSOLETE_SETTINGS,
|
OBSOLETE_SETTINGS,
|
||||||
|
|
@ -628,6 +629,127 @@ def serve(
|
||||||
uvicorn.run(ds.app(), **uvicorn_kwargs)
|
uvicorn.run(ds.app(), **uvicorn_kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument("id")
|
||||||
|
@click.option(
|
||||||
|
"--secret",
|
||||||
|
help="Secret used for signing the API tokens",
|
||||||
|
envvar="DATASETTE_SECRET",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"-e",
|
||||||
|
"--expires-after",
|
||||||
|
help="Token should expire after this many seconds",
|
||||||
|
type=int,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"alls",
|
||||||
|
"-a",
|
||||||
|
"--all",
|
||||||
|
type=str,
|
||||||
|
metavar="ACTION",
|
||||||
|
multiple=True,
|
||||||
|
help="Restrict token to this action",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"databases",
|
||||||
|
"-d",
|
||||||
|
"--database",
|
||||||
|
type=(str, str),
|
||||||
|
metavar="DB ACTION",
|
||||||
|
multiple=True,
|
||||||
|
help="Restrict token to this action on this database",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"resources",
|
||||||
|
"-r",
|
||||||
|
"--resource",
|
||||||
|
type=(str, str, str),
|
||||||
|
metavar="DB RESOURCE ACTION",
|
||||||
|
multiple=True,
|
||||||
|
help="Restrict token to this action on this database resource (a table, SQL view or named query)",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--debug",
|
||||||
|
help="Show decoded token",
|
||||||
|
is_flag=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--plugins-dir",
|
||||||
|
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
||||||
|
help="Path to directory containing custom plugins",
|
||||||
|
)
|
||||||
|
def create_token(
|
||||||
|
id, secret, expires_after, alls, databases, resources, debug, plugins_dir
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create a signed API token for the specified actor ID
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
datasette create-token root --secret mysecret
|
||||||
|
|
||||||
|
To only allow create-table:
|
||||||
|
|
||||||
|
\b
|
||||||
|
datasette create-token root --secret mysecret \\
|
||||||
|
--all create-table
|
||||||
|
|
||||||
|
Or to only allow insert-row against a specific table:
|
||||||
|
|
||||||
|
\b
|
||||||
|
datasette create-token root --secret myscret \\
|
||||||
|
--resource mydb mytable insert-row
|
||||||
|
|
||||||
|
Restricted actions can be specified multiple times using
|
||||||
|
multiple --all, --database, and --resource options.
|
||||||
|
|
||||||
|
Add --debug to see a decoded version of the token.
|
||||||
|
"""
|
||||||
|
ds = Datasette(secret=secret, plugins_dir=plugins_dir)
|
||||||
|
|
||||||
|
# Run ds.invoke_startup() in an event loop
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.run_until_complete(ds.invoke_startup())
|
||||||
|
|
||||||
|
def fix_action(action):
|
||||||
|
# Warn if invalid, rename to abbr if possible
|
||||||
|
permission = ds.permissions.get(action)
|
||||||
|
if not permission:
|
||||||
|
# Output red message
|
||||||
|
click.secho(
|
||||||
|
f" Unknown permission: {action} ",
|
||||||
|
fg="red",
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
return action
|
||||||
|
return permission.abbr or action
|
||||||
|
|
||||||
|
bits = {"a": id, "token": "dstok", "t": int(time.time())}
|
||||||
|
if expires_after:
|
||||||
|
bits["d"] = expires_after
|
||||||
|
if alls or databases or resources:
|
||||||
|
bits["_r"] = {}
|
||||||
|
if alls:
|
||||||
|
bits["_r"]["a"] = [fix_action(a) for a in alls]
|
||||||
|
if databases:
|
||||||
|
bits["_r"]["d"] = {}
|
||||||
|
for database, action in databases:
|
||||||
|
bits["_r"]["d"].setdefault(database, []).append(fix_action(action))
|
||||||
|
if resources:
|
||||||
|
bits["_r"]["r"] = {}
|
||||||
|
for database, table, action in resources:
|
||||||
|
bits["_r"]["r"].setdefault(database, {}).setdefault(table, []).append(
|
||||||
|
fix_action(action)
|
||||||
|
)
|
||||||
|
token = ds.sign(bits, namespace="token")
|
||||||
|
click.echo("dstok_{}".format(token))
|
||||||
|
if debug:
|
||||||
|
click.echo("\nDecoded:\n")
|
||||||
|
click.echo(json.dumps(ds.unsign(token, namespace="token"), indent=2))
|
||||||
|
|
||||||
|
|
||||||
pm.hook.register_commands(cli=cli)
|
pm.hook.register_commands(cli=cli)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
from datasette import hookimpl, Permission
|
from datasette import hookimpl, Permission
|
||||||
from datasette.utils import actor_matches_allow
|
from datasette.utils import actor_matches_allow
|
||||||
import asyncio
|
|
||||||
import click
|
|
||||||
import itsdangerous
|
import itsdangerous
|
||||||
import json
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -261,131 +258,6 @@ def actor_from_request(datasette, request):
|
||||||
return actor
|
return actor
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
|
||||||
def register_commands(cli):
|
|
||||||
from datasette.app import Datasette
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
@click.argument("id")
|
|
||||||
@click.option(
|
|
||||||
"--secret",
|
|
||||||
help="Secret used for signing the API tokens",
|
|
||||||
envvar="DATASETTE_SECRET",
|
|
||||||
required=True,
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"-e",
|
|
||||||
"--expires-after",
|
|
||||||
help="Token should expire after this many seconds",
|
|
||||||
type=int,
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"alls",
|
|
||||||
"-a",
|
|
||||||
"--all",
|
|
||||||
type=str,
|
|
||||||
metavar="ACTION",
|
|
||||||
multiple=True,
|
|
||||||
help="Restrict token to this action",
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"databases",
|
|
||||||
"-d",
|
|
||||||
"--database",
|
|
||||||
type=(str, str),
|
|
||||||
metavar="DB ACTION",
|
|
||||||
multiple=True,
|
|
||||||
help="Restrict token to this action on this database",
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"resources",
|
|
||||||
"-r",
|
|
||||||
"--resource",
|
|
||||||
type=(str, str, str),
|
|
||||||
metavar="DB RESOURCE ACTION",
|
|
||||||
multiple=True,
|
|
||||||
help="Restrict token to this action on this database resource (a table, SQL view or named query)",
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"--debug",
|
|
||||||
help="Show decoded token",
|
|
||||||
is_flag=True,
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"--plugins-dir",
|
|
||||||
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
||||||
help="Path to directory containing custom plugins",
|
|
||||||
)
|
|
||||||
def create_token(
|
|
||||||
id, secret, expires_after, alls, databases, resources, debug, plugins_dir
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Create a signed API token for the specified actor ID
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
datasette create-token root --secret mysecret
|
|
||||||
|
|
||||||
To only allow create-table:
|
|
||||||
|
|
||||||
\b
|
|
||||||
datasette create-token root --secret mysecret \\
|
|
||||||
--all create-table
|
|
||||||
|
|
||||||
Or to only allow insert-row against a specific table:
|
|
||||||
|
|
||||||
\b
|
|
||||||
datasette create-token root --secret myscret \\
|
|
||||||
--resource mydb mytable insert-row
|
|
||||||
|
|
||||||
Restricted actions can be specified multiple times using
|
|
||||||
multiple --all, --database, and --resource options.
|
|
||||||
|
|
||||||
Add --debug to see a decoded version of the token.
|
|
||||||
"""
|
|
||||||
ds = Datasette(secret=secret, plugins_dir=plugins_dir)
|
|
||||||
|
|
||||||
# Run ds.invoke_startup() in an event loop
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.run_until_complete(ds.invoke_startup())
|
|
||||||
|
|
||||||
def fix_action(action):
|
|
||||||
# Warn if invalid, rename to abbr if possible
|
|
||||||
permission = ds.permissions.get(action)
|
|
||||||
if not permission:
|
|
||||||
# Output red message
|
|
||||||
click.secho(
|
|
||||||
f" Unknown permission: {action} ",
|
|
||||||
fg="red",
|
|
||||||
err=True,
|
|
||||||
)
|
|
||||||
return action
|
|
||||||
return permission.abbr or action
|
|
||||||
|
|
||||||
bits = {"a": id, "token": "dstok", "t": int(time.time())}
|
|
||||||
if expires_after:
|
|
||||||
bits["d"] = expires_after
|
|
||||||
if alls or databases or resources:
|
|
||||||
bits["_r"] = {}
|
|
||||||
if alls:
|
|
||||||
bits["_r"]["a"] = [fix_action(a) for a in alls]
|
|
||||||
if databases:
|
|
||||||
bits["_r"]["d"] = {}
|
|
||||||
for database, action in databases:
|
|
||||||
bits["_r"]["d"].setdefault(database, []).append(fix_action(action))
|
|
||||||
if resources:
|
|
||||||
bits["_r"]["r"] = {}
|
|
||||||
for database, table, action in resources:
|
|
||||||
bits["_r"]["r"].setdefault(database, {}).setdefault(
|
|
||||||
table, []
|
|
||||||
).append(fix_action(action))
|
|
||||||
token = ds.sign(bits, namespace="token")
|
|
||||||
click.echo("dstok_{}".format(token))
|
|
||||||
if debug:
|
|
||||||
click.echo("\nDecoded:\n")
|
|
||||||
click.echo(json.dumps(ds.unsign(token, namespace="token"), indent=2))
|
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def skip_csrf(scope):
|
def skip_csrf(scope):
|
||||||
# Skip CSRF check for requests with content-type: application/json
|
# Skip CSRF check for requests with content-type: application/json
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue