admin felület fejlesztése garázs, előfizeztési csomagok

This commit is contained in:
Roo
2026-06-25 01:58:04 +00:00
parent 80a5d67f79
commit 52011606ff
334 changed files with 46636 additions and 1606 deletions

View 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}.",
)