"""Converged DELETE grants (F20/Astra). Removes the effect of the blanket ``GRANT DELETE ON ALL TABLES`` that prestart.sh applied on every boot — which silently undid migration 0100's protections on every container start. Documented target state: Runtime-legitimate DELETEs (crm_api only): - users, user_tenants (user deletion on last membership, BUG-030) - sessions (logout session invalidation) - plugins, notification_types (plugin uninstall + registry sync) Protected — DELETE stays REVOKED from crm_api AND crm_worker: - audit_log (Astra acceptance: API/Worker write, never delete) - api_tokens (revoke is an UPDATE on revoked_at) - password_reset_tokens (consumption is an UPDATE on used_at) - plugin_allowlist, plugin_migrations (install/migration path only — plugin_migrations rows are deleted via the migration factory) - tenants (never deleted at runtime) - tenant_plugin_activation (deactivation is an UPDATE) crm_worker receives no DELETE on any protected table (workers never delete users, sessions or plugin rows). Revision ID: 0145 Revises: 0144 """ from alembic import op revision = "0145" down_revision = "0144" branch_labels = None depends_on = None # Tables where runtime DELETE is a documented, legitimate operation (crm_api) RUNTIME_DELETE_TABLES = [ "users", "user_tenants", "sessions", "plugins", "notification_types", ] # Tables where DELETE must stay revoked from BOTH runtime roles (0100 + F20) PROTECTED_TABLES = [ "audit_log", "api_tokens", "password_reset_tokens", "plugin_allowlist", "plugin_migrations", "tenants", "tenant_plugin_activation", ] def upgrade() -> None: # 1. Re-assert 0100's revocations — production DBs have lived with the # blanket boot grant, so revoke first for a deterministic baseline. for table in PROTECTED_TABLES: op.execute(f"REVOKE DELETE ON TABLE {table} FROM crm_api;") op.execute(f"REVOKE DELETE ON TABLE {table} FROM crm_worker;") # 2. Grant the runtime-legitimate DELETEs to crm_api (BUG-030 stays # fixed, logout keeps working, plugin management keeps working). for table in RUNTIME_DELETE_TABLES: op.execute(f"GRANT DELETE ON TABLE {table} TO crm_api;") def downgrade() -> None: # Best-effort inverse: revoke the runtime grants, re-grant the # protected tables (matching the pre-F20 blanket state). for table in RUNTIME_DELETE_TABLES: op.execute(f"REVOKE DELETE ON TABLE {table} FROM crm_api;") for table in PROTECTED_TABLES: op.execute(f"GRANT DELETE ON TABLE {table} TO crm_api;") op.execute(f"GRANT DELETE ON TABLE {table} TO crm_worker;")