diff --git a/datasette/app.py b/datasette/app.py index bafee857..fa273df0 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -35,6 +35,7 @@ from .views.special import ( PatternPortfolioView, AuthTokenView, LogoutView, + AllowDebugView, PermissionsDebugView, MessagesDebugView, ) @@ -869,6 +870,9 @@ class Datasette: add_route( MessagesDebugView.as_view(self), r"/-/messages$", ) + add_route( + AllowDebugView.as_view(self), r"/-/allow-debug$", + ) add_route( PatternPortfolioView.as_view(self), r"/-/patterns$", ) diff --git a/datasette/templates/allow_debug.html b/datasette/templates/allow_debug.html new file mode 100644 index 00000000..05e3dd90 --- /dev/null +++ b/datasette/templates/allow_debug.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} + +{% block title %}Debug allow rules{% endblock %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} + +

Debug allow rules

+ +

Use this tool to try out different actor and allow combinations. See Defining permissions with "allow" blocks for documentation.

+ +
+
+

+ +
+
+

+ +
+
+ +
+
+ +{% if error %}

{{ error }}

{% endif %} + +{% if result == "True" %}

Result: allow

{% endif %} + +{% if result == "False" %}

Result: deny

{% endif %} + +{% endblock %} diff --git a/datasette/views/special.py b/datasette/views/special.py index ed5a36f7..3067b0d1 100644 --- a/datasette/views/special.py +++ b/datasette/views/special.py @@ -1,5 +1,6 @@ import json from datasette.utils.asgi import Response, Forbidden +from datasette.utils import actor_matches_allow from .base import BaseView import secrets @@ -107,6 +108,43 @@ class PermissionsDebugView(BaseView): ) +class AllowDebugView(BaseView): + name = "allow_debug" + + def __init__(self, datasette): + self.ds = datasette + + async def get(self, request): + errors = [] + actor_input = request.args.get("actor") or '{"id": "root"}' + try: + actor = json.loads(actor_input) + actor_input = json.dumps(actor, indent=4) + except json.decoder.JSONDecodeError as ex: + errors.append("Actor JSON error: {}".format(ex)) + allow_input = request.args.get("allow") or '{"id": "*"}' + try: + allow = json.loads(allow_input) + allow_input = json.dumps(allow, indent=4) + except json.decoder.JSONDecodeError as ex: + errors.append("Allow JSON error: {}".format(ex)) + + result = None + if not errors: + result = str(actor_matches_allow(actor, allow)) + + return await self.render( + ["allow_debug.html"], + request, + { + "result": result, + "error": "\n\n".join(errors) if errors else "", + "actor_input": actor_input, + "allow_input": allow_input, + }, + ) + + class MessagesDebugView(BaseView): name = "messages_debug" diff --git a/docs/authentication.rst b/docs/authentication.rst index a2b1276b..648d40f8 100644 --- a/docs/authentication.rst +++ b/docs/authentication.rst @@ -157,6 +157,13 @@ You can specify that unauthenticated actors (from anynomous HTTP requests) shoul Allow keys act as an "or" mechanism. An actor will be able to execute the query if any of their JSON properties match any of the values in the corresponding lists in the ``allow`` block. +.. _AllowDebugView: + +The /-/allow-debug tool +----------------------- + +The ``/-/allow-debug`` tool lets you try out different ``"action"`` blocks against different ``"actor"`` JSON objects. You can try that out here: https://latest.datasette.io/-/allow-debug + .. _authentication_permissions_metadata: Configuring permissions in metadata.json diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 90e58a27..e66ba53b 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -2,6 +2,7 @@ from .fixtures import app_client, assert_permissions_checked, make_app_client from bs4 import BeautifulSoup as Soup import copy import pytest +import urllib @pytest.mark.parametrize( @@ -312,6 +313,23 @@ def test_permissions_debug(app_client): ] == checks +@pytest.mark.parametrize( + "actor,allow,expected_fragment", + [ + ('{"id":"root"}', "{}", "Result: deny"), + ('{"id":"root"}', '{"id": "*"}', "Result: allow"), + ('{"', '{"id": "*"}', "Actor JSON error"), + ('{"id":"root"}', '"*"}', "Allow JSON error"), + ], +) +def test_allow_debug(app_client, actor, allow, expected_fragment): + response = app_client.get( + "/-/allow-debug?" + urllib.parse.urlencode({"actor": actor, "allow": allow}) + ) + assert 200 == response.status + assert expected_fragment in response.text + + @pytest.mark.parametrize( "allow,expected", [({"id": "root"}, 403), ({"id": "root", "unauthenticated": True}, 200),],