Update test infrastructure to use register_actions hook

- Consolidated register_permissions and register_actions hooks in my_plugin.py
- Added permission_resources_sql hook to provide SQL-based permission rules
- Updated conftest.py to reference datasette.actions instead of datasette.permissions
- Updated fixtures.py to include permission_resources_sql hook and remove register_permissions
- Added backwards compatibility support for old datasette-register-permissions config
- Converted test actions (this_is_allowed, this_is_denied, etc.) to use permission_resources_sql

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Simon Willison 2025-10-24 14:31:33 -07:00
commit fe2084df66
3 changed files with 89 additions and 34 deletions

View file

@ -147,8 +147,8 @@ def check_permission_actions_are_documented():
def before(hook_name, hook_impls, kwargs):
if hook_name == "permission_allowed":
datasette = kwargs["datasette"]
assert kwargs["action"] in datasette.permissions, (
"'{}' has not been registered with register_permissions()".format(
assert kwargs["action"] in datasette.actions, (
"'{}' has not been registered with register_actions()".format(
kwargs["action"]
)
+ " (or maybe a test forgot to do await ds.invoke_startup())"

View file

@ -45,13 +45,13 @@ EXPECTED_PLUGINS = [
"homepage_actions",
"menu_links",
"permission_allowed",
"permission_resources_sql",
"prepare_connection",
"prepare_jinja2_environment",
"query_actions",
"register_actions",
"register_facet_classes",
"register_magic_parameters",
"register_permissions",
"register_routes",
"render_cell",
"row_actions",

View file

@ -1,9 +1,9 @@
import asyncio
from datasette import hookimpl, Permission
from datasette import hookimpl
from datasette.facets import Facet
from datasette import tracer
from datasette.permissions import Action
from datasette.resources import DatabaseResource
from datasette.resources import DatabaseResource, InstanceResource
from datasette.utils import path_with_added_args
from datasette.utils.asgi import asgi_send_json, Response
import base64
@ -474,37 +474,20 @@ def skip_csrf(scope):
return scope["path"] == "/skip-csrf"
@hookimpl
def register_permissions(datasette):
extras = datasette.plugin_config("datasette-register-permissions") or {}
permissions = [
Permission(
name="permission-from-plugin",
abbr="np",
description="New permission added by a plugin",
takes_database=True,
takes_resource=False,
default=False,
)
]
if extras:
permissions.extend(
Permission(
name=p["name"],
abbr=p["abbr"],
description=p["description"],
takes_database=p["takes_database"],
takes_resource=p["takes_resource"],
default=p["default"],
)
for p in extras["permissions"]
)
return permissions
@hookimpl
def register_actions(datasette):
return [
extras_old = datasette.plugin_config("datasette-register-permissions") or {}
extras_new = datasette.plugin_config("datasette-register-actions") or {}
actions = [
Action(
name="action-from-plugin",
abbr="ap",
description="New action added by a plugin",
takes_parent=True,
takes_child=False,
resource_class=DatabaseResource,
),
Action(
name="view-collection",
abbr="vc",
@ -514,3 +497,75 @@ def register_actions(datasette):
resource_class=DatabaseResource,
)
]
# Support old-style config for backwards compatibility
if extras_old:
for p in extras_old["permissions"]:
# Map old takes_database/takes_resource to new takes_parent/takes_child
actions.append(
Action(
name=p["name"],
abbr=p["abbr"],
description=p["description"],
takes_parent=p.get("takes_database", False),
takes_child=p.get("takes_resource", False),
resource_class=DatabaseResource if p.get("takes_database") else InstanceResource,
)
)
# Support new-style config
if extras_new:
for a in extras_new["actions"]:
# Map string resource_class to actual class
resource_class_map = {
"InstanceResource": InstanceResource,
"DatabaseResource": DatabaseResource,
}
resource_class = resource_class_map.get(a.get("resource_class", "InstanceResource"), InstanceResource)
actions.append(
Action(
name=a["name"],
abbr=a["abbr"],
description=a["description"],
takes_parent=a.get("takes_parent", False),
takes_child=a.get("takes_child", False),
resource_class=resource_class,
)
)
return actions
@hookimpl
def permission_resources_sql(datasette, actor, action):
from datasette.permissions import PermissionSQL
# Handle test actions used in test_hook_permission_allowed
if action == "this_is_allowed":
sql = "SELECT NULL AS parent, NULL AS child, 1 AS allow, 'test plugin allows this_is_allowed' AS reason, 'my_plugin' AS source_plugin"
return PermissionSQL(source="my_plugin", sql=sql, params={})
elif action == "this_is_denied":
sql = "SELECT NULL AS parent, NULL AS child, 0 AS allow, 'test plugin denies this_is_denied' AS reason, 'my_plugin' AS source_plugin"
return PermissionSQL(source="my_plugin", sql=sql, params={})
elif action == "this_is_allowed_async":
sql = "SELECT NULL AS parent, NULL AS child, 1 AS allow, 'test plugin allows this_is_allowed_async' AS reason, 'my_plugin' AS source_plugin"
return PermissionSQL(source="my_plugin", sql=sql, params={})
elif action == "this_is_denied_async":
sql = "SELECT NULL AS parent, NULL AS child, 0 AS allow, 'test plugin denies this_is_denied_async' AS reason, 'my_plugin' AS source_plugin"
return PermissionSQL(source="my_plugin", sql=sql, params={})
elif action == "view-database-download":
# Return rule based on actor's can_download permission
if actor and actor.get("can_download"):
sql = "SELECT NULL AS parent, NULL AS child, 1 AS allow, 'actor has can_download' AS reason, 'my_plugin' AS source_plugin"
else:
return None # No opinion
return PermissionSQL(source="my_plugin", sql=sql, params={})
elif action in ("insert-row", "create-table", "drop-table", "delete-row", "update-row"):
# Special permissions for latest.datasette.io demos
actor_id = actor.get("id") if actor else None
if actor_id == "todomvc":
sql = f"SELECT NULL AS parent, NULL AS child, 1 AS allow, 'todomvc actor allowed for {action}' AS reason, 'my_plugin' AS source_plugin"
return PermissionSQL(source="my_plugin", sql=sql, params={})
return None