From 1da744787d6133c530a936a3355b9e84f4c3955b Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Fri, 30 Aug 2024 17:29:50 -0700 Subject: [PATCH] Populate new acl_audit table, refs #7 --- datasette_acl/__init__.py | 52 +++++++++++++++++++++++++--- tests/test_acl.py | 72 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 117 insertions(+), 7 deletions(-) diff --git a/datasette_acl/__init__.py b/datasette_acl/__init__.py index a42f216..7fbef42 100644 --- a/datasette_acl/__init__.py +++ b/datasette_acl/__init__.py @@ -5,20 +5,20 @@ CREATE_TABLES_SQL = """ create table if not exists acl_resources ( - id integer primary key autoincrement, + id integer primary key, database text not null, resource text, unique(database, resource) ); create table if not exists acl_actions ( - id integer primary key autoincrement, + id integer primary key, name text not null unique ); -- new table for groups create table if not exists acl_groups ( - id integer primary key autoincrement, + id integer primary key, name text not null unique ); @@ -31,7 +31,7 @@ ); create table if not exists acl ( - acl_id integer primary key autoincrement, + acl_id integer primary key, actor_id text, group_id integer, resource_id integer, @@ -42,6 +42,21 @@ check ((actor_id is null) != (group_id is null)), unique(actor_id, group_id, resource_id, action_id) ); + +-- Audit log +create table if not exists acl_audit ( + id integer primary key, + timestamp text default (datetime('now')), + operation_by text, + operation text check (operation in ('added', 'removed')), + action_id integer, + resource_id integer, + group_id integer, + actor_id text, + foreign key (group_id) references acl_groups(id), + foreign key (resource_id) references acl_resources(id), + foreign key (action_id) references acl_actions(id) +) """ ACL_RESOURCE_PAIR_SQL = """ @@ -323,6 +338,7 @@ async def table_acls(request, datasette): "resource_id": resource_id, }, ) + operation = "added" changes_made["added"].append((group_name, action_name)) else: # They removed it @@ -340,8 +356,34 @@ async def table_acls(request, datasette): "resource_id": resource_id, }, ) + operation = "removed" changes_made["removed"].append((group_name, action_name)) - + await internal_db.execute_write( + """ + insert into acl_audit ( + operation, + actor_id, + group_id, + resource_id, + action_id, + operation_by + ) values ( + :operation, + null, + (SELECT id FROM acl_groups WHERE name = :group_name), + :resource_id, + (SELECT id FROM acl_actions WHERE name = :action_name), + :operation_by + ) + """, + { + "operation": operation, + "group_name": group_name, + "resource_id": resource_id, + "action_name": action_name, + "operation_by": request.actor["id"], + }, + ) datasette.add_message(request, f"Made changes: {repr(changes_made)}") return Response.redirect(request.path) diff --git a/tests/test_acl.py b/tests/test_acl.py index 82f5351..a6c6adc 100644 --- a/tests/test_acl.py +++ b/tests/test_acl.py @@ -104,6 +104,8 @@ async def test_permission_allowed(): actor=admin_actor, action="insert-row", resource=["db", "t"] ) assert not allowed + # acl_audit table should be empty + assert (await db.execute("select count(*) from acl_audit")).single_value() == 0 # Use the /db/table/-/acl page to insert a permission csrf_token_response = await datasette.client.get( "/db/t/-/acl", @@ -150,9 +152,75 @@ async def test_permission_allowed(): "resource_name": "t", } ] - allowed = await datasette.permission_allowed( + # Should have added to the audit table + AUDIT_SQL = """ + select + acl_groups.name as group_name, + acl_actions.name as action_name, + acl_resources.database as database_name, + acl_resources.resource as resource_name, + acl_audit.operation_by, + acl_audit.operation + from acl_audit + join acl_groups on acl_audit.group_id = acl_groups.id + join acl_actions on acl_audit.action_id = acl_actions.id + join acl_resources on acl_audit.resource_id = acl_resources.id + order by acl_audit.id + """ + audit_rows = [dict(r) for r in (await db.execute(AUDIT_SQL))] + assert audit_rows == [ + { + "group_name": "admin", + "action_name": "insert-row", + "database_name": "db", + "resource_name": "t", + "operation_by": "root", + "operation": "added", + } + ] + # Now the admin actor should be able to insert a row + assert await datasette.permission_allowed( actor=admin_actor, action="insert-row", resource=["db", "t"], ) - assert allowed + # remove insert-row and add alter-table and check the audit log + response2 = await datasette.client.post( + "/db/t/-/acl", + data={ + "permissions_admin_alter-table": "on", + "csrftoken": csrftoken, + }, + cookies={ + "ds_actor": datasette.client.actor_cookie({"id": "root"}), + "ds_csrftoken": csrftoken, + }, + ) + assert response2.status_code == 302 + audit_rows2 = [dict(r) for r in (await db.execute(AUDIT_SQL))] + assert audit_rows2 == [ + { + "group_name": "admin", + "action_name": "insert-row", + "database_name": "db", + "resource_name": "t", + "operation_by": "root", + "operation": "added", + }, + { + "group_name": "admin", + "action_name": "insert-row", + "database_name": "db", + "resource_name": "t", + "operation_by": "root", + "operation": "removed", + }, + { + "group_name": "admin", + "action_name": "alter-table", + "database_name": "db", + "resource_name": "t", + "operation_by": "root", + "operation": "added", + }, + ]