From 0cd061196cb3d74aa31b37c2eef42bae57fe9233 Mon Sep 17 00:00:00 2001 From: Patrick Buckley Date: Sat, 14 Mar 2026 17:03:21 -0700 Subject: [PATCH] fix: catch-up migration ensuring builtin-admin has all 20 permissions (#63) Migrations 011-016 each appended a permission to the builtin-admin role via conditional UPDATE, but on some deployments these never applied. Migration 017 idempotently sets the complete permission string rather than appending incrementally. Must be merged after feat/admin-mcp-servers (migration 016). --- .../versions/017_catchup_admin_permissions.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 turnstone/core/storage/migrations/versions/017_catchup_admin_permissions.py diff --git a/turnstone/core/storage/migrations/versions/017_catchup_admin_permissions.py b/turnstone/core/storage/migrations/versions/017_catchup_admin_permissions.py new file mode 100644 index 00000000..30640aa7 --- /dev/null +++ b/turnstone/core/storage/migrations/versions/017_catchup_admin_permissions.py @@ -0,0 +1,46 @@ +"""Catch-up: ensure builtin-admin role has all current permissions. + +Migrations 011-016 each appended a permission to the builtin-admin role, +but on some deployments these UPDATE statements did not take effect +(e.g. due to version stamping without running, or create_all bypassing +Alembic). This migration idempotently ensures the builtin-admin role +has the complete permission set. + +Revision ID: 017 +Revises: 016 +Create Date: 2026-03-14 +""" + +import sqlalchemy as sa +from alembic import op + +revision = "017" +down_revision = "016" +branch_labels = None +depends_on = None + +# The complete set of permissions the builtin-admin role should have. +# Must stay in sync with _VALID_PERMISSIONS in console/server.py. +_EXPECTED_ADMIN_PERMS = ( + "read,write,approve," + "admin.users,admin.roles,admin.orgs," + "admin.policies,admin.templates,admin.ws_templates," + "admin.audit,admin.usage," + "admin.schedules,admin.watches," + "admin.judge,admin.memories,admin.settings,admin.mcp," + "tools.approve,workstreams.create,workstreams.close" +) + + +def upgrade() -> None: + conn = op.get_bind() + conn.execute( + sa.text("UPDATE roles SET permissions = :perms WHERE role_id = 'builtin-admin'"), + {"perms": _EXPECTED_ADMIN_PERMS}, + ) + + +def downgrade() -> None: + # No-op: we don't remove permissions on downgrade since we can't + # know which subset the deployment originally had. + pass