Enforce query ownership and remove canned query hook

Refs #2735
This commit is contained in:
Simon Willison 2026-05-24 22:58:50 -07:00
commit 040e42ddca
11 changed files with 182 additions and 99 deletions

View file

@ -1,4 +1,5 @@
import collections
from asgiref.sync import async_to_sync
from datasette.app import Datasette
from datasette.cli import cli
from datasette.default_permissions import restrictions_allow_action
@ -609,6 +610,10 @@ def test_padlocks_on_database_page(cascade_app_client):
previous_config = cascade_app_client.ds.config
try:
cascade_app_client.ds.config = config
async_to_sync(cascade_app_client.ds.invoke_startup)()
async_to_sync(cascade_app_client.ds.add_query)(
"fixtures", "query_two", "select 2", source="config"
)
response = cascade_app_client.get(
"/fixtures",
cookies={"ds_actor": cascade_app_client.actor_cookie({"id": "test"})},
@ -624,6 +629,7 @@ def test_padlocks_on_database_page(cascade_app_client):
assert ">simple_view</a></li>" in response.text
finally:
cascade_app_client.ds.config = previous_config
async_to_sync(cascade_app_client.ds.remove_query)("fixtures", "query_two")
@pytest.mark.asyncio
@ -954,39 +960,20 @@ async def test_permissions_in_config(
@pytest.mark.asyncio
async def test_allowed_resources_view_query_includes_actor_specific_canned_queries():
"""
Actor-specific canned queries should be listed by allowed_resources("view-query").
This test is intentionally explicit about the previous bug:
- the canned query only exists for actor "alice"
- the permission rule only allows actor "alice" to view it
- allowed() succeeds for that specific query resource
- allowed_resources("view-query", actor) must include the same query
Before the fix, QueryResource.resources_sql() called canned_queries(..., actor=None),
so the query was omitted from resource enumeration and allowed_resources() returned
an empty list even though allowed() returned True.
"""
async def test_allowed_resources_view_query_includes_actor_specific_query_permissions():
from datasette import hookimpl
from datasette.permissions import PermissionSQL
from datasette.resources import QueryResource
class ActorSpecificQueryPlugin:
__name__ = "ActorSpecificQueryPlugin"
@hookimpl
def canned_queries(self, datasette, database, actor):
if database == "testdb" and actor and actor.get("id") == "alice":
return {"user_only": {"sql": "select 1 as n"}}
return {}
class ActorSpecificQueryPermissionPlugin:
__name__ = "ActorSpecificQueryPermissionPlugin"
@hookimpl
def permission_resources_sql(self, datasette, actor, action):
if action == "view-query" and actor and actor.get("id") == "alice":
return PermissionSQL(sql="""
SELECT 'testdb' AS parent, 'user_only' AS child, 1 AS allow,
'alice can view her actor-specific canned query' AS reason
'alice can view this query' AS reason
""")
return None
@ -994,9 +981,10 @@ async def test_allowed_resources_view_query_includes_actor_specific_canned_queri
await ds.invoke_startup()
ds.add_memory_database("testdb")
await ds._refresh_schemas()
await ds.add_query("testdb", "user_only", "select 1 as n")
plugin = ActorSpecificQueryPlugin()
ds.pm.register(plugin, name="actor_specific_query_plugin")
plugin = ActorSpecificQueryPermissionPlugin()
ds.pm.register(plugin, name="actor_specific_query_permission_plugin")
try:
actor = {"id": "alice"}
@ -1012,7 +1000,7 @@ async def test_allowed_resources_view_query_includes_actor_specific_canned_queri
("testdb", "user_only")
]
finally:
ds.pm.unregister(name="actor_specific_query_plugin")
ds.pm.unregister(name="actor_specific_query_permission_plugin")
@pytest.mark.asyncio