Migrate /database view to use bulk allowed_resources()

Replace one-by-one permission checks with bulk allowed_resources() call:
- DatabaseView and QueryView now fetch all allowed tables once
- Filter views and tables using pre-fetched allowed_table_set
- Update TableResource.resources_sql() to include views from catalog_views

This improves performance by reducing permission checks from O(n) to O(1) per
table/view, where n is the number of tables in the database.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Simon Willison 2025-10-24 00:28:16 -07:00
commit 2620938661
2 changed files with 40 additions and 15 deletions

View file

@ -48,6 +48,9 @@ class TableResource(Resource):
return """ return """
SELECT database_name AS parent, table_name AS child SELECT database_name AS parent, table_name AS child
FROM catalog_tables FROM catalog_tables
UNION ALL
SELECT database_name AS parent, view_name AS child
FROM catalog_views
""" """

View file

@ -71,17 +71,26 @@ class DatabaseView(View):
metadata = await datasette.get_database_metadata(database) metadata = await datasette.get_database_metadata(database)
# Get all tables/views this actor can see in bulk
from datasette.resources import TableResource
allowed_tables = await datasette.allowed_resources("view-table", request.actor)
allowed_table_set = {
(r.parent, r.child) for r in allowed_tables if r.parent == database
}
sql_views = [] sql_views = []
for view_name in await db.view_names(): for view_name in await db.view_names():
view_visible, view_private = await datasette.check_visibility( if (database, view_name) in allowed_table_set:
request.actor, # Check if it's private (requires elevated permissions)
permissions=[ _, view_private = await datasette.check_visibility(
("view-table", (database, view_name)), request.actor,
("view-database", database), permissions=[
"view-instance", ("view-table", (database, view_name)),
], ("view-database", database),
) "view-instance",
if view_visible: ],
)
sql_views.append( sql_views.append(
{ {
"name": view_name, "name": view_name,
@ -89,7 +98,7 @@ class DatabaseView(View):
} }
) )
tables = await get_tables(datasette, request, db) tables = await get_tables(datasette, request, db, allowed_table_set)
canned_queries = [] canned_queries = []
for query in ( for query in (
await datasette.get_canned_queries(database, request.actor) await datasette.get_canned_queries(database, request.actor)
@ -332,7 +341,7 @@ class QueryContext(Context):
) )
async def get_tables(datasette, request, db): async def get_tables(datasette, request, db, allowed_table_set):
tables = [] tables = []
database = db.name database = db.name
table_counts = await db.table_counts(100) table_counts = await db.table_counts(100)
@ -340,7 +349,11 @@ async def get_tables(datasette, request, db):
all_foreign_keys = await db.get_all_foreign_keys() all_foreign_keys = await db.get_all_foreign_keys()
for table in table_counts: for table in table_counts:
table_visible, table_private = await datasette.check_visibility( if (database, table) not in allowed_table_set:
continue
# Check if it's private (requires elevated permissions)
_, table_private = await datasette.check_visibility(
request.actor, request.actor,
permissions=[ permissions=[
("view-table", (database, table)), ("view-table", (database, table)),
@ -348,8 +361,7 @@ async def get_tables(datasette, request, db):
"view-instance", "view-instance",
], ],
) )
if not table_visible:
continue
table_columns = await db.table_columns(table) table_columns = await db.table_columns(table)
tables.append( tables.append(
{ {
@ -509,6 +521,14 @@ class QueryView(View):
db = await datasette.resolve_database(request) db = await datasette.resolve_database(request)
database = db.name database = db.name
# Get all tables/views this actor can see in bulk
from datasette.resources import TableResource
allowed_tables = await datasette.allowed_resources("view-table", request.actor)
allowed_table_set = {
(r.parent, r.child) for r in allowed_tables if r.parent == database
}
# Are we a canned query? # Are we a canned query?
canned_query = None canned_query = None
canned_query_write = False canned_query_write = False
@ -808,7 +828,9 @@ class QueryView(View):
show_hide_text=show_hide_text, show_hide_text=show_hide_text,
editable=not canned_query, editable=not canned_query,
allow_execute_sql=allow_execute_sql, allow_execute_sql=allow_execute_sql,
tables=await get_tables(datasette, request, db), tables=await get_tables(
datasette, request, db, allowed_table_set
),
named_parameter_values=named_parameter_values, named_parameter_values=named_parameter_values,
edit_sql_url=edit_sql_url, edit_sql_url=edit_sql_url,
display_rows=await display_rows( display_rows=await display_rows(