Skip to content

Commit

Permalink
Tests and docs for per-table permissions
Browse files Browse the repository at this point in the history
Also rename table now requires drop-table and create-table permissions.

Closes #59, closes #60
  • Loading branch information
simonw committed Feb 18, 2024
1 parent e81a3a9 commit 03f7f53
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 1 deletion.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,34 @@ These permission checks will call the `permission_allowed()` plugin hook with th
- `actor` will be the currently authenticated actor - usually a dictionary
- `resource` will be the string name of the database

You can instead use more finely-grained permissions.
You can instead use more finely-grained permissions from the default Datasette permissions collection:

- `create-table` allows users to create a new table. The `resource` will be the name of the database.
- `drop-table` allows users to drop a table. The `resource` will be a tuple of `(database_name, table_name)`.
- `alter-table` allows users to alter a table. The `resource` will be a tuple of `(database_name, table_name)`.

To rename a table a user must have both `drop-table` permission for that table and `create-table` permission for that database.

For example, to configure Datasette to allow the user with ID `pelican` to create, alter and drop tables in the `marketing` database and to alter just the `notes` table in the `sales` database, you could use the following configuration:

```yaml
databases:
marketing:
permissions:
create-table:
id: pelican
drop-table:
id: pelican
alter-table:
id: pelican
sales:
tables:
notes:
permissions:
alter-table:
id: pelican
```
## Events
This plugin fires `create-table`, `alter-table` and `drop-table` events when tables are modified, using the [Datasette Events](https://docs.datasette.io/en/latest/events.html) system introduced in [Datasette 1.0a8](https://docs.datasette.io/en/latest/changelog.html#a8-2024-02-07).
Expand Down
20 changes: 20 additions & 0 deletions datasette_edit_schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ async def can_alter_table(datasette, actor, database, table):
return False


async def can_rename_table(datasette, actor, database, table):
if not await can_drop_table(datasette, actor, database, table):
return False
if not await can_create_table(datasette, actor, database):
return False
return True


async def can_drop_table(datasette, actor, database, table):
if await datasette.permission_allowed(
actor, "edit-schema", resource=database, default=False
Expand Down Expand Up @@ -606,6 +614,9 @@ def get_columns_and_schema_and_fks_and_pks_and_indexes(conn):
"can_drop_table": await can_drop_table(
datasette, request.actor, database_name, table
),
"can_rename_table": await can_rename_table(
datasette, request.actor, database_name, table
),
},
request=request,
)
Expand Down Expand Up @@ -699,6 +710,15 @@ async def rename_table(request, datasette, database, table, formdata):
)
return redirect

# User must have drop-table permission on old table and create-table on new table
if not await can_rename_table(datasette, request.actor, database, table):
datasette.add_message(
request,
"Permission denied to rename table '{}'".format(table),
datasette.ERROR,
)
return redirect

try:
before_schema = await database.execute_fn(
lambda conn: sqlite_utils.Database(conn)[table].schema
Expand Down
2 changes: 2 additions & 0 deletions datasette_edit_schema/templates/edit_schema_table.html
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
{% block content %}
<h1>Edit table <a href="{{ base_url }}{{ database.name|quote_plus }}/{{ table|quote_plus }}">{{ database.name }}/{{ table }}</a></h1>

{% if can_rename_table %}
<h2>Rename table</h2>

<form action="{{ base_url }}-/edit-schema/{{ database.name|quote_plus }}/{{ table|quote_plus }}" method="post">
Expand All @@ -96,6 +97,7 @@ <h2>Rename table</h2>
<input type="hidden" name="rename_table" value="1">
<input type="submit" value="Rename">
</form>
{% endif %}

<form action="{{ base_url }}-/edit-schema/{{ database.name|quote_plus }}/{{ table|quote_plus }}" method="post">
<h2>Change existing columns</h2>
Expand Down
62 changes: 62 additions & 0 deletions tests/test_edit_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1091,3 +1091,65 @@ async def test_add_remove_index(
for index in indexes
if "sqlite_autoindex" not in index.name
] == expected_indexes


@pytest.mark.asyncio
async def test_database_and_table_level_permissions(tmp_path):
marketing_path = str(tmp_path / "marketing.db")
sales_path = str(tmp_path / "sales.db")
marketing_db = sqlite_utils.Database(marketing_path)
marketing_db["one"].insert({"id": 1}, pk="id")
sales_db = sqlite_utils.Database(sales_path)
sales_db["notes"].insert({"id": 1, "note": "Hello"}, pk="id")
sales_db["not_allowed"].insert({"id": 1}, pk="id")

ds = Datasette(
[marketing_path, sales_path],
config={
"databases": {
"marketing": {
"permissions": {
"create-table": {"id": "pelican"},
"drop-table": {"id": "pelican"},
"alter-table": {"id": "pelican"},
}
},
"sales": {
"tables": {
"notes": {"permissions": {"alter-table": {"id": "pelican"}}}
}
},
}
},
)

pelican_cookies = {"ds_actor": ds.sign({"a": {"id": "pelican"}}, "actor")}
walrus_cookies = {"ds_actor": ds.sign({"a": {"id": "walrus"}}, "actor")}

async def pelican_can_see(path):
response = await ds.client.get(path, cookies=pelican_cookies)
return response if response.status_code == 200 else None

async def walrus_can_see(path):
response = await ds.client.get(path, cookies=walrus_cookies)
return response if response.status_code == 200 else None

assert await pelican_can_see("/-/edit-schema/marketing/one")
assert not await walrus_can_see("/-/edit-schema/marketing/one")

# pelican cannot edit sales/not_allowed
assert not await pelican_can_see("/-/edit-schema/sales/not_allowed")
assert not await walrus_can_see("/-/edit-schema/sales/not_allowed")

# pelican can edit notes - but not drop or rename it
response = await pelican_can_see("/-/edit-schema/sales/notes")
assert response
assert '<input type="submit" value="Add column">' in response.text
assert 'value="Drop this table">' not in response.text
assert ' <input type="submit" value="Rename">' not in response.text

# But they can drop table or rename table in marketing/one
response2 = await pelican_can_see("/-/edit-schema/marketing/one")
assert response2
assert 'value="Drop this table">' in response2.text
assert ' <input type="submit" value="Rename">' in response2.text

0 comments on commit 03f7f53

Please sign in to comment.