From e5f392ae7a3aad7f778e7d6be7e06b1ad0b84878 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Fri, 31 Oct 2025 15:07:37 -0700 Subject: [PATCH] datasette.allowed_resources_sql() returns namedtuple --- datasette/app.py | 10 +++++++--- docs/internals.rst | 2 +- tests/test_internals_datasette.py | 13 ++++++++++++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/datasette/app.py b/datasette/app.py index 7b9fb67d..5a3d59eb 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -248,6 +248,9 @@ FAVICON_PATH = app_root / "datasette" / "static" / "favicon.png" DEFAULT_NOT_SET = object() +ResourcesSQL = collections.namedtuple("ResourcesSQL", ("sql", "params")) + + async def favicon(request, send): await asgi_send_file( send, @@ -1110,7 +1113,7 @@ class Datasette: actor: dict | None = None, parent: str | None = None, include_is_private: bool = False, - ) -> tuple[str, dict]: + ) -> ResourcesSQL: """ Build SQL query to get all resources the actor can access for the given action. @@ -1120,7 +1123,7 @@ class Datasette: parent: Optional parent filter (e.g., database name) to limit results include_is_private: If True, include is_private column showing if anonymous cannot access - Returns a tuple of (query: str, params: dict) that can be executed against the internal database. + Returns a namedtuple of (query: str, params: dict) that can be executed against the internal database. The query returns rows with (parent, child, reason) columns, plus is_private if requested. Example: @@ -1138,9 +1141,10 @@ class Datasette: if not action_obj: raise ValueError(f"Unknown action: {action}") - return await build_allowed_resources_sql( + sql, params = await build_allowed_resources_sql( self, actor, action, parent=parent, include_is_private=include_is_private ) + return ResourcesSQL(sql, params) async def allowed_resources( self, diff --git a/docs/internals.rst b/docs/internals.rst index f0d3c99a..0132fddf 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -467,7 +467,7 @@ This method uses :ref:`datasette_allowed_resources_sql` under the hood and is an await .allowed_resources_sql(\*, action, actor=None, parent=None, include_is_private=False) ------------------------------------------------------------------------------------------- -Builds the SQL query that Datasette uses to determine which resources an actor may access for a specific action. Returns a ``(sql: str, params: dict)`` tuple that can be executed against the internal ``catalog_*`` database tables. ``parent`` can be used to limit results to a specific database, and ``include_is_private`` adds a column indicating whether anonymous users would be denied access to that resource. +Builds the SQL query that Datasette uses to determine which resources an actor may access for a specific action. Returns a ``(sql: str, params: dict)`` namedtuple that can be executed against the internal ``catalog_*`` database tables. ``parent`` can be used to limit results to a specific database, and ``include_is_private`` adds a column indicating whether anonymous users would be denied access to that resource. Plugins that need to execute custom analysis over the raw allow/deny rules can use this helper to run the same query that powers the ``/-/allowed`` debugging interface. diff --git a/tests/test_internals_datasette.py b/tests/test_internals_datasette.py index 60bcfe25..c64620a6 100644 --- a/tests/test_internals_datasette.py +++ b/tests/test_internals_datasette.py @@ -4,7 +4,7 @@ Tests for the datasette.app.Datasette class import dataclasses from datasette import Context -from datasette.app import Datasette, Database +from datasette.app import Datasette, Database, ResourcesSQL from datasette.resources import DatabaseResource from itsdangerous import BadSignature import pytest @@ -195,3 +195,14 @@ async def test_apply_metadata_json(): assert (await ds.client.get("/")).status_code == 200 value = (await ds.get_instance_metadata()).get("weird_instance_value") assert value == '{"nested": [1, 2, 3]}' + + +@pytest.mark.asyncio +async def test_allowed_resources_sql(datasette): + result = await datasette.allowed_resources_sql( + action="view-table", + actor=None, + ) + assert isinstance(result, ResourcesSQL) + assert "all_rules AS" in result.sql + assert result.params["action"] == "view-table"