diff --git a/datasette/default_permissions.py b/datasette/default_permissions.py index e873361c..a37c47c1 100644 --- a/datasette/default_permissions.py +++ b/datasette/default_permissions.py @@ -203,23 +203,22 @@ def permission_allowed_root(datasette, actor, action, resource): @hookimpl async def permission_resources_sql(datasette, actor, action): - # Root user with root_enabled gets all permissions + rules: list[PermissionSQL] = [] + + # Root user with root_enabled gets all permissions at global level + # Config rules at more specific levels (database/table) can still override if datasette.root_enabled and actor and actor.get("id") == "root": - # Return SQL that grants access to ALL resources for this action - action_obj = datasette.actions.get(action) - if action_obj and action_obj.resource_class: - resources_sql = action_obj.resource_class.resources_sql() - sql = f""" - SELECT parent, child, 1 AS allow, 'root user' AS reason - FROM ({resources_sql}) - """ - return PermissionSQL( + # Add a single global-level allow rule (NULL, NULL) for root + # This allows root to access everything by default, but database-level + # and table-level deny rules in config can still block specific resources + sql = "SELECT NULL AS parent, NULL AS child, 1 AS allow, 'root user' AS reason" + rules.append( + PermissionSQL( source="root_permissions", sql=sql, params={}, ) - - rules: list[PermissionSQL] = [] + ) config_rules = await _config_permission_rules(datasette, actor, action) rules.extend(config_rules) diff --git a/tests/test_permission_endpoints.py b/tests/test_permission_endpoints.py index 0210fb95..33e7cd75 100644 --- a/tests/test_permission_endpoints.py +++ b/tests/test_permission_endpoints.py @@ -494,3 +494,84 @@ async def test_html_endpoints_return_html(ds_with_permissions, path, needs_debug # Check for HTML structure text = response.text assert "" in text or "