# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/admin_permissions.py """ 🎯 RBAC Permission Matrix & Override Endpoints (P0) Provides admin-facing endpoints for: - GET /admin/permissions/matrix — Returns the full permission matrix showing which roles can perform which actions. - PATCH /admin/permissions/override/{org_id} — Updates custom permissions for a specific organization (scope-based override). All endpoints are protected by Depends(RequireRole(...)) so only authorized staff (SUPERADMIN, ADMIN) can access them. """ from __future__ import annotations import logging from typing import Any, Dict, List, Optional from fastapi import APIRouter, Depends, HTTPException, status, Body from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, update from app.api import deps from app.api.deps import RequireRole, get_db, get_current_user from app.models.identity import User, UserRole from app.models.marketplace.organization import Organization from app.services.rbac_service import ( rbac_service, AdminAction, ScopeType, ADMIN_SCOPE_ACTIONS, MODERATOR_SCOPE_ACTIONS, SALES_REP_SCOPE_ACTIONS, SERVICE_MGR_SCOPE_ACTIONS, ) from pydantic import BaseModel, Field logger = logging.getLogger(__name__) router = APIRouter() # ────────────────────────────────────────────── # Pydantic Schemas # ────────────────────────────────────────────── class PermissionMatrixEntry(BaseModel): """A single entry in the permission matrix.""" role: str actions: List[str] class PermissionMatrixResponse(BaseModel): """Full permission matrix response.""" matrix: List[PermissionMatrixEntry] all_actions: List[str] class PermissionOverrideRequest(BaseModel): """Request body for overriding organization permissions.""" action: str = Field(..., description="The action to override (e.g., EDIT_DATA)") granted: bool = Field(..., description="Whether to grant or revoke this action") class PermissionOverrideResponse(BaseModel): """Response after updating custom permissions.""" status: str org_id: int action: str granted: bool message: str # ────────────────────────────────────────────── # Endpoints # ────────────────────────────────────────────── @router.get( "/admin/permissions/matrix", response_model=PermissionMatrixResponse, tags=["Admin Permissions"], summary="Get the full RBAC permission matrix", ) async def get_permission_matrix( current_user: User = Depends(get_current_user), _ = Depends(RequireRole([UserRole.SUPERADMIN, UserRole.ADMIN])), ): """ 🔐 Returns the complete RBAC permission matrix. Shows which roles are permitted to perform which admin actions. Only SUPERADMIN and ADMIN roles can access this endpoint. Returns: - matrix: List of {role, actions[]} entries - all_actions: Complete list of all possible admin actions """ matrix = [] for role in [UserRole.SUPERADMIN, UserRole.ADMIN, UserRole.MODERATOR, UserRole.SALES_REP, UserRole.SERVICE_MGR]: actions = rbac_service.get_permitted_actions(role) matrix.append(PermissionMatrixEntry( role=role.value, actions=sorted(actions), )) all_actions = [a.value for a in AdminAction] return PermissionMatrixResponse( matrix=matrix, all_actions=sorted(all_actions), ) @router.patch( "/admin/permissions/override/{org_id}", response_model=PermissionOverrideResponse, tags=["Admin Permissions"], summary="Override custom permissions for an organization", ) async def override_org_permission( org_id: int, payload: PermissionOverrideRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), _ = Depends(RequireRole([UserRole.SUPERADMIN, UserRole.ADMIN])), ): """ 🔐 Updates custom permissions for a specific organization. This endpoint allows SUPERADMIN and ADMIN to grant or revoke specific actions for an organization, overriding the default role-based permissions. The RBACService.check_admin_access() is invoked to verify that the requesting admin has scope-based access to the target organization before applying the override. Args: org_id: The target organization ID. payload.action: The action to override (e.g., "EDIT_DATA"). payload.granted: True to grant, False to revoke. Returns: Confirmation of the override operation. """ # 1. Validate that the action exists valid_actions = {a.value for a in AdminAction} if payload.action not in valid_actions: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid action '{payload.action}'. Valid actions: {sorted(valid_actions)}", ) # 2. Verify scope-based access to the target organization try: await rbac_service.check_admin_access( db=db, user=current_user, action=AdminAction.EDIT_DATA, target_org_id=org_id, ) except PermissionError as e: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=str(e), ) # 3. Fetch the organization stmt = select(Organization).where(Organization.id == org_id) result = await db.execute(stmt) org = result.scalar_one_or_none() if not org: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Organization {org_id} not found.", ) # 4. Update custom permissions via the settings JSONB field # The Organization model has a `settings` JSONB column that stores # dynamic org-level configuration. We store permission overrides # under a "permission_overrides" key within settings. if not isinstance(org.settings, dict): org.settings = {} # Ensure the permission_overrides sub-key exists if "permission_overrides" not in org.settings: org.settings["permission_overrides"] = {} # Apply the override org.settings["permission_overrides"][payload.action] = payload.granted # 5. Persist await db.commit() await db.refresh(org) logger.info( f"Permission override applied: org={org_id}, " f"action={payload.action}, granted={payload.granted}, " f"by_user={current_user.id}" ) return PermissionOverrideResponse( status="success", org_id=org_id, action=payload.action, granted=payload.granted, message=f"Permission '{payload.action}' {'granted' if payload.granted else 'revoked'} for organization {org_id}.", )