mirror of
https://github.com/simonw/datasette.git
synced 2026-07-09 17:14:35 +02:00
parent
b4c63966f8
commit
221be2632e
3 changed files with 129 additions and 0 deletions
|
|
@ -93,6 +93,7 @@ from .utils import (
|
|||
module_from_path,
|
||||
move_plugins_and_allow,
|
||||
move_table_config,
|
||||
named_parameters,
|
||||
parse_metadata,
|
||||
resolve_env_secrets,
|
||||
resolve_routes,
|
||||
|
|
@ -1237,6 +1238,35 @@ class Datasette:
|
|||
)
|
||||
return {row["name"]: self._query_row_to_dict(row) for row in rows}
|
||||
|
||||
async def ensure_query_write_permissions(self, database, sql, *, actor=None):
|
||||
write_actions = {
|
||||
"insert": "insert-row",
|
||||
"update": "update-row",
|
||||
"delete": "delete-row",
|
||||
}
|
||||
db = self.get_database(database)
|
||||
params = {name: "" for name in named_parameters(sql)}
|
||||
try:
|
||||
analysis = await db.analyze_sql(sql, params)
|
||||
except sqlite3.DatabaseError as ex:
|
||||
raise Forbidden(f"Could not analyze query: {ex}") from ex
|
||||
|
||||
for access in analysis.table_accesses:
|
||||
action = write_actions.get(access.operation)
|
||||
if action is None:
|
||||
continue
|
||||
if access.database != database:
|
||||
raise Forbidden("Writable queries may not write to attached databases")
|
||||
if not await self.allowed(
|
||||
action=action,
|
||||
resource=TableResource(database=access.database, table=access.table),
|
||||
actor=actor,
|
||||
):
|
||||
raise Forbidden(
|
||||
f"Permission denied: need {action} on {access.database}/{access.table}"
|
||||
)
|
||||
return analysis
|
||||
|
||||
# Column types API
|
||||
|
||||
async def _get_resource_column_details(self, database: str, resource: str):
|
||||
|
|
|
|||
|
|
@ -54,6 +54,20 @@ def register_actions():
|
|||
description="Create tables",
|
||||
resource_class=DatabaseResource,
|
||||
),
|
||||
Action(
|
||||
name="insert-query",
|
||||
abbr="iq",
|
||||
description="Create saved queries",
|
||||
resource_class=DatabaseResource,
|
||||
also_requires="execute-sql",
|
||||
),
|
||||
Action(
|
||||
name="publish-query",
|
||||
abbr="pq",
|
||||
description="Publish saved queries for actors without execute-sql",
|
||||
resource_class=DatabaseResource,
|
||||
also_requires="insert-query",
|
||||
),
|
||||
# Table-level actions (child-level)
|
||||
Action(
|
||||
name="view-table",
|
||||
|
|
@ -104,4 +118,16 @@ def register_actions():
|
|||
description="View named query results",
|
||||
resource_class=QueryResource,
|
||||
),
|
||||
Action(
|
||||
name="update-query",
|
||||
abbr="uq",
|
||||
description="Update saved queries",
|
||||
resource_class=QueryResource,
|
||||
),
|
||||
Action(
|
||||
name="delete-query",
|
||||
abbr="dq",
|
||||
description="Delete saved queries",
|
||||
resource_class=QueryResource,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import pytest
|
|||
|
||||
from datasette.app import Datasette
|
||||
from datasette.resources import DatabaseResource, QueryResource
|
||||
from datasette.utils.asgi import Forbidden
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -206,3 +207,75 @@ async def test_unpublished_query_requires_execute_sql_but_published_does_not():
|
|||
resource=QueryResource("data", "published"),
|
||||
actor=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_actions_are_registered():
|
||||
ds = Datasette()
|
||||
await ds.invoke_startup()
|
||||
|
||||
assert ds.get_action("insert-query").resource_class is DatabaseResource
|
||||
assert ds.get_action("publish-query").resource_class is DatabaseResource
|
||||
assert ds.get_action("update-query").resource_class is QueryResource
|
||||
assert ds.get_action("delete-query").resource_class is QueryResource
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_write_query_requires_table_permissions():
|
||||
ds = Datasette(memory=True, default_deny=True)
|
||||
db = ds.add_memory_database("query_write_permissions", name="data")
|
||||
await db.execute_write("create table dogs (id integer primary key, name text)")
|
||||
await ds.invoke_startup()
|
||||
|
||||
actor = {"id": "writer"}
|
||||
await ds.add_query(
|
||||
"data",
|
||||
"write_dog",
|
||||
"insert into dogs (name) values (:name)",
|
||||
is_write=True,
|
||||
source="user",
|
||||
owner_id="writer",
|
||||
)
|
||||
|
||||
with pytest.raises(Forbidden):
|
||||
await ds.ensure_query_write_permissions(
|
||||
"data",
|
||||
"insert into dogs (name) values (:name)",
|
||||
actor=actor,
|
||||
)
|
||||
|
||||
ds.config = {
|
||||
"databases": {
|
||||
"data": {
|
||||
"tables": {
|
||||
"dogs": {
|
||||
"permissions": {
|
||||
"insert-row": {"id": "writer"},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await ds.ensure_query_write_permissions(
|
||||
"data",
|
||||
"insert into dogs (name) values (:name)",
|
||||
actor=actor,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_write_query_rejects_writes_to_attached_databases():
|
||||
ds = Datasette(memory=True, default_deny=True)
|
||||
db = ds.add_memory_database("query_attached_writes", name="data")
|
||||
await db.execute_write("attach database ':memory:' as extra")
|
||||
await db.execute_write("create table extra.cats (id integer primary key)")
|
||||
await ds.invoke_startup()
|
||||
|
||||
with pytest.raises(Forbidden):
|
||||
await ds.ensure_query_write_permissions(
|
||||
"data",
|
||||
"insert into extra.cats (id) values (1)",
|
||||
actor={"id": "writer"},
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue