Filter foreign-key helper targets by view permission

This commit is contained in:
Simon Willison 2026-09-08 21:12:33 -07:00
commit c01e95f3bd
2 changed files with 44 additions and 3 deletions

View file

@ -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 = {}

View file

@ -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)")