mirror of
https://github.com/simonw/datasette.git
synced 2026-05-31 14:16:59 +02:00
parent
8bd7e165f4
commit
51dab16149
6 changed files with 91 additions and 15 deletions
|
|
@ -82,9 +82,7 @@ def decision_for_write_sql_operation(
|
|||
"Writes to shadow tables are not allowed in user-supplied SQL"
|
||||
)
|
||||
if operation.operation == "function":
|
||||
# SQL functions currently have no Datasette permission mapping. They are
|
||||
# rejected by the user-supplied write SQL allow-list as unsupported.
|
||||
return UnsupportedWriteSqlOperation(unsupported_message)
|
||||
return IgnoreWriteSqlOperation("SQL function")
|
||||
if (
|
||||
operation.operation == "read"
|
||||
and operation.target_type == "table"
|
||||
|
|
|
|||
|
|
@ -1425,7 +1425,7 @@ See also :ref:`the default_allow_sql setting <setting_default_allow_sql>`.
|
|||
execute-write-sql
|
||||
-----------------
|
||||
|
||||
Actor is allowed to run arbitrary writable SQL queries against a specific database, subject to table-level write permissions such as ``insert-row``, ``update-row`` and ``delete-row``.
|
||||
Actor is allowed to run arbitrary writable SQL queries against a specific database, subject to table-level write permissions such as ``insert-row``, ``update-row`` and ``delete-row``. SQL functions are allowed and are not separately restricted by Datasette permissions.
|
||||
|
||||
``resource`` - ``datasette.resources.DatabaseResource(database)``
|
||||
``database`` is the name of the database (string)
|
||||
|
|
|
|||
|
|
@ -531,7 +531,7 @@ The request body must include a ``"sql"`` string. Named SQL parameters can be pr
|
|||
|
||||
The SQL must be writable. Read-only ``select`` queries should use the regular :ref:`custom SQL query API <sql>` instead.
|
||||
|
||||
Datasette analyzes the SQL before executing it. The actor must have ``execute-write-sql`` permission for the database, and must also have any permissions required by the operations in the SQL. For example, inserts and updates against a table require ``insert-row``, ``update-row`` and ``delete-row`` permissions for that table. Reads performed as part of the write, such as ``insert into dogs select ... from other_table``, require ``view-table`` permission on the source table.
|
||||
Datasette analyzes the SQL before executing it. The actor must have ``execute-write-sql`` permission for the database, and must also have any permissions required by the operations in the SQL. For example, inserts and updates against a table require ``insert-row``, ``update-row`` and ``delete-row`` permissions for that table. Reads performed as part of the write, such as ``insert into dogs select ... from other_table``, require ``view-table`` permission on the source table. SQL functions are allowed and are not separately restricted by Datasette permissions.
|
||||
|
||||
A successful response includes a message, the SQLite ``rowcount`` and a summary of the operations that were executed:
|
||||
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ Datasette stores both configured queries and user-created queries in the ``queri
|
|||
|
||||
Stored queries created by users default to private. Private stored queries can only be viewed, updated or deleted by the actor that created them. Broad ``view-query``, ``update-query`` or ``delete-query`` permission grants still do not allow other actors to access another actor's private stored queries.
|
||||
|
||||
Stored queries created by users are untrusted. This means they execute using the permissions of the actor who runs them, as if that actor had pasted the SQL into the regular custom SQL interface or write SQL interface. Read-only stored queries require ``execute-sql``. Writable stored queries require ``execute-write-sql`` plus the relevant table-level write permissions.
|
||||
Stored queries created by users are untrusted. This means they execute using the permissions of the actor who runs them, as if that actor had pasted the SQL into the regular custom SQL interface or write SQL interface. Read-only stored queries require ``execute-sql``. Writable stored queries require ``execute-write-sql`` plus the relevant table-level write permissions. SQL functions are allowed and are not separately restricted by Datasette permissions.
|
||||
|
||||
.. _trusted_stored_queries:
|
||||
.. _trusted_saved_queries:
|
||||
|
|
|
|||
|
|
@ -1414,6 +1414,11 @@ async def test_execute_write_analyze_endpoint_uses_sql_only():
|
|||
actor={"id": "root"},
|
||||
params={"sql": "insert into dogs (name) values (:name)"},
|
||||
)
|
||||
function_response = await ds.client.get(
|
||||
"/data/-/execute-write/analyze",
|
||||
actor={"id": "root"},
|
||||
params={"sql": "insert into dogs (name) values (upper(:name))"},
|
||||
)
|
||||
read_only_response = await ds.client.get(
|
||||
"/data/-/execute-write/analyze",
|
||||
actor={"id": "root"},
|
||||
|
|
@ -1438,6 +1443,22 @@ async def test_execute_write_analyze_endpoint_uses_sql_only():
|
|||
]
|
||||
assert "params" not in data
|
||||
|
||||
assert function_response.status_code == 200
|
||||
function_data = function_response.json()
|
||||
assert function_data["ok"] is True
|
||||
assert function_data["parameters"] == ["name"]
|
||||
assert function_data["execute_disabled"] is False
|
||||
assert function_data["analysis_rows"] == [
|
||||
{
|
||||
"operation": "insert",
|
||||
"database": "data",
|
||||
"table": "dogs",
|
||||
"required_permission": "insert-row, update-row, delete-row",
|
||||
"source": None,
|
||||
"allowed": True,
|
||||
}
|
||||
]
|
||||
|
||||
assert read_only_response.status_code == 200
|
||||
read_only_data = read_only_response.json()
|
||||
assert read_only_data["ok"] is False
|
||||
|
|
@ -1970,7 +1991,7 @@ async def test_execute_write_create_table_as_select_requires_view_table_on_sourc
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_write_rejects_function_operations():
|
||||
async def test_execute_write_allows_function_operations():
|
||||
ds = Datasette(
|
||||
memory=True,
|
||||
default_deny=True,
|
||||
|
|
@ -1998,17 +2019,65 @@ async def test_execute_write_rejects_function_operations():
|
|||
await db.execute_write("create table dogs (id integer primary key, name text)")
|
||||
await ds.invoke_startup()
|
||||
|
||||
denied_response = await ds.client.post(
|
||||
response = await ds.client.post(
|
||||
"/data/-/execute-write",
|
||||
actor={"id": "writer"},
|
||||
json={"sql": "insert into dogs (name) values (upper('cleo'))"},
|
||||
)
|
||||
|
||||
assert denied_response.status_code == 403
|
||||
assert denied_response.json()["errors"] == [
|
||||
"Unsupported SQL operation: function function"
|
||||
]
|
||||
assert (await db.execute("select name from dogs")).dicts() == []
|
||||
assert response.status_code == 200
|
||||
assert response.json()["ok"] is True
|
||||
assert (await db.execute("select name from dogs")).dicts() == [{"name": "CLEO"}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_untrusted_stored_write_query_allows_function_operations():
|
||||
ds = Datasette(
|
||||
memory=True,
|
||||
default_deny=True,
|
||||
config={
|
||||
"databases": {
|
||||
"data": {
|
||||
"permissions": {
|
||||
"view-database": {"id": "writer"},
|
||||
"view-query": {"id": "writer"},
|
||||
"execute-write-sql": {"id": "writer"},
|
||||
},
|
||||
"tables": {
|
||||
"dogs": {
|
||||
"permissions": {
|
||||
"insert-row": {"id": "writer"},
|
||||
"update-row": {"id": "writer"},
|
||||
"delete-row": {"id": "writer"},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
db = ds.add_memory_database("stored_query_function_operation", name="data")
|
||||
await db.execute_write("create table dogs (id integer primary key, name text)")
|
||||
await ds.invoke_startup()
|
||||
await ds.add_query(
|
||||
"data",
|
||||
"insert_dog",
|
||||
"insert into dogs (name) values (upper(:name))",
|
||||
is_write=True,
|
||||
is_trusted=False,
|
||||
source="user",
|
||||
owner_id="writer",
|
||||
)
|
||||
|
||||
response = await ds.client.post(
|
||||
"/data/insert_dog?_json=1",
|
||||
actor={"id": "writer"},
|
||||
data={"name": "cleo"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["ok"] is True
|
||||
assert (await db.execute("select name from dogs")).dicts() == [{"name": "CLEO"}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -50,10 +50,19 @@ def test_decision_for_write_sql_operation_rejects_vacuum():
|
|||
assert decision.message == "VACUUM is not allowed in user-supplied SQL"
|
||||
|
||||
|
||||
def test_decision_for_write_sql_operation_reports_unsupported_functions():
|
||||
def test_decision_for_write_sql_operation_ignores_functions():
|
||||
decision = decision_for_write_sql_operation(
|
||||
Operation("function", "function", None, None, None, target="upper")
|
||||
)
|
||||
|
||||
assert isinstance(decision, IgnoreWriteSqlOperation)
|
||||
assert decision.reason == "SQL function"
|
||||
|
||||
|
||||
def test_decision_for_write_sql_operation_reports_unsupported_operations():
|
||||
decision = decision_for_write_sql_operation(
|
||||
Operation("unknown", "unknown", None, None, None)
|
||||
)
|
||||
|
||||
assert isinstance(decision, UnsupportedWriteSqlOperation)
|
||||
assert decision.message == "Unsupported SQL operation: function function"
|
||||
assert decision.message == "Unsupported SQL operation: unknown unknown"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue