diff --git a/README.md b/README.md index 3552be9..56592f7 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ These permission checks will call the `permission_allowed()` plugin hook with th You can instead use more finely-grained permissions. - `edit-schema-create-table` allows users to create a new table. The `resource` will be the name of the database. +- `edit-schema-alter-table` allows users to alter the schema of a table. The `resource` will be a tuple of `(database_name, table_name)`. ## Screenshot diff --git a/datasette_edit_schema/__init__.py b/datasette_edit_schema/__init__.py index 2119f6d..939de29 100644 --- a/datasette_edit_schema/__init__.py +++ b/datasette_edit_schema/__init__.py @@ -29,9 +29,7 @@ def permission_allowed(actor, action, resource): @hookimpl def table_actions(datasette, actor, database, table): async def inner(): - if not await datasette.permission_allowed( - actor, "edit-schema", resource=database, default=False - ): + if not await can_alter_table(datasette, actor, database, table): return [] return [ { @@ -58,6 +56,19 @@ async def can_create_table(datasette, actor, database): return False +async def can_alter_table(datasette, actor, database, table): + if await datasette.permission_allowed( + actor, "edit-schema", resource=database, default=False + ): + return True + # Or maybe they have edit-schema-create-table + if await datasette.permission_allowed( + actor, "edit-schema-alter-table", resource=(database, table), default=False + ): + return True + return False + + @hookimpl def database_actions(datasette, actor, database): async def inner(): @@ -262,7 +273,10 @@ async def edit_schema_table(request, datasette): table = unquote_plus(request.url_vars["table"]) databases = get_databases(datasette) database_name = request.url_vars["database"] - await check_permissions(datasette, request, database_name) + + if not await can_alter_table(datasette, request.actor, database_name, table): + raise Forbidden("Permission denied for edit-schema-alter-table") + try: database = [db for db in databases if db.name == database_name][0] except IndexError: diff --git a/tests/test_edit_schema.py b/tests/test_edit_schema.py index cd5a96d..59872dd 100644 --- a/tests/test_edit_schema.py +++ b/tests/test_edit_schema.py @@ -26,21 +26,43 @@ async def test_csrf_required(db_path): @pytest.mark.parametrize( - "authenticated,path,should_allow", + "actor_id,should_allow", ( - (False, "/data/creatures", False), - (True, "/data/creatures", True), + (None, False), + ("user_with_edit_schema", True), + ("user_with_alter_table", True), + ("user_with_create_table", False), + ("user_with_no_perms", False), ), ) @pytest.mark.asyncio -async def test_table_actions(db_path, authenticated, path, should_allow): - ds = Datasette([db_path]) +async def test_table_actions(permission_plugin, ds, actor_id, should_allow): + ds._rules_allow = [ + Rule( + actor_id="user_with_edit_schema", + action="edit-schema", + database="data", + resource=None, + ), + Rule( + actor_id="user_with_alter_table", + action="edit-schema-alter-table", + database="data", + resource="creatures", + ), + Rule( + actor_id="user_with_create_table", + action="edit-schema-create-table", + database="data", + resource=None, + ), + ] cookies = None - if authenticated: - cookies = {"ds_actor": ds.sign({"a": {"id": "root"}}, "actor")} - response = await ds.client.get(path, cookies=cookies) + if actor_id: + cookies = {"ds_actor": ds.sign({"a": {"id": actor_id}}, "actor")} + response = await ds.client.get("/data/creatures", cookies=cookies) assert response.status_code == 200 - fragment = '