diff --git a/datasette/app.py b/datasette/app.py index 2df6e4e8e1..b521a8c74a 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -1820,6 +1820,7 @@ async def menu_links(): "base_url": self.setting("base_url"), "csrftoken": request.scope["csrftoken"] if request else lambda: "", "datasette_version": __version__, + "default_deny": self.default_deny, }, **extra_template_vars, } diff --git a/datasette/templates/api_explorer.html b/datasette/templates/api_explorer.html index dc393c203e..cb9da81136 100644 --- a/datasette/templates/api_explorer.html +++ b/datasette/templates/api_explorer.html @@ -8,7 +8,7 @@ {% block content %} -

API Explorer{% if private %} 🔒{% endif %}

+

API Explorer{% if default_deny and not private %} 🌐{% elif not default_deny and private %} 🔒{% endif %}

Use this tool to try out the {% if datasette_version %} diff --git a/datasette/templates/database.html b/datasette/templates/database.html index 42b4ca0b73..25207f91d9 100644 --- a/datasette/templates/database.html +++ b/datasette/templates/database.html @@ -15,7 +15,7 @@ {% block content %}

{% set action_links, action_title = database_actions(), "Database actions" %} {% include "_action_menu.html" %} @@ -50,7 +50,7 @@

Custom SQL query

Queries

{% endif %} @@ -62,7 +62,7 @@

Tables -

{{ table.name }}{% if table.private %} 🔒{% endif %}{% if table.hidden %} (hidden){% endif %}

+

{{ table.name }}{% if default_deny and not table.private %} 🌐{% elif not default_deny and table.private %} 🔒{% endif %}{% if table.hidden %} (hidden){% endif %}

{% for column in table.columns %}{{ column }}{% if not loop.last %}, {% endif %}{% endfor %}

{% if table.count is none %}Many rows{% elif table.count == count_limit + 1 %}>{{ "{:,}".format(count_limit) }} rows{% else %}{{ "{:,}".format(table.count) }} row{% if table.count == 1 %}{% else %}s{% endif %}{% endif %}

@@ -77,7 +77,7 @@

{{ table.name }}{% if t

Views

{% endif %} diff --git a/datasette/templates/index.html b/datasette/templates/index.html index 0334927983..57bdb4e9d9 100644 --- a/datasette/templates/index.html +++ b/datasette/templates/index.html @@ -9,7 +9,7 @@ {% block body_class %}index{% endblock %} {% block content %} -

{{ metadata.title or "Datasette" }}{% if private %} 🔒{% endif %}

+

{{ metadata.title or "Datasette" }}{% if default_deny and not private %} 🌐{% elif not default_deny and private %} 🔒{% endif %}

{% set action_links, action_title = homepage_actions, "Homepage actions" %} {% include "_action_menu.html" %} @@ -19,7 +19,7 @@

{{ metadata.title or "Datasette" }}{% if private %} 🔒{% endif %}

{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %} {% for database in databases %} -

{{ database.name }}{% if database.private %} 🔒{% endif %}

+

{{ database.name }}{% if default_deny and not database.private %} 🌐{% elif not default_deny and database.private %} 🔒{% endif %}

{% if database.show_table_row_counts %}{{ "{:,}".format(database.table_rows_sum) }} rows in {% endif %}{{ database.tables_count }} table{% if database.tables_count != 1 %}s{% endif %}{% if database.hidden_tables_count %}, {% endif -%} {% if database.hidden_tables_count -%} @@ -30,7 +30,7 @@

-

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

+

{% for table in database.tables_and_views_truncated %}{{ table.name }}{% if default_deny and not table.private %} 🌐{% elif not default_deny and table.private %} 🔒{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}{% if database.tables_and_views_more %}, ...{% endif %}

{% endfor %} {% endblock %} diff --git a/datasette/templates/query.html b/datasette/templates/query.html index a6e9a3aa52..9621237cc4 100644 --- a/datasette/templates/query.html +++ b/datasette/templates/query.html @@ -28,7 +28,7 @@

This query cannot be executed because the database is immutable.

{% endif %} -

{{ metadata.title or database }}{% if canned_query and not metadata.title %}: {{ canned_query }}{% endif %}{% if private %} 🔒{% endif %}

+

{{ metadata.title or database }}{% if canned_query and not metadata.title %}: {{ canned_query }}{% endif %}{% if default_deny and not private %} 🌐{% elif not default_deny and private %} 🔒{% endif %}

{% set action_links, action_title = query_actions(), "Query actions" %} {% include "_action_menu.html" %} diff --git a/datasette/templates/row.html b/datasette/templates/row.html index db43e71a60..0a19f9a22c 100644 --- a/datasette/templates/row.html +++ b/datasette/templates/row.html @@ -20,7 +20,7 @@ {% endblock %} {% block content %} -

{{ table }}: {{ ', '.join(primary_key_values) }}{% if private %} 🔒{% endif %}

+

{{ table }}: {{ ', '.join(primary_key_values) }}{% if default_deny and not private %} 🌐{% elif not default_deny and private %} 🔒{% endif %}

{% set action_links, action_title = row_actions, "Row actions" %} {% include "_action_menu.html" %} diff --git a/datasette/templates/table.html b/datasette/templates/table.html index 25ff31efaf..9237d3d632 100644 --- a/datasette/templates/table.html +++ b/datasette/templates/table.html @@ -22,7 +22,7 @@ {% block content %} {% set action_links, action_title = actions(), "View actions" if is_view else "Table actions" %} {% include "_action_menu.html" %} diff --git a/datasette/views/table.py b/datasette/views/table.py index e1e5507f49..6211c51896 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -248,23 +248,34 @@ async def display_columns_and_rows( display_value = plugin_display_value elif isinstance(value, bytes): formatted = format_bytes(len(value)) - display_value = markupsafe.Markup( - '<Binary: {:,} byte{}>'.format( - datasette.urls.row_blob( - database_name, - table_name, - path_from_row_pks(row, pks, not pks), - column, - ), - ( - ' title="{}"'.format(formatted) - if "bytes" not in formatted - else "" - ), - len(value), - "" if len(value) == 1 else "s", - ) + title = ( + ' title="{}"'.format(formatted) if "bytes" not in formatted else "" ) + try: + blob_path = path_from_row_pks(row, pks, not pks) + except (IndexError, KeyError): + blob_path = None + if blob_path is not None: + display_value = markupsafe.Markup( + '<Binary: {:,} byte{}>'.format( + datasette.urls.row_blob( + database_name, + table_name, + blob_path, + column, + ), + title, + len(value), + "" if len(value) == 1 else "s", + ) + ) + else: + display_value = markupsafe.Markup( + "<Binary: {:,} byte{}>".format( + len(value), + "" if len(value) == 1 else "s", + ) + ) elif isinstance(value, dict): # It's an expanded foreign key - display link to other row label = value["label"] diff --git a/tests/test_default_deny.py b/tests/test_default_deny.py index 81e95b845b..890c381291 100644 --- a/tests/test_default_deny.py +++ b/tests/test_default_deny.py @@ -96,6 +96,39 @@ async def test_default_deny_with_config_allow(): assert await ds.allowed(action="view-instance", actor={"id": "user2"}) is False +@pytest.mark.asyncio +async def test_default_deny_shows_public_icons_not_private_icons(): + ds = Datasette( + default_deny=True, + config={ + "allow": {"id": "user1"}, + "databases": { + "test_db": { + "allow": True, + "tables": { + "public_table": {"allow": True}, + "private_table": {"allow": {"id": "user1"}}, + }, + } + }, + }, + ) + await ds.invoke_startup() + + db = ds.add_memory_database("test_db") + await db.execute_write("create table public_table (id integer primary key)") + await db.execute_write("create table private_table (id integer primary key)") + await ds._refresh_schemas() + + cookie = ds.client.actor_cookie({"id": "user1"}) + response = await ds.client.get("/test_db", cookies={"ds_actor": cookie}) + assert response.status_code == 200 + + # In --default-deny mode show public icons, not private padlocks + assert ">public_table 🌐" in response.text + assert ">private_table 🔒" not in response.text + + @pytest.mark.asyncio async def test_default_deny_basic_permissions(): """Test that default_deny=True denies basic permissions""" diff --git a/tests/test_table_html.py b/tests/test_table_html.py index 00cf9e1902..45b187d94a 100644 --- a/tests/test_table_html.py +++ b/tests/test_table_html.py @@ -913,6 +913,50 @@ async def test_binary_data_display_in_table(ds_client): ] +@pytest.mark.asyncio +async def test_blob_column_in_join_view_does_not_error(tmp_path): + from datasette.utils.sqlite import sqlite3 + from datasette.views.table import display_columns_and_rows + + db_path = str(tmp_path / "data.db") + conn = sqlite3.connect(db_path) + conn.executescript(""" +create table foo(term, value); +create table bar(term, definition, bytes); + +insert into foo values ('text one', 1), ('text two', 2); +insert into bar values + ('text one', 'definition one', x'8af8ab88'), + ('text two', 'definition two', x'98246547'); + +create view foo_view as + select foo.*, bar.* from foo join bar on foo.term = bar.term; +""") + conn.close() + + ds = Datasette([db_path]) + await ds.invoke_startup() + internal_db = ds.get_internal_database() + await internal_db.execute_write( + "create table if not exists metadata_columns (database_name text, resource_name text, column_name text, key text, value text)" + ) + db = ds.databases["data"] + results = await db.execute("select * from foo_view") + description = [(column_name,) for column_name in results.columns] + + _, cell_rows = await display_columns_and_rows( + ds, + "data", + "foo_view", + description, + results.rows, + link_column=False, + ) + + binary_cell = next(cell for cell in cell_rows[0] if cell["column"] == "bytes") + assert "<Binary" in str(binary_cell["value"]) + + def test_custom_table_include(): with make_app_client( template_dir=str(pathlib.Path(__file__).parent / "test_templates")