From c01e95f3bde22e43eb0cc293caff0757e61b5f90 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 8 Sep 2026 21:12:33 -0700 Subject: [PATCH] Filter foreign-key helper targets by view permission --- datasette/views/table_create_alter.py | 41 +++++++++++++++++++++++++++ tests/test_api_write.py | 6 ++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/datasette/views/table_create_alter.py b/datasette/views/table_create_alter.py index f8f8c31e..47a2a39e 100644 --- a/datasette/views/table_create_alter.py +++ b/datasette/views/table_create_alter.py @@ -27,6 +27,11 @@ from datasette.utils import ( table_column_details, ) from datasette.utils.asgi import NotFound, PayloadTooLarge, Response +from datasette.utils.permissions import ( + SKIP_PERMISSION_CHECKS, + gather_permission_sql_from_hooks, + resolve_permissions_with_candidates, +) from datasette.utils.sqlite import sqlite_hidden_table_names from .base import BaseView @@ -122,6 +127,30 @@ def _public_foreign_key_target(target): } +async def _filter_visible_foreign_key_targets(datasette, actor, database_name, targets): + if not targets: + return [] + + permission_sqls = await gather_permission_sql_from_hooks( + datasette=datasette, + actor=actor, + action="view-table", + ) + if permission_sqls is SKIP_PERMISSION_CHECKS: + return targets + + candidate_tables = list(dict.fromkeys(target["fk_table"] for target in targets)) + permission_rows = await resolve_permissions_with_candidates( + datasette.get_internal_database(), + actor, + permission_sqls, + [(database_name, table_name) for table_name in candidate_tables], + "view-table", + ) + visible_tables = {row["child"] for row in permission_rows if bool(row["allow"])} + return [target for target in targets if target["fk_table"] in visible_tables] + + def _singular(name): if name.endswith("ies") and len(name) > 3: return name[:-3] + "y" @@ -1014,6 +1043,9 @@ class DatabaseForeignKeyTargetsView(BaseView): for target in (await db.execute(FOREIGN_KEY_TARGETS_SQL)).dicts() if target["fk_table"] not in hidden_tables ] + targets = await _filter_visible_foreign_key_targets( + self.ds, request.actor, database_name, targets + ) return Response.json( { "ok": True, @@ -1052,6 +1084,15 @@ class TableForeignKeySuggestionsView(BaseView): source_columns, targets, current_by_column = await db.execute_fn( lambda conn: _foreign_key_suggestion_metadata(conn, table_name) ) + targets = await _filter_visible_foreign_key_targets( + self.ds, request.actor, database_name, targets + ) + visible_target_tables = {target["fk_table"] for target in targets} + current_by_column = { + column: current + for column, current in current_by_column.items() + if current["fk_table"] in visible_target_tables + } columns = [] options_by_column = {} diff --git a/tests/test_api_write.py b/tests/test_api_write.py index 1c560cf5..839a50bc 100644 --- a/tests/test_api_write.py +++ b/tests/test_api_write.py @@ -1296,7 +1296,7 @@ async def test_alter_table_foreign_key_without_fk_column_requires_single_pk(ds_w @pytest.mark.asyncio async def test_foreign_key_suggestions(ds_write): - token = write_token(ds_write, permissions=["at"]) + token = write_token(ds_write, permissions=["alter-table", "view-table"]) db = ds_write.get_database("data") await db.execute_write("create table owners (id integer primary key)") await db.execute_write("insert into owners (id) values (1), (2), (3)") @@ -1362,7 +1362,7 @@ async def test_foreign_key_suggestions_permission_denied(ds_write): @pytest.mark.asyncio async def test_foreign_key_suggestions_fail_open(ds_write, monkeypatch): - token = write_token(ds_write, permissions=["at"]) + token = write_token(ds_write, permissions=["alter-table", "view-table"]) db = ds_write.get_database("data") await db.execute_write("create table owners (id integer primary key)") @@ -1393,7 +1393,7 @@ async def test_foreign_key_suggestions_fail_open(ds_write, monkeypatch): @pytest.mark.asyncio async def test_foreign_key_targets(ds_write): - token = write_token(ds_write, permissions=["ct"]) + token = write_token(ds_write, permissions=["create-table", "view-table"]) db = ds_write.get_database("data") await db.execute_write("create table owners (id integer primary key)") await db.execute_write("create table categories (slug varchar(30) primary key)")