Files
service-finder/backend/scripts/fix_phantom_permissions.py

145 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())