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 import hookimpl
|
||||||
from datasette.utils import actor_matches_allow
|
from datasette.utils import actor_matches_allow
|
||||||
|
import click
|
||||||
import itsdangerous
|
import itsdangerous
|
||||||
|
import json
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -72,3 +74,39 @@ def actor_from_request(datasette, request):
|
||||||
if expires_at < time.time():
|
if expires_at < time.time():
|
||||||
return None
|
return None
|
||||||
return {"id": decoded["a"], "token": "dstok"}
|
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.
|
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:
|
.. _permissions_plugins:
|
||||||
|
|
||||||
Checking permissions in 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.
|
--help Show this message and exit.
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
serve* Serve up specified SQLite database files with a web UI
|
serve* Serve up specified SQLite database files with a web UI
|
||||||
inspect Generate JSON summary of provided database files
|
create-token Create a signed API token for the specified actor ID
|
||||||
install Install plugins and packages from PyPI into the same...
|
inspect Generate JSON summary of provided database files
|
||||||
package Package SQLite files into a Datasette Docker container
|
install Install plugins and packages from PyPI into the same...
|
||||||
plugins List currently installed plugins
|
package Package SQLite files into a Datasette Docker container
|
||||||
publish Publish specified SQLite database files to the internet along...
|
plugins List currently installed plugins
|
||||||
uninstall Uninstall plugins and Python packages from the Datasette...
|
publish Publish specified SQLite database files to the internet...
|
||||||
|
uninstall Uninstall plugins and Python packages from the Datasette...
|
||||||
|
|
||||||
|
|
||||||
.. [[[end]]]
|
.. [[[end]]]
|
||||||
|
|
@ -591,3 +592,31 @@ This performance optimization is used automatically by some of the ``datasette p
|
||||||
|
|
||||||
|
|
||||||
.. [[[end]]]
|
.. [[[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,
|
"version": null,
|
||||||
"hooks": [
|
"hooks": [
|
||||||
"actor_from_request",
|
"actor_from_request",
|
||||||
"permission_allowed"
|
"permission_allowed",
|
||||||
|
"register_commands"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -806,6 +806,7 @@ def test_settings_json(app_client):
|
||||||
"max_returned_rows": 100,
|
"max_returned_rows": 100,
|
||||||
"sql_time_limit_ms": 200,
|
"sql_time_limit_ms": 200,
|
||||||
"allow_download": True,
|
"allow_download": True,
|
||||||
|
"allow_signed_tokens": True,
|
||||||
"allow_facet": True,
|
"allow_facet": True,
|
||||||
"suggest_facets": True,
|
"suggest_facets": True,
|
||||||
"default_cache_ttl": 5,
|
"default_cache_ttl": 5,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
from .fixtures import app_client
|
from .fixtures import app_client
|
||||||
|
from click.testing import CliRunner
|
||||||
from datasette.utils import baseconv
|
from datasette.utils import baseconv
|
||||||
|
from datasette.cli import cli
|
||||||
import pytest
|
import pytest
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
@ -235,3 +237,29 @@ def test_auth_with_dstok_token(app_client, scenario, should_work):
|
||||||
assert response.json == {"actor": None}
|
assert response.json == {"actor": None}
|
||||||
finally:
|
finally:
|
||||||
app_client.ds._settings["allow_signed_tokens"] = True
|
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",
|
"plugins",
|
||||||
"publish",
|
"publish",
|
||||||
"uninstall",
|
"uninstall",
|
||||||
|
"create-token",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Now install a plugin
|
# Now install a plugin
|
||||||
|
|
@ -1001,6 +1002,7 @@ def test_hook_register_commands():
|
||||||
"uninstall",
|
"uninstall",
|
||||||
"verify",
|
"verify",
|
||||||
"unverify",
|
"unverify",
|
||||||
|
"create-token",
|
||||||
}
|
}
|
||||||
pm.unregister(name="verify")
|
pm.unregister(name="verify")
|
||||||
importlib.reload(cli)
|
importlib.reload(cli)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue