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:
![prototype](https://user-images.githubusercontent.com/9599/199649219-f146e43b-bfb5-45e6-9777-956f21a79887.gif)
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 {