datasette create-token command, refs #1859

This commit is contained in:
Simon Willison 2022-10-25 21:26:12 -07:00
commit c7956eed77
7 changed files with 130 additions and 8 deletions

View file

@ -1,6 +1,8 @@
from datasette import hookimpl
from datasette.utils import actor_matches_allow
import click
import itsdangerous
import json
import time
@ -72,3 +74,39 @@ def actor_from_request(datasette, request):
if expires_at < time.time():
return None
return {"id": decoded["a"], "token": "dstok"}
@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(
"--debug",
help="Show decoded token",
is_flag=True,
)
def create_token(id, secret, expires_after, debug):
"Create a signed API token for the specified actor ID"
ds = Datasette(secret=secret)
bits = {"a": id, "token": "dstok"}
if expires_after:
bits["e"] = int(time.time()) + expires_after
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))

View file

@ -352,6 +352,29 @@ This page cannot be accessed by actors with a ``"token": "some-value"`` property
You can disable this feature using the :ref:`allow_signed_tokens <setting_allow_signed_tokens>` setting.
.. _authentication_cli_create_token:
datasette create-token
----------------------
You can also create tokens on the command line using the ``datasette create-token`` command.
This command takes one required argument - the ID of the actor to be associated with the created token.
You can specify an ``--expires-after`` option in seconds. If omitted, the token will never expire.
The command will sign the token using the ``DATASETTE_SECRET`` environment variable, if available. You can also pass the secret using the ``--secret`` option.
This means you can run the command locally to create tokens for use with a deployed Datasette instance, provided you know that instance's secret.
To create a token for the ``root`` actor that will expire in one hour::
datasette create-token root --expires-after 3600
To create a secret that never expires using a specific secret::
datasette create-token root --secret my-secret-goes-here
.. _permissions_plugins:
Checking permissions in plugins

View file

@ -47,13 +47,14 @@ Running ``datasette --help`` shows a list of all of the available commands.
--help Show this message and exit.
Commands:
serve* Serve up specified SQLite database files with a web UI
inspect Generate JSON summary of provided database files
install Install plugins and packages from PyPI into the same...
package Package SQLite files into a Datasette Docker container
plugins List currently installed plugins
publish Publish specified SQLite database files to the internet along...
uninstall Uninstall plugins and Python packages from the Datasette...
serve* Serve up specified SQLite database files with a web UI
create-token Create a signed API token for the specified actor ID
inspect Generate JSON summary of provided database files
install Install plugins and packages from PyPI into the same...
package Package SQLite files into a Datasette Docker container
plugins List currently installed plugins
publish Publish specified SQLite database files to the internet...
uninstall Uninstall plugins and Python packages from the Datasette...
.. [[[end]]]
@ -591,3 +592,31 @@ This performance optimization is used automatically by some of the ``datasette p
.. [[[end]]]
.. _cli_help_create_token___help:
datasette create-token
======================
Create a signed API token, see :ref:`authentication_cli_create_token`.
.. [[[cog
help(["create-token", "--help"])
.. ]]]
::
Usage: datasette create-token [OPTIONS] ID
Create a signed API token for the specified actor ID
Options:
--secret TEXT Secret used for signing the API tokens
[required]
-e, --expires-after INTEGER Token should expire after this many seconds
--debug Show decoded token
--help Show this message and exit.
.. [[[end]]]

View file

@ -152,7 +152,8 @@ If you run ``datasette plugins --all`` it will include default plugins that ship
"version": null,
"hooks": [
"actor_from_request",
"permission_allowed"
"permission_allowed",
"register_commands"
]
},
{

View file

@ -806,6 +806,7 @@ def test_settings_json(app_client):
"max_returned_rows": 100,
"sql_time_limit_ms": 200,
"allow_download": True,
"allow_signed_tokens": True,
"allow_facet": True,
"suggest_facets": True,
"default_cache_ttl": 5,

View file

@ -1,5 +1,7 @@
from .fixtures import app_client
from click.testing import CliRunner
from datasette.utils import baseconv
from datasette.cli import cli
import pytest
import time
@ -235,3 +237,29 @@ def test_auth_with_dstok_token(app_client, scenario, should_work):
assert response.json == {"actor": None}
finally:
app_client.ds._settings["allow_signed_tokens"] = True
@pytest.mark.parametrize("expires", (None, 1000, -1000))
def test_cli_create_token(app_client, expires):
secret = app_client.ds._secret
runner = CliRunner(mix_stderr=False)
args = ["create-token", "--secret", secret, "test"]
if expires:
args += ["--expires-after", str(expires)]
result = runner.invoke(cli, args)
assert result.exit_code == 0
token = result.output.strip()
assert token.startswith("dstok_")
details = app_client.ds.unsign(token[len("dstok_") :], "token")
expected_keys = {"a", "token"}
if expires:
expected_keys.add("e")
assert details.keys() == expected_keys
assert details["a"] == "test"
response = app_client.get(
"/-/actor.json", headers={"Authorization": "Bearer {}".format(token)}
)
if expires is None or expires > 0:
assert response.json == {"actor": {"id": "test", "token": "dstok"}}
else:
assert response.json == {"actor": None}

View file

@ -971,6 +971,7 @@ def test_hook_register_commands():
"plugins",
"publish",
"uninstall",
"create-token",
}
# Now install a plugin
@ -1001,6 +1002,7 @@ def test_hook_register_commands():
"uninstall",
"verify",
"unverify",
"create-token",
}
pm.unregister(name="verify")
importlib.reload(cli)