mirror of
https://github.com/simonw/datasette.git
synced 2026-07-03 06:04:39 +02:00
Support CREATE VIEW / DROP VIEW in execute-write-sql
New create-view and drop-view actions. Also fix a related bug in analyze_sql_tables(): SQLite's authorizer fires a spurious SQLITE_DELETE callback against the view name when a view is dropped (the same thing it does for dropped tables), which was incorrectly surfaced as a delete-row requirement on the view. Broaden the existing drop-table-delete suppression to cover dropped views too. Closes #2819
This commit is contained in:
parent
34ab85e664
commit
2f84ab77f2
5 changed files with 155 additions and 6 deletions
|
|
@ -61,6 +61,12 @@ def register_actions():
|
|||
description="Create tables",
|
||||
resource_class=DatabaseResource,
|
||||
),
|
||||
Action(
|
||||
name="create-view",
|
||||
abbr="cv",
|
||||
description="Create views",
|
||||
resource_class=DatabaseResource,
|
||||
),
|
||||
Action(
|
||||
name="store-query",
|
||||
abbr="sq",
|
||||
|
|
@ -111,6 +117,12 @@ def register_actions():
|
|||
description="Drop tables",
|
||||
resource_class=TableResource,
|
||||
),
|
||||
Action(
|
||||
name="drop-view",
|
||||
abbr="dv",
|
||||
description="Drop views",
|
||||
resource_class=TableResource,
|
||||
),
|
||||
# Query-level actions (child-level)
|
||||
Action(
|
||||
name="view-query",
|
||||
|
|
|
|||
|
|
@ -488,17 +488,17 @@ def analyze_sql_tables(
|
|||
and key.operation in {"create", "alter", "drop"}
|
||||
for key in operations
|
||||
)
|
||||
dropped_tables = {
|
||||
dropped_tables_and_views = {
|
||||
(key.database, key.table)
|
||||
for key in operations
|
||||
if key.operation == "drop" and key.target_type == "table"
|
||||
if key.operation == "drop" and key.target_type in {"table", "view"}
|
||||
}
|
||||
|
||||
def key_is_drop_table_delete(key: OperationKey) -> bool:
|
||||
return (
|
||||
key.operation == "delete"
|
||||
and key.target_type == "table"
|
||||
and (key.database, key.table) in dropped_tables
|
||||
and (key.database, key.table) in dropped_tables_and_views
|
||||
)
|
||||
|
||||
has_user_table_access_in_schema_operation = any(
|
||||
|
|
|
|||
|
|
@ -138,6 +138,33 @@ def decision_for_write_sql_operation(
|
|||
),
|
||||
)
|
||||
)
|
||||
if operation.operation == "create" and operation.target_type == "view":
|
||||
if operation.database is None:
|
||||
return UnsupportedWriteSqlOperation(unsupported_message)
|
||||
return RequireWriteSqlPermissions(
|
||||
(
|
||||
PermissionRequirement(
|
||||
action="create-view",
|
||||
resource=DatabaseResource(database=operation.database),
|
||||
),
|
||||
)
|
||||
)
|
||||
if (
|
||||
operation.operation == "drop"
|
||||
and operation.target_type == "view"
|
||||
and operation.database is not None
|
||||
and operation.table is not None
|
||||
):
|
||||
return RequireWriteSqlPermissions(
|
||||
(
|
||||
PermissionRequirement(
|
||||
action="drop-view",
|
||||
resource=TableResource(
|
||||
database=operation.database, table=operation.table
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
if (
|
||||
operation.operation == "alter"
|
||||
and operation.target_type == "table"
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ The one exception is the "root" account, which you can sign into while using Dat
|
|||
The ``--root`` flag is designed for local development and testing. When you start Datasette with ``--root``, the root user automatically receives every permission, including:
|
||||
|
||||
* All view permissions (``view-instance``, ``view-database``, ``view-table``, etc.)
|
||||
* All write permissions (``insert-row``, ``update-row``, ``delete-row``, ``create-table``, ``alter-table``, ``set-column-type``, ``drop-table``)
|
||||
* All write permissions (``insert-row``, ``update-row``, ``delete-row``, ``create-table``, ``create-view``, ``alter-table``, ``set-column-type``, ``drop-table``, ``drop-view``)
|
||||
* Debug permissions (``permissions-debug``, ``debug-menu``)
|
||||
* Any custom permissions defined by plugins
|
||||
|
||||
|
|
@ -1386,6 +1386,16 @@ create-table
|
|||
|
||||
Actor is allowed to create a database table.
|
||||
|
||||
``resource`` - ``datasette.resources.DatabaseResource(database)``
|
||||
``database`` is the name of the database (string)
|
||||
|
||||
.. _actions_create_view:
|
||||
|
||||
create-view
|
||||
-----------
|
||||
|
||||
Actor is allowed to create a database view.
|
||||
|
||||
``resource`` - ``datasette.resources.DatabaseResource(database)``
|
||||
``database`` is the name of the database (string)
|
||||
|
||||
|
|
@ -1425,6 +1435,18 @@ Actor is allowed to drop a database table.
|
|||
|
||||
``table`` is the name of the table (string)
|
||||
|
||||
.. _actions_drop_view:
|
||||
|
||||
drop-view
|
||||
---------
|
||||
|
||||
Actor is allowed to drop a database view.
|
||||
|
||||
``resource`` - ``datasette.resources.TableResource(database, table)``
|
||||
``database`` is the name of the database (string)
|
||||
|
||||
``table`` is the name of the view (string)
|
||||
|
||||
.. _actions_execute_sql:
|
||||
|
||||
execute-sql
|
||||
|
|
|
|||
|
|
@ -3214,6 +3214,74 @@ async def test_execute_write_create_table_uses_create_table_permission():
|
|||
assert not await db.table_exists("should_not_exist")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_write_create_view_uses_create_view_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-view": {"id": "creator"},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
db = ds.add_memory_database("execute_write_create_view", name="data")
|
||||
await db.execute_write("create table dogs (id integer primary key, name text)")
|
||||
await ds.invoke_startup()
|
||||
|
||||
analysis_response = await ds.client.get(
|
||||
"/data/-/execute-write/analyze",
|
||||
actor={"id": "creator"},
|
||||
params={"sql": "create view dog_names as select id, name from dogs"},
|
||||
)
|
||||
allowed_response = await ds.client.post(
|
||||
"/data/-/execute-write",
|
||||
actor={"id": "creator"},
|
||||
json={"sql": "create view dog_names as select id, name from dogs"},
|
||||
)
|
||||
row_permission_response = await ds.client.post(
|
||||
"/data/-/execute-write",
|
||||
actor={"id": "row-writer"},
|
||||
json={"sql": "create view should_not_exist as select id from dogs"},
|
||||
)
|
||||
|
||||
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": "dog_names",
|
||||
"required_permission": "create-view",
|
||||
"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.view_exists("dog_names")
|
||||
|
||||
assert row_permission_response.status_code == 403
|
||||
assert row_permission_response.json()["errors"] == [
|
||||
"Permission denied: need create-view on data"
|
||||
]
|
||||
assert not await db.view_exists("should_not_exist")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"database_name",
|
||||
|
|
@ -3264,8 +3332,20 @@ async def test_execute_write_create_table_uses_create_table_permission():
|
|||
(),
|
||||
"drop-table",
|
||||
),
|
||||
(
|
||||
"execute_write_drop_view",
|
||||
"dropper",
|
||||
"drop view dogs_view",
|
||||
"drop view cats_view",
|
||||
"Permission denied: need drop-view on data/cats_view",
|
||||
(
|
||||
"create view dogs_view as select * from dogs",
|
||||
"create view cats_view as select * from cats",
|
||||
),
|
||||
"drop-view",
|
||||
),
|
||||
),
|
||||
ids=("alter-table", "create-index", "drop-index", "drop-table"),
|
||||
ids=("alter-table", "create-index", "drop-index", "drop-table", "drop-view"),
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_write_schema_operations_use_schema_permissions(
|
||||
|
|
@ -3300,7 +3380,12 @@ async def test_execute_write_schema_operations_use_schema_permissions(
|
|||
"drop-table": {"id": "dropper"},
|
||||
"view-table": {"id": "alterer"},
|
||||
}
|
||||
}
|
||||
},
|
||||
"dogs_view": {
|
||||
"permissions": {
|
||||
"drop-view": {"id": "dropper"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
|
@ -3353,6 +3438,9 @@ async def test_execute_write_schema_operations_use_schema_permissions(
|
|||
elif expected_state == "drop-table":
|
||||
assert not await db.table_exists("dogs")
|
||||
assert await db.table_exists("cats")
|
||||
elif expected_state == "drop-view":
|
||||
assert not await db.view_exists("dogs_view")
|
||||
assert await db.view_exists("cats_view")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue