Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion datasette/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ async def inspect_(files, sqlite_extensions):
app = Datasette([], immutables=files, sqlite_extensions=sqlite_extensions)
data = {}
for name, database in app.databases.items():
counts = await database.table_counts(limit=3600 * 1000)
counts = await database.table_counts(limit=3600 * 1000, exact=True)
data[name] = {
"hash": database.hash,
"size": database.size,
Expand Down
13 changes: 7 additions & 6 deletions datasette/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,25 +561,26 @@ def size(self):
self.cached_size = Path(self.path).stat().st_size
return self.cached_size

async def table_counts(self, limit=10):
async def table_counts(self, limit=10, exact=False):
if not self.is_mutable and self.cached_table_counts is not None:
return self.cached_table_counts
# Try to get counts for each table, $limit timeout for each count
counts = {}
for table in await self.table_names():
try:
if exact:
sql = f"select count(*) from [{table}]"
else:
sql = f"select count(*) from (select * from [{table}] limit {self.count_limit + 1})"
table_count = (
await self.execute(
f"select count(*) from (select * from [{table}] limit {self.count_limit + 1})",
custom_time_limit=limit,
)
await self.execute(sql, custom_time_limit=limit)
).rows[0][0]
counts[table] = table_count
# In some cases I saw "SQL Logic Error" here in addition to
# QueryInterrupted - so we catch that too:
except (QueryInterrupted, sqlite3.OperationalError, sqlite3.DatabaseError):
counts[table] = None
if not self.is_mutable:
if not self.is_mutable and not exact:
self._cached_table_counts = counts
return counts

Expand Down
14 changes: 14 additions & 0 deletions datasette/utils/actions_sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,20 @@ async def _build_single_action_sql(
"),",
]
)
else:
# The anon_*_lvl CTEs below LEFT JOIN anon_rules unconditionally,
# so when no anonymous rules are produced (e.g. --default-deny
# without a config file) we still need an empty CTE with the
# expected columns. Without it the whole permissions query is
# invalid SQL and the index pages 500.
query_parts.extend(
[
"anon_rules AS (",
" SELECT NULL AS parent, NULL AS child, NULL AS allow,",
" NULL AS reason, NULL AS source_plugin WHERE 0",
"),",
]
)

# Continue with the cascading logic
query_parts.extend(
Expand Down
22 changes: 22 additions & 0 deletions tests/test_default_deny.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,25 @@ async def test_default_deny_basic_permissions():

# Authenticated user without explicit permission should also be denied
assert await ds.allowed(action="view-instance", actor={"id": "user"}) is False


@pytest.mark.asyncio
async def test_default_deny_no_config_index_pages_dont_500():
"""Regression for #2644: root visiting the index pages on a --default-deny
instance without any config file used to return 500 because the anon_rules
CTE was conditionally emitted but unconditionally referenced."""
ds = Datasette(default_deny=True)
ds.root_enabled = True
await ds.invoke_startup()

db = ds.add_memory_database("test_db_no_config")
await db.execute_write("create table test_table (id integer primary key)")
await ds._refresh_schemas()

cookies = {"ds_actor": ds.client.actor_cookie({"id": "root"})}

response = await ds.client.get("/", cookies=cookies)
assert response.status_code == 200

response = await ds.client.get("/test_db_no_config", cookies=cookies)
assert response.status_code == 200