#!/usr/bin/env python3 """ šŸ”§ P0 EXECUTION - Fix Phantom Permissions Inserts 8 orphaned permission codes into system.permissions and maps them to SUPERADMIN (role_id=1) and ADMIN (role_id=2) with granted=True. The 8 missing codes: - dual-control:request (security) - dual-control:approve (security) - dual-control:view (security) - services:manage (service) - subscription:manage (subscription) - user:manage (user) - moderation:manage (moderation) - gamification:manage (gamification) Run: docker compose exec sf_api python3 /app/backend/scripts/fix_phantom_permissions.py """ import asyncio import logging import os from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy import text, select from sqlalchemy.orm import sessionmaker logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) DATABASE_URL = os.getenv( "DATABASE_URL", "postgresql+asyncpg://sf_user:sf_password@postgres:5432/service_finder", ) # ── The 8 orphaned permissions ── PHANTOM_PERMISSIONS = [ {"code": "dual-control:request", "domain": "security", "action": "request"}, {"code": "dual-control:approve", "domain": "security", "action": "approve"}, {"code": "dual-control:view", "domain": "security", "action": "view"}, {"code": "services:manage", "domain": "service", "action": "manage"}, {"code": "subscription:manage", "domain": "subscription", "action": "manage"}, {"code": "user:manage", "domain": "user", "action": "manage"}, {"code": "moderation:manage", "domain": "moderation", "action": "manage"}, {"code": "gamification:manage", "domain": "gamification", "action": "manage"}, ] # Target roles: SUPERADMIN=1, ADMIN=2 TARGET_ROLE_IDS = [1, 2] async def main(): engine = create_async_engine(DATABASE_URL, echo=False) async_session_factory = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) async with async_session_factory() as session: # ── Step 1: Fetch existing permission codes ── result = await session.execute(text("SELECT code FROM system.permissions")) existing_codes = {row[0] for row in result.all()} logger.info(f"Existing permission codes ({len(existing_codes)}): {sorted(existing_codes)}") # ── Step 2: Insert missing permissions ── inserted_ids = {} # code -> id for perm in PHANTOM_PERMISSIONS: if perm["code"] in existing_codes: logger.info(f" ā­ļø Already exists: {perm['code']}") # Fetch its id row = (await session.execute( text("SELECT id FROM system.permissions WHERE code = :code"), {"code": perm["code"]}, )).scalar_one_or_none() if row: inserted_ids[perm["code"]] = row continue logger.info(f" āž• Inserting: {perm['code']} (domain={perm['domain']})") row = (await session.execute( text(""" INSERT INTO system.permissions (code, domain, action, description, is_system) VALUES (:code, :domain, :action, :description, TRUE) RETURNING id """), { "code": perm["code"], "domain": perm["domain"], "action": perm["action"], "description": f"Auto-inserted by P0 fix: {perm['code']}", }, )).scalar_one() inserted_ids[perm["code"]] = row await session.commit() logger.info(f"āœ… Permission insertion complete. {len(inserted_ids)} codes tracked.") # ── Step 3: Fetch existing role_permission pairs ── result = await session.execute( text("SELECT role_id, permission_id FROM system.role_permissions WHERE granted = TRUE") ) existing_pairs = {(row[0], row[1]) for row in result.all()} logger.info(f"Existing role_permission pairs: {len(existing_pairs)}") # ── Step 4: Insert missing role_permission mappings ── inserted_count = 0 for code, perm_id in inserted_ids.items(): for role_id in TARGET_ROLE_IDS: if (role_id, perm_id) in existing_pairs: logger.info(f" ā­ļø Already mapped: role_id={role_id} -> {code}") continue logger.info(f" āž• Mapping: role_id={role_id} -> {code} (perm_id={perm_id})") await session.execute( text(""" INSERT INTO system.role_permissions (role_id, permission_id, granted) VALUES (:role_id, :perm_id, TRUE) """), {"role_id": role_id, "perm_id": perm_id}, ) inserted_count += 1 await session.commit() logger.info(f"āœ… Role-permission mapping complete. {inserted_count} new mappings inserted.") # ── Step 5: Verify ── logger.info("\nšŸ” VERIFICATION:") result = await session.execute( text(""" SELECT p.code, r.name, rp.granted FROM system.role_permissions rp JOIN system.permissions p ON p.id = rp.permission_id JOIN system.roles r ON r.id = rp.role_id WHERE p.code IN :codes ORDER BY p.code, r.name """), {"codes": tuple(perm["code"] for perm in PHANTOM_PERMISSIONS)}, ) for row in result.all(): logger.info(f" {row[0]} | role={row[1]} | granted={row[2]}") await engine.dispose() logger.info("\nšŸŽ‰ Done. All 8 phantom permissions have been inserted and mapped.") if __name__ == "__main__": asyncio.run(main())