diff --git a/datasette/app.py b/datasette/app.py index 0227f627..618c0ecc 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -935,7 +935,7 @@ class Datasette: log_sql_errors=log_sql_errors, ) - async def expand_foreign_keys(self, database, table, column, values): + async def expand_foreign_keys(self, actor, database, table, column, values): """Returns dict mapping (column, value) -> label""" labeled_fks = {} db = self.databases[database] @@ -949,6 +949,13 @@ class Datasette: ][0] except IndexError: return {} + # Ensure user has permission to view the referenced table + if not await self.permission_allowed( + actor=actor, + action="view-table", + resource=(database, fk["other_table"]), + ): + return {} label_column = await db.label_column_for_table(fk["other_table"]) if not label_column: return {(fk["column"], value): str(value) for value in values} diff --git a/datasette/facets.py b/datasette/facets.py index 7fb0c68b..b23615fe 100644 --- a/datasette/facets.py +++ b/datasette/facets.py @@ -253,7 +253,7 @@ class ColumnFacet(Facet): # Attempt to expand foreign keys into labels values = [row["value"] for row in facet_rows] expanded = await self.ds.expand_foreign_keys( - self.database, self.table, column, values + self.request.actor, self.database, self.table, column, values ) else: expanded = {} diff --git a/datasette/views/table.py b/datasette/views/table.py index 6df8b915..50ba2b78 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -1144,7 +1144,7 @@ async def table_view_data( # Expand them expanded_labels.update( await datasette.expand_foreign_keys( - database_name, table_name, column, values + request.actor, database_name, table_name, column, values ) ) if expanded_labels: diff --git a/tests/test_table_html.py b/tests/test_table_html.py index c4c7878c..e66eb6f0 100644 --- a/tests/test_table_html.py +++ b/tests/test_table_html.py @@ -1204,3 +1204,56 @@ async def test_format_of_binary_links(size, title, length_bytes): sql_response = await ds.client.get("/{}".format(db_name), params={"sql": sql}) assert sql_response.status_code == 200 assert expected in sql_response.text + + +@pytest.mark.asyncio +async def test_foreign_key_labels_obey_permissions(): + ds = Datasette( + metadata={ + "databases": { + "foreign_key_labels": { + "tables": { + # Table a is only visible to root + "a": {"allow": {"id": "root"}}, + } + } + } + } + ) + db = ds.add_memory_database("foreign_key_labels") + await db.execute_write("create table a(id integer primary key, name text)") + await db.execute_write("insert into a (id, name) values (1, 'hello')") + await db.execute_write( + "create table b(id integer primary key, name text, a_id integer references a(id))" + ) + await db.execute_write("insert into b (id, name, a_id) values (1, 'world', 1)") + # Anonymous user can see table b but not table a + blah = await ds.client.get("/foreign_key_labels.json") + anon_a = await ds.client.get("/foreign_key_labels/a.json?_labels=on") + assert anon_a.status_code == 403 + anon_b = await ds.client.get("/foreign_key_labels/b.json?_labels=on") + assert anon_b.status_code == 200 + # root user can see both + cookies = {"ds_actor": ds.sign({"a": {"id": "root"}}, "actor")} + root_a = await ds.client.get( + "/foreign_key_labels/a.json?_labels=on", cookies=cookies + ) + assert root_a.status_code == 200 + root_b = await ds.client.get( + "/foreign_key_labels/b.json?_labels=on", cookies=cookies + ) + assert root_b.status_code == 200 + # Labels should have been expanded for root + assert root_b.json() == { + "ok": True, + "next": None, + "rows": [{"id": 1, "name": "world", "a_id": {"value": 1, "label": "hello"}}], + "truncated": False, + } + # But not for anon + assert anon_b.json() == { + "ok": True, + "next": None, + "rows": [{"id": 1, "name": "world", "a_id": 1}], + "truncated": False, + }