{"html_url": "https://github.com/simonw/datasette/issues/1881#issuecomment-1301635340", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1881", "id": 1301635340, "node_id": "IC_kwDOBm6k_c5NlWEM", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T04:46:41Z", "updated_at": "2022-11-03T04:46:41Z", "author_association": "OWNER", "body": "Built this prototype:\r\n\r\n![prototype](https://user-images.githubusercontent.com/9599/199649219-f146e43b-bfb5-45e6-9777-956f21a79887.gif)\r\n\r\nIn 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.\r\n\r\nHere's the prototype so far (which includes a prototype of the logic for the `_r` field on actor, see #1855):\r\n\r\n```diff\r\ndiff --git a/datasette/default_permissions.py b/datasette/default_permissions.py\r\nindex 32b0c758..f68aa38f 100644\r\n--- a/datasette/default_permissions.py\r\n+++ b/datasette/default_permissions.py\r\n@@ -6,8 +6,8 @@ import json\r\n import time\r\n \r\n \r\n-@hookimpl(tryfirst=True)\r\n-def permission_allowed(datasette, actor, action, resource):\r\n+@hookimpl(tryfirst=True, specname=\"permission_allowed\")\r\n+def permission_allowed_default(datasette, actor, action, resource):\r\n async def inner():\r\n if action in (\r\n \"permissions-debug\",\r\n@@ -57,6 +57,44 @@ def permission_allowed(datasette, actor, action, resource):\r\n return inner\r\n \r\n \r\n+@hookimpl(specname=\"permission_allowed\")\r\n+def permission_allowed_actor_restrictions(actor, action, resource):\r\n+ if actor is None:\r\n+ return None\r\n+ _r = actor.get(\"_r\")\r\n+ if not _r:\r\n+ # No restrictions, so we have no opinion\r\n+ return None\r\n+ action_initials = \"\".join([word[0] for word in action.split(\"-\")])\r\n+ # If _r is defined then we use those to further restrict the actor\r\n+ # Crucially, we only use this to say NO (return False) - we never\r\n+ # use it to return YES (True) because that might over-ride other\r\n+ # restrictions placed on this actor\r\n+ all_allowed = _r.get(\"a\")\r\n+ if all_allowed is not None:\r\n+ assert isinstance(all_allowed, list)\r\n+ if action_initials in all_allowed:\r\n+ return None\r\n+ # How about for the current database?\r\n+ if action in (\"view-database\", \"view-database-download\", \"execute-sql\"):\r\n+ database_allowed = _r.get(\"d\", {}).get(resource)\r\n+ if database_allowed is not None:\r\n+ assert isinstance(database_allowed, list)\r\n+ if action_initials in database_allowed:\r\n+ return None\r\n+ # Or the current table? That's any time the resource is (database, table)\r\n+ if not isinstance(resource, str) and len(resource) == 2:\r\n+ database, table = resource\r\n+ table_allowed = _r.get(\"t\", {}).get(database, {}).get(table)\r\n+ # TODO: What should this do for canned queries?\r\n+ if table_allowed is not None:\r\n+ assert isinstance(table_allowed, list)\r\n+ if action_initials in table_allowed:\r\n+ return None\r\n+ # This action is not specifically allowed, so reject it\r\n+ return False\r\n+\r\n+\r\n @hookimpl\r\n def actor_from_request(datasette, request):\r\n prefix = \"dstok_\"\r\ndiff --git a/datasette/templates/allow_debug.html b/datasette/templates/allow_debug.html\r\nindex 0f1b30f0..ae43f0f5 100644\r\n--- a/datasette/templates/allow_debug.html\r\n+++ b/datasette/templates/allow_debug.html\r\n@@ -35,7 +35,7 @@ p.message-warning {\r\n \r\n