mirror of
https://github.com/simonw/datasette.git
synced 2026-09-28 12:54:10 +02:00
Expanded analysis of SQL operations, refs #2748
This commit is contained in:
parent
9f66cf72c1
commit
737ff03efb
9 changed files with 740 additions and 120 deletions
|
|
@ -12,10 +12,22 @@ import pytest
|
|||
import pytest_asyncio
|
||||
from datasette.app import Datasette
|
||||
from datasette.permissions import PermissionSQL
|
||||
from datasette.resources import TableResource
|
||||
from datasette.resources import DatabaseResource, QueryResource, TableResource
|
||||
from datasette import hookimpl
|
||||
|
||||
|
||||
def test_resource_string_representations():
|
||||
assert str(DatabaseResource("content")) == "content"
|
||||
assert repr(DatabaseResource("content")) == (
|
||||
"DatabaseResource(parent='content', child=None)"
|
||||
)
|
||||
assert str(TableResource("content", "dogs")) == "content/dogs"
|
||||
assert repr(TableResource("content", "dogs")) == (
|
||||
"TableResource(parent='content', child='dogs')"
|
||||
)
|
||||
assert str(QueryResource("content", "insert-a-dog")) == "content/insert-a-dog"
|
||||
|
||||
|
||||
# Test plugin that provides permission rules
|
||||
class PermissionRulesPlugin:
|
||||
def __init__(self, rules_callback):
|
||||
|
|
|
|||
|
|
@ -698,14 +698,17 @@ async def test_analyze_sql():
|
|||
|
||||
assert [
|
||||
(
|
||||
access.operation,
|
||||
access.database,
|
||||
access.sqlite_schema,
|
||||
access.table,
|
||||
access.columns,
|
||||
access.source,
|
||||
operation.operation,
|
||||
operation.database,
|
||||
operation.sqlite_schema,
|
||||
operation.table,
|
||||
operation.columns,
|
||||
operation.source,
|
||||
)
|
||||
for access in analysis.table_accesses
|
||||
for operation in analysis.operations
|
||||
if operation.target_type == "table"
|
||||
and operation.operation in {"read", "insert", "update", "delete"}
|
||||
and not operation.internal
|
||||
] == [
|
||||
("read", "data", "main", "dogs", ("id", "name"), None),
|
||||
]
|
||||
|
|
@ -722,14 +725,17 @@ async def test_analyze_sql_insert_select():
|
|||
|
||||
assert {
|
||||
(
|
||||
access.operation,
|
||||
access.database,
|
||||
access.sqlite_schema,
|
||||
access.table,
|
||||
access.columns,
|
||||
access.source,
|
||||
operation.operation,
|
||||
operation.database,
|
||||
operation.sqlite_schema,
|
||||
operation.table,
|
||||
operation.columns,
|
||||
operation.source,
|
||||
)
|
||||
for access in analysis.table_accesses
|
||||
for operation in analysis.operations
|
||||
if operation.target_type == "table"
|
||||
and operation.operation in {"read", "insert", "update", "delete"}
|
||||
and not operation.internal
|
||||
} == {
|
||||
("insert", "data", "main", "dogs", (), None),
|
||||
("read", "data", "main", "cats", ("name",), None),
|
||||
|
|
|
|||
|
|
@ -1643,6 +1643,172 @@ async def test_execute_write_post_requires_database_and_table_permissions():
|
|||
assert (await db.execute("select name from dogs")).first()[0] == "Cleo"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_write_create_table_uses_create_table_permission():
|
||||
ds = Datasette(
|
||||
memory=True,
|
||||
default_deny=True,
|
||||
config={
|
||||
"permissions": {
|
||||
"insert-row": {"id": "row-writer"},
|
||||
"update-row": {"id": "row-writer"},
|
||||
},
|
||||
"databases": {
|
||||
"data": {
|
||||
"permissions": {
|
||||
"view-database": {"id": ["creator", "row-writer"]},
|
||||
"execute-write-sql": {"id": ["creator", "row-writer"]},
|
||||
"create-table": {"id": "creator"},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
db = ds.add_memory_database("execute_write_create_table", name="data")
|
||||
await ds.invoke_startup()
|
||||
|
||||
analysis_response = await ds.client.get(
|
||||
"/data/-/execute-write/analyze",
|
||||
actor={"id": "creator"},
|
||||
params={"sql": "create table foobar (id integer primary key, name text)"},
|
||||
)
|
||||
allowed_response = await ds.client.post(
|
||||
"/data/-/execute-write",
|
||||
actor={"id": "creator"},
|
||||
json={"sql": "create table foobar (id integer primary key, name text)"},
|
||||
)
|
||||
row_permission_response = await ds.client.post(
|
||||
"/data/-/execute-write",
|
||||
actor={"id": "row-writer"},
|
||||
json={"sql": "create table should_not_exist (id integer primary key)"},
|
||||
)
|
||||
|
||||
assert analysis_response.status_code == 200
|
||||
analysis_data = analysis_response.json()
|
||||
assert analysis_data["ok"] is True
|
||||
assert analysis_data["execute_disabled"] is False
|
||||
assert analysis_data["analysis_rows"] == [
|
||||
{
|
||||
"operation": "create",
|
||||
"database": "data",
|
||||
"table": "foobar",
|
||||
"required_permission": "create-table",
|
||||
"source": None,
|
||||
"allowed": True,
|
||||
}
|
||||
]
|
||||
|
||||
assert allowed_response.status_code == 200
|
||||
assert allowed_response.json()["ok"] is True
|
||||
assert allowed_response.json()["message"] == "Query executed"
|
||||
assert await db.table_exists("foobar")
|
||||
|
||||
assert row_permission_response.status_code == 403
|
||||
assert row_permission_response.json()["errors"] == [
|
||||
"Permission denied: need create-table on data"
|
||||
]
|
||||
assert not await db.table_exists("should_not_exist")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_write_alter_and_drop_table_use_schema_permissions():
|
||||
ds = Datasette(
|
||||
memory=True,
|
||||
default_deny=True,
|
||||
config={
|
||||
"permissions": {
|
||||
"delete-row": {"id": "row-writer"},
|
||||
"update-row": {"id": "row-writer"},
|
||||
},
|
||||
"databases": {
|
||||
"data": {
|
||||
"permissions": {
|
||||
"view-database": {"id": ["alterer", "dropper", "row-writer"]},
|
||||
"execute-write-sql": {
|
||||
"id": ["alterer", "dropper", "row-writer"]
|
||||
},
|
||||
},
|
||||
"tables": {
|
||||
"dogs": {
|
||||
"permissions": {
|
||||
"alter-table": {"id": "alterer"},
|
||||
"drop-table": {"id": "dropper"},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
db = ds.add_memory_database("execute_write_alter_drop_table", name="data")
|
||||
await db.execute_write("create table dogs (id integer primary key, name text)")
|
||||
await db.execute_write("create table cats (id integer primary key, name text)")
|
||||
await ds.invoke_startup()
|
||||
|
||||
alter_allowed_response = await ds.client.post(
|
||||
"/data/-/execute-write",
|
||||
actor={"id": "alterer"},
|
||||
json={"sql": "alter table dogs add column age integer"},
|
||||
)
|
||||
alter_row_permission_response = await ds.client.post(
|
||||
"/data/-/execute-write",
|
||||
actor={"id": "row-writer"},
|
||||
json={"sql": "alter table cats add column age integer"},
|
||||
)
|
||||
|
||||
assert alter_allowed_response.status_code == 200
|
||||
assert "age" in [column.name for column in await db.table_column_details("dogs")]
|
||||
assert alter_row_permission_response.status_code == 403
|
||||
assert alter_row_permission_response.json()["errors"] == [
|
||||
"Permission denied: need alter-table on data/cats"
|
||||
]
|
||||
assert "age" not in [
|
||||
column.name for column in await db.table_column_details("cats")
|
||||
]
|
||||
|
||||
create_index_allowed_response = await ds.client.post(
|
||||
"/data/-/execute-write",
|
||||
actor={"id": "alterer"},
|
||||
json={"sql": "create index idx_dogs_name on dogs(name)"},
|
||||
)
|
||||
create_index_row_permission_response = await ds.client.post(
|
||||
"/data/-/execute-write",
|
||||
actor={"id": "row-writer"},
|
||||
json={"sql": "create index idx_cats_name on cats(name)"},
|
||||
)
|
||||
drop_index_allowed_response = await ds.client.post(
|
||||
"/data/-/execute-write",
|
||||
actor={"id": "alterer"},
|
||||
json={"sql": "drop index idx_dogs_name"},
|
||||
)
|
||||
|
||||
assert create_index_allowed_response.status_code == 200
|
||||
assert create_index_row_permission_response.status_code == 403
|
||||
assert create_index_row_permission_response.json()["errors"] == [
|
||||
"Permission denied: need alter-table on data/cats"
|
||||
]
|
||||
assert drop_index_allowed_response.status_code == 200
|
||||
|
||||
drop_allowed_response = await ds.client.post(
|
||||
"/data/-/execute-write",
|
||||
actor={"id": "dropper"},
|
||||
json={"sql": "drop table dogs"},
|
||||
)
|
||||
drop_row_permission_response = await ds.client.post(
|
||||
"/data/-/execute-write",
|
||||
actor={"id": "row-writer"},
|
||||
json={"sql": "drop table cats"},
|
||||
)
|
||||
|
||||
assert drop_allowed_response.status_code == 200
|
||||
assert not await db.table_exists("dogs")
|
||||
assert drop_row_permission_response.status_code == 403
|
||||
assert drop_row_permission_response.json()["errors"] == [
|
||||
"Permission denied: need drop-table on data/cats"
|
||||
]
|
||||
assert await db.table_exists("cats")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_write_insert_links_to_inserted_row():
|
||||
ds = Datasette(memory=True, default_deny=True)
|
||||
|
|
|
|||
|
|
@ -26,17 +26,20 @@ def conn():
|
|||
conn.close()
|
||||
|
||||
|
||||
def as_tuples(analysis):
|
||||
def table_operation_tuples(analysis):
|
||||
return [
|
||||
(
|
||||
access.operation,
|
||||
access.database,
|
||||
access.sqlite_schema,
|
||||
access.table,
|
||||
access.columns,
|
||||
access.source,
|
||||
operation.operation,
|
||||
operation.database,
|
||||
operation.sqlite_schema,
|
||||
operation.table,
|
||||
operation.columns,
|
||||
operation.source,
|
||||
)
|
||||
for access in analysis.table_accesses
|
||||
for operation in analysis.operations
|
||||
if operation.target_type == "table"
|
||||
and operation.operation in {"read", "insert", "update", "delete"}
|
||||
and not operation.internal
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -48,7 +51,7 @@ def test_analyze_select_tables(conn):
|
|||
database_name="data",
|
||||
)
|
||||
|
||||
assert set(as_tuples(analysis)) == {
|
||||
assert set(table_operation_tuples(analysis)) == {
|
||||
("read", "data", "main", "cats", ("id", "name"), None),
|
||||
("read", "data", "main", "dogs", ("age", "id", "name"), None),
|
||||
}
|
||||
|
|
@ -57,11 +60,73 @@ def test_analyze_select_tables(conn):
|
|||
def test_analyze_uses_sqlite_schema_as_default_database(conn):
|
||||
analysis = analyze_sql_tables(conn, "select name from dogs")
|
||||
|
||||
assert set(as_tuples(analysis)) == {
|
||||
assert set(table_operation_tuples(analysis)) == {
|
||||
("read", "main", "main", "dogs", ("name",), None),
|
||||
}
|
||||
|
||||
|
||||
def operation_dict(operation):
|
||||
return {
|
||||
"operation": operation.operation,
|
||||
"target_type": operation.target_type,
|
||||
"database": operation.database,
|
||||
"sqlite_schema": operation.sqlite_schema,
|
||||
"table": operation.table,
|
||||
"target": operation.target,
|
||||
"columns": operation.columns,
|
||||
"source": operation.source,
|
||||
"internal": operation.internal,
|
||||
}
|
||||
|
||||
|
||||
def test_analyze_create_table_operation():
|
||||
conn = sqlite3.connect(":memory:")
|
||||
try:
|
||||
analysis = analyze_sql_tables(
|
||||
conn,
|
||||
"create table foobar (id integer primary key, name text)",
|
||||
database_name="data",
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
assert {
|
||||
"operation": "create",
|
||||
"target_type": "table",
|
||||
"database": "data",
|
||||
"sqlite_schema": "main",
|
||||
"table": "foobar",
|
||||
"target": "foobar",
|
||||
"columns": (),
|
||||
"source": None,
|
||||
"internal": False,
|
||||
} in [operation_dict(operation) for operation in analysis.operations]
|
||||
assert not [
|
||||
operation
|
||||
for operation in analysis.operations
|
||||
if operation.table in {"sqlite_master", "sqlite_schema"}
|
||||
and not operation.internal
|
||||
]
|
||||
|
||||
|
||||
def test_analyze_transaction_operation(conn):
|
||||
analysis = analyze_sql_tables(conn, "commit", database_name="data")
|
||||
|
||||
assert [operation_dict(operation) for operation in analysis.operations] == [
|
||||
{
|
||||
"operation": "commit",
|
||||
"target_type": "transaction",
|
||||
"database": None,
|
||||
"sqlite_schema": None,
|
||||
"table": None,
|
||||
"target": "COMMIT",
|
||||
"columns": (),
|
||||
"source": None,
|
||||
"internal": False,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_analyze_insert_tables(conn):
|
||||
analysis = analyze_sql_tables(
|
||||
conn,
|
||||
|
|
@ -70,7 +135,7 @@ def test_analyze_insert_tables(conn):
|
|||
database_name="data",
|
||||
)
|
||||
|
||||
assert set(as_tuples(analysis)) == {
|
||||
assert set(table_operation_tuples(analysis)) == {
|
||||
("insert", "data", "main", "dogs", (), None),
|
||||
("read", "data", "main", "dogs", ("id", "name"), "dogs_after_insert"),
|
||||
("update", "data", "main", "cats", ("name",), "dogs_after_insert"),
|
||||
|
|
@ -87,7 +152,7 @@ def test_analyze_update_tables(conn):
|
|||
database_name="data",
|
||||
)
|
||||
|
||||
assert set(as_tuples(analysis)) == {
|
||||
assert set(table_operation_tuples(analysis)) == {
|
||||
("update", "data", "main", "dogs", ("age",), None),
|
||||
("read", "data", "main", "dogs", ("age", "name"), None),
|
||||
}
|
||||
|
|
@ -101,7 +166,7 @@ def test_analyze_delete_tables(conn):
|
|||
database_name="data",
|
||||
)
|
||||
|
||||
assert set(as_tuples(analysis)) == {
|
||||
assert set(table_operation_tuples(analysis)) == {
|
||||
("delete", "data", "main", "dogs", (), None),
|
||||
("read", "data", "main", "dogs", ("name",), None),
|
||||
}
|
||||
|
|
@ -121,7 +186,7 @@ def test_analyze_insert_select_with_cte(conn):
|
|||
database_name="data",
|
||||
)
|
||||
|
||||
assert set(as_tuples(analysis)) == {
|
||||
assert set(table_operation_tuples(analysis)) == {
|
||||
("insert", "data", "main", "cats", (), None),
|
||||
("read", "data", "main", "dogs", ("age", "name"), "old_dogs"),
|
||||
}
|
||||
|
|
@ -135,7 +200,7 @@ def test_analyze_view_with_instead_of_trigger(conn):
|
|||
database_name="data",
|
||||
)
|
||||
|
||||
assert set(as_tuples(analysis)) == {
|
||||
assert set(table_operation_tuples(analysis)) == {
|
||||
("update", "data", "main", "dog_names", ("name",), None),
|
||||
("read", "data", "main", "dogs", ("id", "name"), "dog_names"),
|
||||
("read", "data", "main", "dog_names", ("id", "name"), "dog_names"),
|
||||
|
|
@ -163,7 +228,7 @@ def test_analyze_attached_database_tables(conn):
|
|||
schema_to_database={"extra": "extra_db"},
|
||||
)
|
||||
|
||||
assert set(as_tuples(analysis)) == {
|
||||
assert set(table_operation_tuples(analysis)) == {
|
||||
("insert", "extra_db", "extra", "people", (), None),
|
||||
("read", "data", "main", "dogs", ("name",), None),
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue