home / github / issue_comments

Menu
  • Search all tables
  • GraphQL API

issue_comments: 1301635340

This data as json

html_url issue_url id node_id user created_at updated_at author_association body reactions issue performed_via_github_app
https://github.com/simonw/datasette/issues/1881#issuecomment-1301635340 https://api.github.com/repos/simonw/datasette/issues/1881 1301635340 IC_kwDOBm6k_c5NlWEM 9599 2022-11-03T04:46:41Z 2022-11-03T04:46:41Z OWNER

Built this prototype:

In building it I realized I needed to know which permissions took a table, a database, both or neither. So I had to bake that into the code.

Here's the prototype so far (which includes a prototype of the logic for the _r field on actor, see #1855):

```diff diff --git a/datasette/default_permissions.py b/datasette/default_permissions.py index 32b0c758..f68aa38f 100644 --- a/datasette/default_permissions.py +++ b/datasette/default_permissions.py @@ -6,8 +6,8 @@ import json import time

-@hookimpl(tryfirst=True) -def permission_allowed(datasette, actor, action, resource): +@hookimpl(tryfirst=True, specname="permission_allowed") +def permission_allowed_default(datasette, actor, action, resource): async def inner(): if action in ( "permissions-debug", @@ -57,6 +57,44 @@ def permission_allowed(datasette, actor, action, resource): return inner

+@hookimpl(specname="permission_allowed") +def permission_allowed_actor_restrictions(actor, action, resource): + if actor is None: + return None + r = actor.get("_r") + if not _r: + # No restrictions, so we have no opinion + return None + action_initials = "".join([word[0] for word in action.split("-")]) + # If _r is defined then we use those to further restrict the actor + # Crucially, we only use this to say NO (return False) - we never + # use it to return YES (True) because that might over-ride other + # restrictions placed on this actor + all_allowed = _r.get("a") + if all_allowed is not None: + assert isinstance(all_allowed, list) + if action_initials in all_allowed: + return None + # How about for the current database? + if action in ("view-database", "view-database-download", "execute-sql"): + database_allowed = _r.get("d", {}).get(resource) + if database_allowed is not None: + assert isinstance(database_allowed, list) + if action_initials in database_allowed: + return None + # Or the current table? That's any time the resource is (database, table) + if not isinstance(resource, str) and len(resource) == 2: + database, table = resource + table_allowed = _r.get("t", {}).get(database, {}).get(table) + # TODO: What should this do for canned queries? + if table_allowed is not None: + assert isinstance(table_allowed, list) + if action_initials in table_allowed: + return None + # This action is not specifically allowed, so reject it + return False + + @hookimpl def actor_from_request(datasette, request): prefix = "dstok" diff --git a/datasette/templates/allow_debug.html b/datasette/templates/allow_debug.html index 0f1b30f0..ae43f0f5 100644 --- a/datasette/templates/allow_debug.html +++ b/datasette/templates/allow_debug.html @@ -35,7 +35,7 @@ p.message-warning {

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

-<form action="{{ urls.path('-/allow-debug') }}" method="get"> +<form action="{{ urls.path('-/allow-debug') }}" method="get" style="margin-bottom: 1em">

<label>Allow block</label>

<textarea name="allow">{{ allow_input }}</textarea> @@ -55,4 +55,82 @@ p.message-warning {

{% if result == "False" %}

Result: deny

{% endif %}

+

Test permission check

+ +

This tool lets you simulate an actor and a permission check for that actor.

+ +<form action="{{ urls.path('-/allow-debug') }}" id="debug-post" method="post" style="margin-bottom: 1em"> + +
+

<label>Actor</label>

+ <textarea name="actor">{% if actor_input %}{{ actor_input }}{% else %}{"id": "root"}{% endif %}</textarea> +
+
+

<label>Permission check</label>

+

<label for="permission" style="display:block">Permission</label> + <select name="permission" id="permission"> + {% for permission in [ + "view-instance", + "view-database", + "view-database-download", + "view-table", + "view-query", + "insert-row", + "delete-row", + "drop-table", + "execute-sql", + "permissions-debug", + "debug-menu"] %} + <option value="{{ permission }}">{{ permission }}</option> + {% endfor %} + </select> +

<label for="resource_1">Database name</label>

+

<label for="resource_2">Table or query name</label>

+
+
+ +
+</form> + +<script> +var rawPerms = {{ permissions|tojson }}; +var permissions = Object.fromEntries(rawPerms.map(([label, abbr, needs_resource_1, needs_resource_2, def]) => [label, {needs_resource_1, needs_resource_2, def}])) +var permissionSelect = document.getElementById('permission'); +var resource1 = document.getElementById('resource_1'); +var resource2 = document.getElementById('resource_2'); +function updateResourceVisibility() { + var permission = permissionSelect.value; + var {needs_resource_1, needs_resource_2} = permissions[permission]; + if (needs_resource_1) { + resource1.closest('p').style.display = 'block'; + } else { + resource1.closest('p').style.display = 'none'; + } + if (needs_resource_2) { + resource2.closest('p').style.display = 'block'; + } else { + resource2.closest('p').style.display = 'none'; + } +} +permissionSelect.addEventListener('change', updateResourceVisibility); +updateResourceVisibility(); + +// When #debug-post form is submitted, use fetch() to POST data +var debugPost = document.getElementById('debug-post'); +debugPost.addEventListener('submit', function(ev) { + ev.preventDefault(); + var formData = new FormData(debugPost); + console.log(formData); + fetch(debugPost.action, { + method: 'POST', + body: new URLSearchParams(formData), + }).then(function(response) { + return response.json(); + }).then(function(data) { + alert(JSON.stringify(data, null, 4)); + }); +}); +</script> +
+ {% endblock %} diff --git a/datasette/views/special.py b/datasette/views/special.py index 9922a621..d46fc280 100644 --- a/datasette/views/special.py +++ b/datasette/views/special.py @@ -1,6 +1,8 @@ import json +from datasette.permissions import PERMISSIONS from datasette.utils.asgi import Response, Forbidden from datasette.utils import actor_matches_allow, add_cors_headers +from datasette.permissions import PERMISSIONS from .base import BaseView import secrets import time @@ -138,9 +140,34 @@ class AllowDebugView(BaseView): "error": "\n\n".join(errors) if errors else "", "actor_input": actor_input, "allow_input": allow_input, + "permissions": PERMISSIONS, }, )

  • async def post(self, request):
  • vars = await request.post_vars()
  • actor = json.loads(vars["actor"])
  • permission = vars["permission"]
  • resource_1 = vars["resource_1"]
  • resource_2 = vars["resource_2"]
  • resource = []
  • if resource_1:
  • resource.append(resource_1)
  • if resource_2:
  • resource.append(resource_2)
  • resource = tuple(resource)
  • result = await self.ds.permission_allowed(
  • actor, permission, resource, default="USE_DEFAULT"
  • )
  • return Response.json(
  • {
  • "actor": actor,
  • "permission": permission,
  • "resource": resource,
  • "result": result,
  • }
  • ) +

class MessagesDebugView(BaseView): name = "messages_debug" ```

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
1434094365  
Powered by Datasette · Queries took 1.911ms · About: github-to-sqlite