allow_signed_tokens setting, closes #1856

This commit is contained in:
Simon Willison 2022-10-25 19:55:47 -07:00
commit c23fa850e7
8 changed files with 48 additions and 5 deletions

View file

@ -124,6 +124,11 @@ SETTINGS = (
True,
"Allow users to download the original SQLite database files",
),
Setting(
"allow_signed_tokens",
True,
"Allow users to create and use signed API tokens",
),
Setting("suggest_facets", True, "Calculate and display suggested facets"),
Setting(
"default_cache_ttl",

View file

@ -52,6 +52,8 @@ def permission_allowed(datasette, actor, action, resource):
@hookimpl
def actor_from_request(datasette, request):
prefix = "dstok_"
if not datasette.setting("allow_signed_tokens"):
return None
authorization = request.headers.get("authorization")
if not authorization:
return None

View file

@ -171,6 +171,8 @@ class CreateTokenView(BaseView):
has_json_alternate = False
def check_permission(self, request):
if not self.ds.setting("allow_signed_tokens"):
raise Forbidden("Signed tokens are not enabled for this Datasette instance")
if not request.actor:
raise Forbidden("You must be logged in to create a token")
if not request.actor.get("id"):

View file

@ -350,6 +350,8 @@ Coming soon: a mechanism for creating tokens that can only perform a subset of t
This page cannot be accessed by actors with a ``"token": "some-value"`` property. This is to prevent API tokens from being used to automatically create more tokens. Datasette plugins that implement their own form of API token authentication should follow this convention.
You can disable this feature using the :ref:`allow_signed_tokens <setting_allow_signed_tokens>` setting.
.. _permissions_plugins:
Checking permissions in plugins

View file

@ -226,6 +226,8 @@ These can be passed to ``datasette serve`` using ``datasette serve --setting nam
?_facet= parameter (default=True)
allow_download Allow users to download the original SQLite
database files (default=True)
allow_signed_tokens Allow users to create and use signed API tokens
(default=True)
suggest_facets Calculate and display suggested facets
(default=True)
default_cache_ttl Default HTTP cache TTL (used in Cache-Control:

View file

@ -151,6 +151,7 @@ If you run ``datasette plugins --all`` it will include default plugins that ship
"templates": false,
"version": null,
"hooks": [
"actor_from_request",
"permission_allowed"
]
},

View file

@ -169,6 +169,19 @@ Should users be able to download the original SQLite database using a link on th
datasette mydatabase.db --setting allow_download off
.. _setting_allow_signed_tokens:
allow_signed_tokens
~~~~~~~~~~~~~~~~~~~
Should users be able to create signed API tokens to access Datasette?
This is turned on by default. Use the following to turn it off::
datasette mydatabase.db --setting allow_signed_tokens off
Turning this setting off will disable the ``/-/create-token`` page, :ref:`described here <CreateTokenView>`. It will also cause any incoming ``Authorization: Bearer dstok_...`` API tokens to be ignored.
.. _setting_default_cache_ttl:
default_cache_ttl

View file

@ -189,9 +189,20 @@ def test_auth_create_token_not_allowed_for_tokens(app_client):
assert response.status == 403
def test_auth_create_token_not_allowed_if_allow_signed_tokens_off(app_client):
app_client.ds._settings["allow_signed_tokens"] = False
try:
ds_actor = app_client.actor_cookie({"id": "test"})
response = app_client.get("/-/create-token", cookies={"ds_actor": ds_actor})
assert response.status == 403
finally:
app_client.ds._settings["allow_signed_tokens"] = True
@pytest.mark.parametrize(
"scenario,should_work",
(
("allow_signed_tokens_off", False),
("no_token", False),
("invalid_token", False),
("expired_token", False),
@ -201,7 +212,7 @@ def test_auth_create_token_not_allowed_for_tokens(app_client):
)
def test_auth_with_dstok_token(app_client, scenario, should_work):
token = None
if scenario == "valid_unlimited_token":
if scenario in ("valid_unlimited_token", "allow_signed_tokens_off"):
token = app_client.ds.sign({"a": "test"}, "token")
elif scenario == "valid_expiring_token":
token = app_client.ds.sign({"a": "test", "e": int(time.time()) + 1000}, "token")
@ -211,11 +222,16 @@ def test_auth_with_dstok_token(app_client, scenario, should_work):
token = "invalid"
if token:
token = "dstok_{}".format(token)
if scenario == "allow_signed_tokens_off":
app_client.ds._settings["allow_signed_tokens"] = False
headers = {}
if token:
headers["Authorization"] = "Bearer {}".format(token)
response = app_client.get("/-/actor.json", headers=headers)
if should_work:
assert response.json == {"actor": {"id": "test", "token": "dstok"}}
else:
assert response.json == {"actor": None}
try:
if should_work:
assert response.json == {"actor": {"id": "test", "token": "dstok"}}
else:
assert response.json == {"actor": None}
finally:
app_client.ds._settings["allow_signed_tokens"] = True