admin felület fejlesztése garázs, előfizeztési csomagok
This commit is contained in:
212
backend/app/api/v1/endpoints/admin_permissions.py
Normal file
212
backend/app/api/v1/endpoints/admin_permissions.py
Normal file
@@ -0,0 +1,212 @@
|
||||
# /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}.",
|
||||
)
|
||||
Reference in New Issue
Block a user