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

@ -17,13 +17,6 @@ UNION/INTERSECT operations. The order of evaluation is:
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from datasette.app import Datasette
from datasette import hookimpl
# Re-export all hooks and public utilities
from .restrictions import (
actor_restrictions_sql as actor_restrictions_sql,
@ -38,12 +31,3 @@ from .defaults import (
default_query_permissions_sql as default_query_permissions_sql,
DEFAULT_ALLOW_ACTIONS as DEFAULT_ALLOW_ACTIONS,
)
@hookimpl
def canned_queries(datasette: "Datasette", database: str, actor) -> dict:
"""Return canned queries defined in datasette.yaml configuration."""
queries = (
((datasette.config or {}).get("databases") or {}).get(database) or {}
).get("queries") or {}
return queries

View file

@ -74,6 +74,22 @@ async def default_query_permissions_sql(
actor: Optional[dict],
action: str,
) -> Optional[PermissionSQL]:
actor_id = actor.get("id") if isinstance(actor, dict) else None
if action in {"update-query", "delete-query"}:
if actor_id is None:
return None
return PermissionSQL(
sql="""
SELECT database_name AS parent, name AS child, 1 AS allow,
'query owner' AS reason
FROM queries
WHERE source = 'user'
AND owner_id = :query_owner_id
""",
params={"query_owner_id": actor_id},
)
if action != "view-query":
return None
@ -98,6 +114,19 @@ async def default_query_permissions_sql(
AND source IN ('config', 'plugin')
"""
user_writable_sql = ""
if actor_id is not None:
params["query_owner_id"] = actor_id
user_writable_sql = """
UNION ALL
SELECT database_name AS parent, name AS child, 1 AS allow,
'query owner' AS reason
FROM queries
WHERE is_write = 1
AND source = 'user'
AND owner_id = :query_owner_id
"""
return PermissionSQL(
sql=f"""
WITH execute_sql_allowed AS (
@ -118,6 +147,7 @@ async def default_query_permissions_sql(
WHERE q.is_write = 0
AND q.published = 0
{trusted_writable_sql}
{user_writable_sql}
""",
params=params,
)

View file

@ -137,11 +137,6 @@ def permission_resources_sql(datasette, actor, action):
"""
@hookspec
def canned_queries(datasette, database, actor):
"""Return a dictionary of canned query definitions or an awaitable function that returns them"""
@hookspec
def register_magic_parameters(datasette):
"""Return a list of (name, function) magic parameter functions"""

View file

@ -945,6 +945,18 @@ class QueryView(View):
# That should not have happened
raise DatasetteError("Unexpected table found on POST", status=404)
if not await datasette.allowed(
action="view-query",
resource=QueryResource(database=db.name, query=canned_query["name"]),
actor=request.actor,
):
raise Forbidden("You do not have permission to view this query")
if canned_query.get("write") and canned_query.get("source") == "user":
await datasette.ensure_query_write_permissions(
db.name, canned_query["sql"], actor=request.actor
)
# If database is immutable, return an error
if not db.is_mutable:
raise Forbidden("Database is immutable")