145 lines
5.8 KiB
Python
145 lines
5.8 KiB
Python
#!/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())
|