mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Fix #2509: Settings-based deny rules now override root user privileges
The root user's permission_resources_sql hook was returning early with a blanket "allow all" rule, preventing settings-based deny rules from being considered. This caused /-/allowed and /-/rules endpoints to incorrectly show resources that were denied via settings. Changed permission_resources_sql to append root permissions to the rules list instead of returning early, allowing config-based deny rules to be evaluated. The SQL cascading logic correctly applies: deny rules at the same depth beat allow rules, so database-level denies override root's global-level allow. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d1ea067fde
commit
16b2729847
2 changed files with 92 additions and 12 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 "<!DOCTYPE html>" in text or "<html" in text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_root_user_respects_settings_deny():
|
||||
"""
|
||||
Test for issue #2509: Settings-based deny rules should override root user privileges.
|
||||
|
||||
When a database has `allow: false` in settings, the root user should NOT see
|
||||
that database in /-/allowed.json?action=view-database, even though root normally
|
||||
has all permissions.
|
||||
"""
|
||||
ds = Datasette(
|
||||
config={
|
||||
"databases": {
|
||||
"content": {
|
||||
"allow": False, # Deny everyone, including root
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
ds.root_enabled = True
|
||||
await ds.invoke_startup()
|
||||
ds.add_memory_database("content")
|
||||
|
||||
# Root user should NOT see the content database because settings deny it
|
||||
response = await ds.client.get(
|
||||
"/-/allowed.json?action=view-database",
|
||||
cookies={"ds_actor": ds.client.actor_cookie({"id": "root"})},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Check that content database is NOT in the allowed list
|
||||
allowed_databases = [item["parent"] for item in data["items"]]
|
||||
assert "content" not in allowed_databases, (
|
||||
f"Root user should not see 'content' database when settings deny it, "
|
||||
f"but found it in: {allowed_databases}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_root_user_respects_settings_deny_tables():
|
||||
"""
|
||||
Test for issue #2509: Settings-based deny rules should override root for tables too.
|
||||
|
||||
When a database has `allow: false` in settings, the root user should NOT see
|
||||
tables from that database in /-/allowed.json?action=view-table.
|
||||
"""
|
||||
ds = Datasette(
|
||||
config={
|
||||
"databases": {
|
||||
"content": {
|
||||
"allow": False, # Deny everyone, including root
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
ds.root_enabled = True
|
||||
await ds.invoke_startup()
|
||||
|
||||
# Add a database with a table
|
||||
db = ds.add_memory_database("content")
|
||||
await db.execute_write("CREATE TABLE repos (id INTEGER PRIMARY KEY, name TEXT)")
|
||||
await ds.refresh_schemas()
|
||||
|
||||
# Root user should NOT see tables from the content database
|
||||
response = await ds.client.get(
|
||||
"/-/allowed.json?action=view-table",
|
||||
cookies={"ds_actor": ds.client.actor_cookie({"id": "root"})},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Check that content.repos table is NOT in the allowed list
|
||||
content_tables = [
|
||||
item["child"] for item in data["items"] if item["parent"] == "content"
|
||||
]
|
||||
assert "repos" not in content_tables, (
|
||||
f"Root user should not see tables from 'content' database when settings deny it, "
|
||||
f"but found: {content_tables}"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue