Analysis will show each affected table and required permission.
"
not in blank_create_response.text
)
assert query_response.status_code == 200
assert "Save this query" in query_response.text
assert "/data/-/queries/insert?sql=select+%2A+from+dogs" in query_response.text
assert old_create_response.status_code == 404
@pytest.mark.asyncio
async def test_create_query_analyze_endpoint_uses_sql_only():
ds = Datasette(memory=True, default_deny=True)
ds.root_enabled = True
db = ds.add_memory_database("query_create_analyze", name="data")
await db.execute_write("create table dogs (id integer primary key, name text)")
await ds.invoke_startup()
response = await ds.client.get(
"/data/-/queries/analyze",
actor={"id": "root"},
params={"sql": "select * from dogs where name = :name"},
)
write_response = await ds.client.get(
"/data/-/queries/analyze",
actor={"id": "root"},
params={"sql": "insert into dogs (name) values (:name)"},
)
blank_response = await ds.client.get(
"/data/-/queries/analyze",
actor={"id": "root"},
params={"sql": ""},
)
old_analyze_response = await ds.client.get(
"/data/-/queries/-/create/analyze",
actor={"id": "root"},
params={"sql": "select * from dogs"},
)
assert response.status_code == 200
data = response.json()
assert data["ok"] is True
assert data["parameters"] == ["name"]
assert data["analysis_error"] is None
assert data["has_sql"] is True
assert data["analysis_is_write"] is False
assert data["save_disabled"] is False
assert data["analysis_rows"] == [
{
"operation": "read",
"database": "data",
"table": "dogs",
"required_permission": "",
"source": None,
"allowed": None,
}
]
assert write_response.status_code == 200
write_data = write_response.json()
assert write_data["parameters"] == ["name"]
assert write_data["has_sql"] is True
assert write_data["analysis_is_write"] is True
assert write_data["save_disabled"] is False
assert write_data["analysis_rows"][0]["operation"] == "insert"
assert blank_response.status_code == 200
blank_data = blank_response.json()
assert blank_data["has_sql"] is False
assert blank_data["parameters"] == []
assert blank_data["analysis_rows"] == []
assert blank_data["save_disabled"] is True
assert old_analyze_response.status_code == 404
@pytest.mark.asyncio
async def test_create_query_form_error_redisplays_form_with_values():
ds = Datasette(memory=True, default_deny=True)
ds.root_enabled = True
db = ds.add_memory_database("query_create_form_error", name="data")
await db.execute_write("create table dogs (id integer primary key, name text)")
await ds.invoke_startup()
response = await ds.client.post(
"/data/-/queries/insert",
actor={"id": "root"},
data={
"name": "dogs",
"title": "Dog lookup",
"description": "Find dogs by name",
"sql": "select * from dogs where name = :name",
"is_private": "1",
},
)
assert response.status_code == 400
assert response.headers["content-type"].startswith("text/html")
assert "URL conflicts with an existing table or view" in response.text
assert "Query name conflicts with a table or view" not in response.text
assert '{"ok": false' not in response.text
assert 'value="Dog lookup"' in response.text
assert 'value="dogs"' in response.text
assert ">Find dogs by name" in response.text
assert "select * from dogs where name = :name" in response.text
assert 'name="is_private" value="1" checked' in response.text
public_response = await ds.client.post(
"/data/-/queries/insert",
actor={"id": "root"},
data={
"name": "dogs",
"title": "Public dog lookup",
"description": "Keep this public setting",
"sql": "select * from dogs",
"is_private": "0",
},
)
assert public_response.status_code == 400
assert 'name="is_private" value="1" checked' not in public_response.text
assert 'name="is_private" value="0"' in public_response.text
@pytest.mark.asyncio
async def test_execute_write_get_prepopulates_without_executing():
ds = Datasette(memory=True, default_deny=True)
ds.root_enabled = True
db = ds.add_memory_database("execute_write_get", 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 db.execute_write("create table log (message text)")
await db.execute_write("""
create trigger dogs_after_insert after insert on dogs begin
update cats set name = new.name where id = new.id;
insert into log (message) values (new.name);
end
""")
await ds.invoke_startup()
response = await ds.client.get(
"/data/-/execute-write?sql=insert+into+dogs+(name)+values+('Cleo')",
actor={"id": "root"},
)
assert response.status_code == 200
assert response.headers["content-security-policy"] == "frame-ancestors 'none'"
assert response.headers["x-frame-options"] == "DENY"
assert "Write to this database" in response.text
assert (
"Execute SQL to insert, update or delete rows in this database."
in response.text
)
assert "
Query operations
" in response.text
assert "Start with a template" in response.text
assert '' in response.text
assert 'data-sql-template="insert"' in response.text
assert 'data-sql-template="update"' in response.text
assert 'data-sql-template="delete"' in response.text
assert 'data-analyze-url="/data/-/execute-write/analyze"' in response.text
assert 'addEventListener("paste"' in response.text
assert "setupSqlParameterRefresh" in response.text
assert "datasetteSqlAnalysis.renderAnalysis" in response.text
assert '
' in response.text
assert '
Required permission
' in response.text
assert "
insert
" in response.text
assert "
update
" in response.text
assert "
read
" not in response.text
assert 'action="/data/-/execute-write"' in response.text
assert "insert into dogs (name) values ('Cleo')" in response.text
assert (await db.execute("select count(*) from dogs")).first()[0] == 0
empty_response = await ds.client.get(
"/data/-/execute-write",
actor={"id": "root"},
)
assert '' in empty_response.text
assert 'executeWriteSqlInput.value = "\\n\\n\\n";' in empty_response.text
@pytest.mark.asyncio
async def test_execute_write_analyze_endpoint_uses_sql_only():
ds = Datasette(memory=True, default_deny=True)
ds.root_enabled = True
db = ds.add_memory_database("execute_write_analyze", name="data")
await db.execute_write("create table dogs (id integer primary key, name text)")
await ds.invoke_startup()
response = await ds.client.get(
"/data/-/execute-write/analyze",
actor={"id": "root"},
params={"sql": "insert into dogs (name) values (:name)"},
)
read_only_response = await ds.client.get(
"/data/-/execute-write/analyze",
actor={"id": "root"},
params={"sql": "select * from dogs where name = :name"},
)
assert response.status_code == 200
data = response.json()
assert data["ok"] is True
assert data["parameters"] == ["name"]
assert data["analysis_error"] is None
assert data["execute_disabled"] is False
assert data["analysis_rows"] == [
{
"operation": "insert",
"database": "data",
"table": "dogs",
"required_permission": "insert-row",
"source": None,
"allowed": True,
}
]
assert "params" not in data
assert read_only_response.status_code == 200
read_only_data = read_only_response.json()
assert read_only_data["ok"] is False
assert read_only_data["parameters"] == ["name"]
assert read_only_data["analysis_error"] == (
"Use /-/query for read-only SQL; this endpoint only executes writes"
)
assert read_only_data["execute_disabled"] is True
@pytest.mark.asyncio
async def test_query_parameters_endpoint_uses_get_sql_only():
ds = Datasette(memory=True, default_deny=True)
ds.root_enabled = True
db = ds.add_memory_database("query_parameters", name="data")
await db.execute_write("create table dogs (id integer primary key, name text)")
await ds.invoke_startup()
response = await ds.client.get(
"/data/-/query/parameters",
actor={"id": "root"},
params={
"sql": "select * from dogs where name = :name and id = :id",
},
)
permission_denied_response = await ds.client.get(
"/data/-/query/parameters",
actor={"id": "not-root"},
params={"sql": "select * from dogs where name = :name"},
)
magic_parameter_response = await ds.client.get(
"/data/-/query/parameters",
actor={"id": "root"},
params={"sql": "select :_actor_id"},
)
assert response.status_code == 200
assert response.json() == {"ok": True, "parameters": ["name", "id"]}
assert permission_denied_response.status_code == 403
assert permission_denied_response.json()["errors"] == [
"Permission denied: need execute-sql"
]
assert magic_parameter_response.status_code == 400
assert magic_parameter_response.json()["errors"] == [
"Magic parameters are not allowed"
]
@pytest.mark.asyncio
async def test_database_action_menu_links_to_execute_write_for_permitted_actor():
ds = Datasette(
memory=True,
default_deny=True,
config={
"databases": {
"data": {
"permissions": {
"view-database": {
"id": ["writer", "viewer"],
},
"execute-write-sql": {"id": "writer"},
}
}
}
},
)
ds.add_memory_database("execute_write_menu", name="data")
await ds.invoke_startup()
anonymous_response = await ds.client.get("/data")
viewer_response = await ds.client.get("/data", actor={"id": "viewer"})
writer_response = await ds.client.get("/data", actor={"id": "writer"})
assert anonymous_response.status_code == 403
assert viewer_response.status_code == 200
assert "Execute write SQL" not in viewer_response.text
assert writer_response.status_code == 200
assert "Database actions" in writer_response.text
assert 'href="/data/-/execute-write"' in writer_response.text
assert "Execute write SQL" in writer_response.text
@pytest.mark.asyncio
async def test_database_action_menu_hides_execute_write_for_immutable_database():
ds = Datasette(
memory=True,
default_deny=True,
config={
"databases": {
"data": {
"permissions": {
"view-database": {"id": "writer"},
"execute-write-sql": {"id": "writer"},
}
}
}
},
)
db = ds.add_memory_database("execute_write_menu_immutable", name="data")
db.is_mutable = False
await ds.invoke_startup()
response = await ds.client.get("/data", actor={"id": "writer"})
assert response.status_code == 200
assert "Execute write SQL" not in response.text
assert 'href="/data/-/execute-write"' not in response.text
@pytest.mark.asyncio
async def test_execute_write_get_rejects_immutable_database():
ds = Datasette(memory=True, default_deny=True)
ds.root_enabled = True
db = ds.add_memory_database("execute_write_get_immutable", name="data")
db.is_mutable = False
await ds.invoke_startup()
response = await ds.client.get(
"/data/-/execute-write?sql=insert+into+dogs+(name)+values+('Cleo')",
actor={"id": "root"},
)
assert response.status_code == 403
assert response.json()["errors"] == [
"Cannot execute write SQL because this database is immutable."
]
@pytest.mark.asyncio
async def test_execute_write_post_requires_database_and_table_permissions():
ds = Datasette(
memory=True,
default_deny=True,
config={
"databases": {
"data": {
"permissions": {
"view-database": {"id": "writer"},
"execute-write-sql": {"id": "writer"},
}
}
}
},
)
db = ds.add_memory_database("execute_write_permissions", name="data")
await db.execute_write("create table dogs (id integer primary key, name text)")
await ds.invoke_startup()
no_database_permission = await ds.client.post(
"/data/-/execute-write",
actor={"id": "outsider"},
json={
"sql": "insert into dogs (name) values (:name)",
"params": {"name": "Cleo"},
},
)
no_table_permission = await ds.client.post(
"/data/-/execute-write",
actor={"id": "writer"},
json={
"sql": "insert into dogs (name) values (:name)",
"params": {"name": "Cleo"},
},
)
assert no_database_permission.status_code == 403
assert no_database_permission.json()["errors"] == [
"Permission denied: need execute-write-sql"
]
assert no_table_permission.status_code == 403
assert no_table_permission.json()["errors"] == [
"Permission denied: need insert-row on data/dogs"
]
ds.config = {
"databases": {
"data": {
"permissions": {
"view-database": {"id": "writer"},
"execute-write-sql": {"id": "writer"},
},
"tables": {
"dogs": {
"permissions": {
"insert-row": {"id": "writer"},
}
}
},
}
}
}
allowed = await ds.client.post(
"/data/-/execute-write",
actor={"id": "writer"},
json={
"sql": "insert into dogs (name) values (:name)",
"params": {"name": "Cleo"},
},
)
assert allowed.status_code == 200
assert allowed.json()["ok"] is True
assert allowed.json()["rowcount"] == 1
assert allowed.json()["analysis"][0]["operation"] == "insert"
assert (await db.execute("select name from dogs")).first()[0] == "Cleo"
@pytest.mark.asyncio
async def test_execute_write_insert_links_to_inserted_row():
ds = Datasette(memory=True, default_deny=True)
ds.root_enabled = True
db = ds.add_memory_database("execute_write_insert_link", name="data")
await db.execute_write("create table dogs (id integer primary key, name text)")
await db.execute_write("create table log (id integer primary key, message text)")
await db.execute_write("insert into log (message) values ('existing')")
await db.execute_write("""
create trigger dogs_after_insert after insert on dogs begin
insert into log (message) values (new.name);
end
""")
await ds.invoke_startup()
insert_response = await ds.client.post(
"/data/-/execute-write",
actor={"id": "root"},
data={
"sql": "insert into dogs (name) values (:name)",
"name": "Cleo",
},
)
update_response = await ds.client.post(
"/data/-/execute-write",
actor={"id": "root"},
data={
"sql": "update dogs set name = :name where id = :id",
"name": "Cleo 2",
"id": "1",
},
)
assert insert_response.status_code == 200
assert "Query executed, 1 row affected" in insert_response.text
assert 'View row' in insert_response.text
assert "/data/log/2" not in insert_response.text
assert update_response.status_code == 200
assert "Query executed, 1 row affected" in update_response.text
assert "View row" not in update_response.text
@pytest.mark.asyncio
async def test_execute_write_post_rejects_read_only_sql():
ds = Datasette(memory=True, default_deny=True)
ds.root_enabled = True
db = ds.add_memory_database("execute_write_read_only", name="data")
await db.execute_write("create table dogs (id integer primary key, name text)")
await ds.invoke_startup()
response = await ds.client.post(
"/data/-/execute-write",
actor={"id": "root"},
json={"sql": "select * from dogs"},
)
assert response.status_code == 400
assert response.json()["errors"] == [
"Use /-/query for read-only SQL; this endpoint only executes writes"
]
@pytest.mark.asyncio
async def test_query_owner_gets_update_delete_and_writable_view_defaults():
ds = Datasette(memory=True, default_deny=True)
ds.add_memory_database("query_owner_defaults", name="data")
await ds.invoke_startup()
await ds.add_query(
"data",
"insert_dog",
"insert into dogs (name) values (:name)",
is_write=True,
source="user",
owner_id="alice",
)
for action in ("view-query", "update-query", "delete-query"):
assert await ds.allowed(
action=action,
resource=QueryResource("data", "insert_dog"),
actor={"id": "alice"},
)
assert not await ds.allowed(
action=action,
resource=QueryResource("data", "insert_dog"),
actor={"id": "bob"},
)
@pytest.mark.asyncio
async def test_user_writable_query_execution_rechecks_table_permissions():
ds = Datasette(
memory=True,
default_deny=True,
config={
"databases": {
"data": {
"permissions": {
"view-database": {"id": ["alice", "bob"]},
"execute-write-sql": {"id": ["alice", "bob"]},
},
"tables": {
"dogs": {
"permissions": {
"insert-row": {"id": "alice"},
}
}
},
}
}
},
)
db = ds.add_memory_database("query_write_execution", 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 (:name)",
is_write=True,
source="user",
owner_id="alice",
)
await ds.add_query(
"data",
"insert_cat",
"insert into dogs (name) values (:name)",
is_write=True,
source="user",
owner_id="bob",
)
allowed_response = await ds.client.post(
"/data/insert_dog?_json=1",
actor={"id": "alice"},
data={"name": "Cleo"},
)
denied_response = await ds.client.post(
"/data/insert_cat?_json=1",
actor={"id": "bob"},
data={"name": "Milo"},
)
assert allowed_response.status_code == 200
assert allowed_response.json()["ok"] is True
assert denied_response.status_code == 403
rows = (await db.execute("select name from dogs")).dicts()
assert rows == [{"name": "Cleo"}]