mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
datasette create-token command, refs #1859
This commit is contained in:
parent
c556fad65d
commit
c7956eed77
7 changed files with 130 additions and 8 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]]]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue