Skip to content

Commit

Permalink
Extra permission rules for /-/create, closes #1937
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Dec 14, 2022
1 parent e238df3 commit c094dde
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 1 deletion.
14 changes: 14 additions & 0 deletions datasette/views/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,13 @@ async def post(self, request):
ignore = data.get("ignore")
replace = data.get("replace")

if replace:
# Must have update-row permission
if not await self.ds.permission_allowed(
request.actor, "update-row", resource=database_name
):
return _error(["Permission denied - need update-row"], 403)

table_name = data.get("table")
if not table_name:
return _error(["Table is required"])
Expand All @@ -630,6 +637,13 @@ async def post(self, request):
if rows and row:
return _error(["Cannot specify both rows and row"])

if rows or row:
# Must have insert-row permission
if not await self.ds.permission_allowed(
request.actor, "insert-row", resource=database_name
):
return _error(["Permission denied - need insert-row"], 403)

if columns:
if rows or row:
return _error(["Cannot specify columns with rows or row"])
Expand Down
7 changes: 6 additions & 1 deletion docs/json_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -830,7 +830,8 @@ If the table is successfully created this will return a ``201`` status code and
Creating a table from example data
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Instead of specifying ``columns`` directly you can instead pass a single example ``row`` or a list of ``rows``. Datasette will create a table with a schema that matches those rows and insert them for you:
Instead of specifying ``columns`` directly you can instead pass a single example ``row`` or a list of ``rows``.
Datasette will create a table with a schema that matches those rows and insert them for you:

::

Expand All @@ -855,6 +856,8 @@ Instead of specifying ``columns`` directly you can instead pass a single example
"pk": "id"
}
Doing this requires both the :ref:`permissions_create_table` and :ref:`permissions_insert_row` permissions.

The ``201`` response here will be similar to the ``columns`` form, but will also include the number of rows that were inserted as ``row_count``:

.. code-block:: json
Expand Down Expand Up @@ -884,6 +887,8 @@ If you pass a row to the create endpoint with a primary key that already exists
You can avoid this error by passing the same ``"ignore": true`` or ``"replace": true`` options to the create endpoint as you can to the :ref:`insert endpoint <TableInsertView>`.

To use the ``"replace": true`` option you will also need the :ref:`permissions_update_row` permission.

.. _TableDropView:

Dropping tables
Expand Down
44 changes: 44 additions & 0 deletions tests/test_api_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -1096,6 +1096,50 @@ async def test_create_table(ds_write, input, expected_status, expected_response)
assert data == expected_response


@pytest.mark.asyncio
@pytest.mark.parametrize(
"permissions,body,expected_status,expected_errors",
(
(["create-table"], {"table": "t", "columns": [{"name": "c"}]}, 201, None),
# Need insert-row too if you use "rows":
(
["create-table"],
{"table": "t", "rows": [{"name": "c"}]},
403,
["Permission denied - need insert-row"],
),
# This should work:
(
["create-table", "insert-row"],
{"table": "t", "rows": [{"name": "c"}]},
201,
None,
),
# If you use replace: true you need update-row too:
(
["create-table", "insert-row"],
{"table": "t", "rows": [{"id": 1}], "pk": "id", "replace": True},
403,
["Permission denied - need update-row"],
),
),
)
async def test_create_table_permissions(
ds_write, permissions, body, expected_status, expected_errors
):
token = ds_write.create_token("root", restrict_all=["view-instance"] + permissions)
response = await ds_write.client.post(
"/data/-/create",
json=body,
headers=_headers(token),
)
assert response.status_code == expected_status
if expected_errors:
data = response.json()
assert data["ok"] is False
assert data["errors"] == expected_errors


@pytest.mark.asyncio
@pytest.mark.parametrize(
"input,expected_rows_after",
Expand Down

0 comments on commit c094dde

Please sign in to comment.