213 lines
7.0 KiB
Python
213 lines
7.0 KiB
Python
# /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}.",
|
|
)
|