mirror of
https://github.com/simonw/datasette.git
synced 2026-09-11 19:14:07 +02:00
295 lines
10 KiB
Python
295 lines
10 KiB
Python
import pytest
|
|
|
|
from datasette.app import Datasette
|
|
from datasette.resources import DatabaseResource, TableResource
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("fts_module", ("fts4", "fts5"))
|
|
async def test_external_content_fts_inherits_content_table_view_permission(fts_module):
|
|
actor = {"id": "reader"}
|
|
secret_marker = "ISSUE_17_EXTERNAL_CONTENT_FTS_SECRET"
|
|
ds = Datasette(
|
|
memory=True,
|
|
config={
|
|
"permissions": {
|
|
"view-instance": {"id": "reader"},
|
|
"view-database": {"id": "reader"},
|
|
"view-table": {"id": "reader"},
|
|
"execute-sql": {"id": "nobody"},
|
|
},
|
|
"databases": {
|
|
"data": {
|
|
"tables": {
|
|
"secret": {"permissions": {"view-table": False}},
|
|
}
|
|
}
|
|
},
|
|
},
|
|
)
|
|
db = ds.add_memory_database(f"issue_17_{fts_module}_permissions", name="data")
|
|
await db.execute_write("create table secret (id integer primary key, body text)")
|
|
await db.execute_write(
|
|
"insert into secret (body) values (?)",
|
|
[secret_marker],
|
|
)
|
|
fts_options = "body, content='secret'"
|
|
if fts_module == "fts5":
|
|
fts_options += ", content_rowid='id'"
|
|
await db.execute_write(
|
|
f"create virtual table secret_fts using {fts_module}({fts_options})"
|
|
)
|
|
await db.execute_write("insert into secret_fts(secret_fts) values ('rebuild')")
|
|
await ds.invoke_startup()
|
|
|
|
try:
|
|
assert "secret_fts" in await db.hidden_table_names()
|
|
assert (
|
|
await ds.allowed(
|
|
action="execute-sql",
|
|
resource=DatabaseResource("data"),
|
|
actor=actor,
|
|
)
|
|
is False
|
|
)
|
|
|
|
direct = await ds.client.get("/data/secret.json", actor=actor)
|
|
assert direct.status_code == 403
|
|
|
|
companion = await ds.client.get(
|
|
"/data/secret_fts.json?_shape=array",
|
|
actor=actor,
|
|
)
|
|
assert companion.status_code in (403, 404), (
|
|
"An automatically hidden external-content FTS table must inherit "
|
|
"the content table's view denial or be unavailable: "
|
|
f"{companion.text}"
|
|
)
|
|
assert secret_marker not in companion.text
|
|
finally:
|
|
ds.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("fts_module", ("fts4", "fts5"))
|
|
@pytest.mark.parametrize("contentless", (False, True), ids=("internal", "contentless"))
|
|
async def test_fts_shadow_tables_inherit_logical_table_view_permission(
|
|
fts_module, contentless
|
|
):
|
|
table_config = {
|
|
"secret_fts": {"permissions": {"view-table": False}},
|
|
# An explicit allow on one implementation table must not override
|
|
# the logical FTS table's denial.
|
|
"secret_fts_docsize": {"permissions": {"view-table": True}},
|
|
}
|
|
ds = Datasette(
|
|
memory=True,
|
|
config={
|
|
"permissions": {
|
|
"view-instance": True,
|
|
"view-database": True,
|
|
"view-table": True,
|
|
"execute-sql": False,
|
|
},
|
|
"databases": {"data": {"tables": table_config}},
|
|
},
|
|
)
|
|
db = ds.add_memory_database(
|
|
f"issue_17_{fts_module}_{'contentless' if contentless else 'internal'}",
|
|
name="data",
|
|
)
|
|
options = "body, content=''" if contentless else "body"
|
|
await db.execute_write(
|
|
f"create virtual table secret_fts using {fts_module}({options})"
|
|
)
|
|
await db.execute_write(
|
|
"insert into secret_fts(rowid, body) values (1, 'ISSUE_17_SHADOW_SECRET')"
|
|
)
|
|
await ds.invoke_startup()
|
|
|
|
try:
|
|
dependencies = await db.derived_table_dependencies()
|
|
shadow_tables = sorted(
|
|
table for table, source in dependencies.items() if source == "secret_fts"
|
|
)
|
|
assert shadow_tables
|
|
assert "secret_fts_docsize" in shadow_tables
|
|
|
|
for shadow_table in shadow_tables:
|
|
assert (
|
|
await ds.allowed(
|
|
action="view-table",
|
|
resource=TableResource("data", shadow_table),
|
|
)
|
|
is False
|
|
)
|
|
response = await ds.client.get(f"/data/{shadow_table}.json?_shape=array")
|
|
assert response.status_code == 403
|
|
assert "ISSUE_17_SHADOW_SECRET" not in response.text
|
|
|
|
allowed = await ds.allowed_resources("view-table", parent="data", limit=1000)
|
|
allowed_names = {resource.child for resource in allowed.resources}
|
|
assert not set(shadow_tables).intersection(allowed_names)
|
|
|
|
database_json = await ds.client.get("/data.json")
|
|
assert database_json.status_code == 200
|
|
for shadow_table in shadow_tables:
|
|
assert shadow_table not in database_json.text
|
|
|
|
schema_json = await ds.client.get("/data/-/schema.json")
|
|
assert schema_json.status_code == 200
|
|
for shadow_table in shadow_tables:
|
|
assert shadow_table not in schema_json.text
|
|
finally:
|
|
ds.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"content_allowed,companion_allowed,expected",
|
|
(
|
|
(False, True, False),
|
|
(True, False, False),
|
|
(True, True, True),
|
|
),
|
|
)
|
|
async def test_external_content_and_companion_permissions_are_both_required(
|
|
content_allowed, companion_allowed, expected
|
|
):
|
|
ds = Datasette(
|
|
memory=True,
|
|
default_deny=True,
|
|
config={
|
|
"permissions": {
|
|
"view-instance": True,
|
|
"view-database": True,
|
|
},
|
|
"databases": {
|
|
"data": {
|
|
"tables": {
|
|
"secret": {"permissions": {"view-table": content_allowed}},
|
|
"secret_fts": {
|
|
"permissions": {"view-table": companion_allowed}
|
|
},
|
|
}
|
|
}
|
|
},
|
|
},
|
|
)
|
|
db = ds.add_memory_database(
|
|
f"issue_17_explicit_{int(content_allowed)}_{int(companion_allowed)}",
|
|
name="data",
|
|
)
|
|
await db.execute_write("create table secret(id integer primary key, body text)")
|
|
await db.execute_write("insert into secret(body) values ('ISSUE_17_MATRIX_SECRET')")
|
|
await db.execute_write(
|
|
"create virtual table secret_fts using fts5("
|
|
"body, content='secret', content_rowid='id')"
|
|
)
|
|
await db.execute_write("insert into secret_fts(secret_fts) values ('rebuild')")
|
|
await ds.invoke_startup()
|
|
|
|
try:
|
|
assert (
|
|
await ds.allowed(
|
|
action="view-table",
|
|
resource=TableResource("data", "secret_fts"),
|
|
)
|
|
is expected
|
|
)
|
|
response = await ds.client.get("/data/secret_fts.json?_shape=array")
|
|
assert response.status_code == (200 if expected else 403)
|
|
if not expected:
|
|
assert "ISSUE_17_MATRIX_SECRET" not in response.text
|
|
finally:
|
|
ds.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_derived_tables_propagate_private_flag_and_route_permissions():
|
|
actor = {"id": "reader"}
|
|
ds = Datasette(
|
|
memory=True,
|
|
config={
|
|
"permissions": {
|
|
"view-instance": True,
|
|
"view-database": True,
|
|
"view-table": True,
|
|
},
|
|
"databases": {
|
|
"data": {
|
|
"tables": {
|
|
"secret": {"permissions": {"view-table": {"id": "reader"}}}
|
|
}
|
|
}
|
|
},
|
|
},
|
|
)
|
|
db = ds.add_memory_database("issue_17_private_flag", name="data")
|
|
await db.execute_write("create table secret(id integer primary key, body text)")
|
|
await db.execute_write("insert into secret(body) values ('PRIVATE')")
|
|
await db.execute_write(
|
|
"create virtual table secret_fts using fts5("
|
|
"body, content='secret', content_rowid='id')"
|
|
)
|
|
await db.execute_write("insert into secret_fts(secret_fts) values ('rebuild')")
|
|
await ds.invoke_startup()
|
|
|
|
try:
|
|
actor_page = await ds.allowed_resources(
|
|
"view-table", actor, parent="data", include_is_private=True, limit=1000
|
|
)
|
|
actor_resources = {
|
|
resource.child: resource for resource in actor_page.resources
|
|
}
|
|
derived_names = set(await db.derived_table_dependencies())
|
|
assert derived_names.issubset(actor_resources)
|
|
assert all(actor_resources[name].private for name in derived_names)
|
|
|
|
anonymous_page = await ds.allowed_resources(
|
|
"view-table", parent="data", limit=1000
|
|
)
|
|
anonymous_names = {resource.child for resource in anonymous_page.resources}
|
|
assert not derived_names.intersection(anonymous_names)
|
|
|
|
for path in (
|
|
"/data/secret_fts.json?_facet=body",
|
|
"/data/secret_fts.csv",
|
|
"/data/secret_fts/-/autocomplete?q=PRIVATE",
|
|
"/data/secret_fts/-/schema.json",
|
|
):
|
|
denied = await ds.client.get(path)
|
|
assert denied.status_code == 403
|
|
allowed = await ds.client.get(path, actor=actor)
|
|
assert allowed.status_code == 200
|
|
finally:
|
|
ds.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cyclic_derived_table_dependencies_fail_closed():
|
|
ds = Datasette(memory=True)
|
|
db = ds.add_memory_database("issue_17_cycle", name="data")
|
|
await db.execute_write(
|
|
"create virtual table first_fts using fts5(body, content='second_fts')"
|
|
)
|
|
await db.execute_write(
|
|
"create virtual table second_fts using fts5(body, content='first_fts')"
|
|
)
|
|
await ds.invoke_startup()
|
|
|
|
try:
|
|
for table in ("first_fts", "second_fts"):
|
|
assert (
|
|
await ds.allowed(
|
|
action="view-table", resource=TableResource("data", table)
|
|
)
|
|
is False
|
|
)
|
|
|
|
page = await ds.allowed_resources("view-table", parent="data", limit=1000)
|
|
allowed_names = {resource.child for resource in page.resources}
|
|
assert "first_fts" not in allowed_names
|
|
assert "second_fts" not in allowed_names
|
|
finally:
|
|
ds.close()
|