From 5598c5de011db95396b65b5c8c251cbe6884d6ae Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Mon, 8 Jun 2020 11:34:14 -0700 Subject: [PATCH] Database list on index page respects table/view permissions, refs #811 --- datasette/templates/index.html | 2 +- datasette/views/index.py | 25 ++++++++++++++++++++----- tests/test_permissions.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/datasette/templates/index.html b/datasette/templates/index.html index 5a8dccae..c1adfc59 100644 --- a/datasette/templates/index.html +++ b/datasette/templates/index.html @@ -22,7 +22,7 @@ {% endif %}

{% for table in database.tables_and_views_truncated %}{{ table.name }}{% if not loop.last %}, {% endif %}{% endfor %}{% if database.tables_and_views_more %}, ...{% endif %}

+ }}"{% if table.count %} title="{{ table.count }} rows"{% endif %}>{{ table.name }}{% if table.private %} 🔒{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}{% if database.tables_and_views_more %}, ...{% endif %}

{% endfor %} {% endblock %} diff --git a/datasette/views/index.py b/datasette/views/index.py index 59d3e042..a3e8388c 100644 --- a/datasette/views/index.py +++ b/datasette/views/index.py @@ -25,14 +25,22 @@ class IndexView(BaseView): await self.check_permission(request, "view-instance") databases = [] for name, db in self.ds.databases.items(): - visible, private = await check_visibility( + visible, database_private = await check_visibility( self.ds, request.actor, "view-database", "database", name, ) if not visible: continue table_names = await db.table_names() hidden_table_names = set(await db.hidden_table_names()) - views = await db.view_names() + + views = [] + for view_name in await db.view_names(): + visible, private = await check_visibility( + self.ds, request.actor, "view-table", "table", (name, view_name), + ) + if visible: + views.append({"name": view_name, "private": private}) + # Perform counts only for immutable or DBS with <= COUNT_TABLE_LIMIT tables table_counts = {} if not db.is_mutable or db.size < COUNT_DB_SIZE_LIMIT: @@ -40,8 +48,14 @@ class IndexView(BaseView): # If any of these are None it means at least one timed out - ignore them all if any(v is None for v in table_counts.values()): table_counts = {} + tables = {} for table in table_names: + visible, private = await check_visibility( + self.ds, request.actor, "view-table", "table", (name, table), + ) + if not visible: + continue table_columns = await db.table_columns(table) tables[table] = { "name": table, @@ -51,6 +65,7 @@ class IndexView(BaseView): "hidden": table in hidden_table_names, "fts_table": await db.fts_table(table), "num_relationships_for_sorting": 0, + "private": private, } if request.args.get("_sort") == "relationships" or not table_counts: @@ -78,8 +93,8 @@ class IndexView(BaseView): # Only add views if this is less than TRUNCATE_AT if len(tables_and_views_truncated) < TRUNCATE_AT: num_views_to_add = TRUNCATE_AT - len(tables_and_views_truncated) - for view_name in views[:num_views_to_add]: - tables_and_views_truncated.append({"name": view_name}) + for view in views[:num_views_to_add]: + tables_and_views_truncated.append(view) databases.append( { @@ -100,7 +115,7 @@ class IndexView(BaseView): ), "hidden_tables_count": len(hidden_tables), "views_count": len(views), - "private": private, + "private": database_private, } ) diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 5c338e04..475f93dd 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -74,6 +74,37 @@ def test_database_list_respects_view_database(): assert 'fixtures 🔒' in auth_response.text +def test_database_list_respects_view_table(): + with make_app_client( + metadata={ + "databases": { + "data": { + "tables": { + "names": {"allow": {"id": "root"}}, + "v": {"allow": {"id": "root"}}, + } + } + } + }, + extra_databases={ + "data.db": "create table names (name text); create view v as select * from names" + }, + ) as client: + html_fragments = [ + ">names 🔒", + ">v 🔒", + ] + anon_response_text = client.get("/").text + assert "0 rows in 0 tables" in anon_response_text + for html_fragment in html_fragments: + assert html_fragment not in anon_response_text + auth_response_text = client.get( + "/", cookies={"ds_actor": client.ds.sign({"id": "root"}, "actor")}, + ).text + for html_fragment in html_fragments: + assert html_fragment in auth_response_text + + @pytest.mark.parametrize( "allow,expected_anon,expected_auth", [(None, 200, 200), ({}, 403, 403), ({"id": "root"}, 403, 200),],