jogosultsági szintek RBAC beállítva tesztelve
This commit is contained in:
@@ -7,9 +7,16 @@ Provides admin-facing endpoints for:
|
||||
showing which roles can perform which actions.
|
||||
- PATCH /admin/permissions/override/{org_id} — Updates custom permissions
|
||||
for a specific organization (scope-based override).
|
||||
- GET /admin/permissions/roles — Returns all SystemRole records.
|
||||
- GET /admin/permissions — Returns all SystemPermission records.
|
||||
- PUT /admin/permissions/roles/{role_id}/permissions — Updates the granted
|
||||
permissions for a role (bulk toggle).
|
||||
|
||||
All endpoints are protected by Depends(RequireRole(...)) so only
|
||||
authorized staff (SUPERADMIN, ADMIN) can access them.
|
||||
All endpoints are protected by Depends(RequirePermission(...)) so only
|
||||
authorized staff with the appropriate DB-driven permissions can access them.
|
||||
|
||||
P0 Phase 6: Removed AdminAction enum dependency. All permission lookups
|
||||
are now fully DB-driven via system.role_permissions.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -18,20 +25,17 @@ 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 sqlalchemy import select, update, delete
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.api import deps
|
||||
from app.api.deps import RequireRole, get_db, get_current_user
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.models.identity import User, UserRole
|
||||
from app.models.marketplace.organization import Organization
|
||||
from app.models.system.rbac import SystemRole, SystemPermission, SystemRolePermission
|
||||
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
|
||||
|
||||
@@ -72,6 +76,53 @@ class PermissionOverrideResponse(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
# ── RBAC Phase 4: DB-Driven Role & Permission Schemas ──
|
||||
|
||||
|
||||
class SystemRoleOut(BaseModel):
|
||||
"""Output schema for a SystemRole."""
|
||||
id: int
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
rank: int
|
||||
is_system: bool
|
||||
is_active: bool
|
||||
permission_ids: List[int] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class SystemPermissionOut(BaseModel):
|
||||
"""Output schema for a SystemPermission."""
|
||||
id: int
|
||||
code: str
|
||||
domain: str
|
||||
action: str
|
||||
description: Optional[str] = None
|
||||
is_system: bool
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class RolePermissionsUpdateRequest(BaseModel):
|
||||
"""Request body for updating a role's granted permissions."""
|
||||
permission_ids: List[int] = Field(
|
||||
...,
|
||||
description="List of permission IDs that should be granted to this role",
|
||||
)
|
||||
|
||||
|
||||
class RolePermissionsUpdateResponse(BaseModel):
|
||||
"""Response after updating role permissions."""
|
||||
status: str
|
||||
role_id: int
|
||||
role_name: str
|
||||
granted_permission_ids: List[int]
|
||||
message: str
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Endpoints
|
||||
# ──────────────────────────────────────────────
|
||||
@@ -84,30 +135,39 @@ class PermissionOverrideResponse(BaseModel):
|
||||
summary="Get the full RBAC permission matrix",
|
||||
)
|
||||
async def get_permission_matrix(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
_ = Depends(RequireRole([UserRole.SUPERADMIN, UserRole.ADMIN])),
|
||||
_ = Depends(deps.RequirePermission("permissions:view")),
|
||||
):
|
||||
"""
|
||||
🔐 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.
|
||||
Protected by RequirePermission("permissions:view").
|
||||
|
||||
P0 Phase 6: Fully DB-driven. All permission codes are fetched
|
||||
from system.permissions and role mappings from system.role_permissions.
|
||||
|
||||
Returns:
|
||||
- matrix: List of {role, actions[]} entries
|
||||
- all_actions: Complete list of all possible admin actions
|
||||
- all_actions: Complete list of all possible permission codes
|
||||
"""
|
||||
# Fetch all permission codes from the DB
|
||||
all_perms_result = await db.execute(
|
||||
select(SystemPermission.code).order_by(SystemPermission.code)
|
||||
)
|
||||
all_actions = [row[0] for row in all_perms_result.all()]
|
||||
|
||||
# Fetch role permissions for each role
|
||||
matrix = []
|
||||
for role in [UserRole.SUPERADMIN, UserRole.ADMIN, UserRole.MODERATOR,
|
||||
UserRole.SALES_REP, UserRole.SERVICE_MGR]:
|
||||
actions = rbac_service.get_permitted_actions(role)
|
||||
actions = await rbac_service.get_permitted_actions(db, 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),
|
||||
@@ -125,7 +185,7 @@ async def override_org_permission(
|
||||
payload: PermissionOverrideRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
_ = Depends(RequireRole([UserRole.SUPERADMIN, UserRole.ADMIN])),
|
||||
_ = Depends(deps.RequirePermission("permissions:edit")),
|
||||
):
|
||||
"""
|
||||
🔐 Updates custom permissions for a specific organization.
|
||||
@@ -138,6 +198,8 @@ async def override_org_permission(
|
||||
the requesting admin has scope-based access to the target
|
||||
organization before applying the override.
|
||||
|
||||
P0 Phase 6: Action validation is now DB-driven via system.permissions.
|
||||
|
||||
Args:
|
||||
org_id: The target organization ID.
|
||||
payload.action: The action to override (e.g., "EDIT_DATA").
|
||||
@@ -146,9 +208,17 @@ async def override_org_permission(
|
||||
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:
|
||||
# 1. Validate that the action exists in the DB
|
||||
perm_stmt = select(SystemPermission.code).where(
|
||||
SystemPermission.code == payload.action
|
||||
)
|
||||
perm_result = await db.execute(perm_stmt)
|
||||
if not perm_result.scalar_one_or_none():
|
||||
# Fetch all valid codes for the error message
|
||||
all_codes_result = await db.execute(
|
||||
select(SystemPermission.code).order_by(SystemPermission.code)
|
||||
)
|
||||
valid_actions = [row[0] for row in all_codes_result.all()]
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid action '{payload.action}'. Valid actions: {sorted(valid_actions)}",
|
||||
@@ -159,7 +229,7 @@ async def override_org_permission(
|
||||
await rbac_service.check_admin_access(
|
||||
db=db,
|
||||
user=current_user,
|
||||
action=AdminAction.EDIT_DATA,
|
||||
action="permissions:edit",
|
||||
target_org_id=org_id,
|
||||
)
|
||||
except PermissionError as e:
|
||||
@@ -180,17 +250,12 @@ async def override_org_permission(
|
||||
)
|
||||
|
||||
# 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
|
||||
@@ -210,3 +275,222 @@ async def override_org_permission(
|
||||
granted=payload.granted,
|
||||
message=f"Permission '{payload.action}' {'granted' if payload.granted else 'revoked'} for organization {org_id}.",
|
||||
)
|
||||
|
||||
|
||||
# ── RBAC Phase 4: DB-Driven Role & Permission Endpoints ──
|
||||
|
||||
|
||||
@router.get(
|
||||
"/admin/permissions/roles",
|
||||
response_model=List[SystemRoleOut],
|
||||
tags=["Admin Permissions"],
|
||||
summary="Get all system roles with their granted permission IDs",
|
||||
)
|
||||
async def get_all_roles(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
_ = Depends(deps.RequirePermission("permissions:view")),
|
||||
):
|
||||
"""
|
||||
🔐 Returns all SystemRole records with their associated permission IDs.
|
||||
|
||||
Each role includes:
|
||||
- id, name, description, rank, is_system, is_active
|
||||
- permission_ids: List of permission IDs granted to this role
|
||||
|
||||
Protected by RequirePermission("permissions:view").
|
||||
"""
|
||||
stmt = (
|
||||
select(SystemRole)
|
||||
.options(selectinload(SystemRole.permissions))
|
||||
.order_by(SystemRole.rank.desc())
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
roles = result.unique().scalars().all()
|
||||
|
||||
role_list = []
|
||||
for role in roles:
|
||||
permission_ids = [
|
||||
rp.permission_id for rp in role.permissions if rp.granted
|
||||
]
|
||||
role_list.append(SystemRoleOut(
|
||||
id=role.id,
|
||||
name=role.name,
|
||||
description=role.description,
|
||||
rank=role.rank,
|
||||
is_system=role.is_system,
|
||||
is_active=role.is_active,
|
||||
permission_ids=permission_ids,
|
||||
))
|
||||
|
||||
return role_list
|
||||
|
||||
|
||||
@router.get(
|
||||
"/admin/permissions",
|
||||
response_model=List[SystemPermissionOut],
|
||||
tags=["Admin Permissions"],
|
||||
summary="Get all system permissions",
|
||||
)
|
||||
async def get_all_permissions(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
_ = Depends(deps.RequirePermission("permissions:view")),
|
||||
):
|
||||
"""
|
||||
🔐 Returns all SystemPermission records.
|
||||
|
||||
Each permission includes:
|
||||
- id, code, domain, action, description, is_system
|
||||
|
||||
Protected by RequirePermission("permissions:view").
|
||||
"""
|
||||
stmt = select(SystemPermission).order_by(SystemPermission.domain, SystemPermission.action)
|
||||
result = await db.execute(stmt)
|
||||
permissions = result.scalars().all()
|
||||
|
||||
return [
|
||||
SystemPermissionOut(
|
||||
id=p.id,
|
||||
code=p.code,
|
||||
domain=p.domain,
|
||||
action=p.action,
|
||||
description=p.description,
|
||||
is_system=p.is_system,
|
||||
)
|
||||
for p in permissions
|
||||
]
|
||||
|
||||
|
||||
@router.put(
|
||||
"/admin/permissions/roles/{role_id}/permissions",
|
||||
response_model=RolePermissionsUpdateResponse,
|
||||
tags=["Admin Permissions"],
|
||||
summary="Update granted permissions for a role",
|
||||
)
|
||||
async def update_role_permissions(
|
||||
role_id: int,
|
||||
payload: RolePermissionsUpdateRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
_ = Depends(deps.RequirePermission("permissions:edit")),
|
||||
):
|
||||
"""
|
||||
🔐 Updates the granted permissions for a specific role.
|
||||
|
||||
This endpoint performs a full replacement of the role's granted
|
||||
permissions. The provided permission_ids list becomes the new set
|
||||
of granted permissions. Any existing mappings not in the list
|
||||
are removed.
|
||||
|
||||
SUPERADMIN roles (is_system=True, rank=100) are protected:
|
||||
- Their permissions cannot be modified through this endpoint
|
||||
to prevent accidental lockout.
|
||||
|
||||
Args:
|
||||
role_id: The target role ID.
|
||||
payload.permission_ids: The complete list of permission IDs to grant.
|
||||
|
||||
Returns:
|
||||
Confirmation with the updated permission IDs.
|
||||
"""
|
||||
# 1. Fetch the role
|
||||
stmt = select(SystemRole).where(SystemRole.id == role_id)
|
||||
result = await db.execute(stmt)
|
||||
role = result.scalar_one_or_none()
|
||||
|
||||
if not role:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Role with id={role_id} not found.",
|
||||
)
|
||||
|
||||
# 2. Protect SUPERADMIN system roles
|
||||
if role.is_system and role.rank >= 100:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Cannot modify permissions for the SUPERADMIN system role. "
|
||||
"This would risk locking administrators out of the system.",
|
||||
)
|
||||
|
||||
# 3. Validate that all permission_ids exist
|
||||
if payload.permission_ids:
|
||||
perm_stmt = select(SystemPermission.id).where(
|
||||
SystemPermission.id.in_(payload.permission_ids)
|
||||
)
|
||||
perm_result = await db.execute(perm_stmt)
|
||||
existing_ids = {row[0] for row in perm_result.all()}
|
||||
|
||||
missing_ids = set(payload.permission_ids) - existing_ids
|
||||
if missing_ids:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid permission IDs: {sorted(missing_ids)}. These permissions do not exist.",
|
||||
)
|
||||
|
||||
# 4. Remove existing role-permission mappings
|
||||
delete_stmt = delete(SystemRolePermission).where(
|
||||
SystemRolePermission.role_id == role_id
|
||||
)
|
||||
await db.execute(delete_stmt)
|
||||
|
||||
# 5. Insert new mappings
|
||||
for perm_id in payload.permission_ids:
|
||||
db.add(SystemRolePermission(
|
||||
role_id=role_id,
|
||||
permission_id=perm_id,
|
||||
granted=True,
|
||||
))
|
||||
|
||||
await db.commit()
|
||||
|
||||
# 6. Invalidate cache
|
||||
rbac_service.invalidate_role_cache(role_id)
|
||||
|
||||
logger.info(
|
||||
f"Role permissions updated: role_id={role_id}, "
|
||||
f"permissions={payload.permission_ids}, "
|
||||
f"by_user={current_user.id}"
|
||||
)
|
||||
|
||||
return RolePermissionsUpdateResponse(
|
||||
status="success",
|
||||
role_id=role_id,
|
||||
role_name=role.name,
|
||||
granted_permission_ids=payload.permission_ids,
|
||||
message=f"Permissions updated for role '{role.name}' ({len(payload.permission_ids)} granted).",
|
||||
)
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# RBAC Phase 2: Test Endpoint (Verification)
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get(
|
||||
"/admin/rbac-test",
|
||||
tags=["Admin Permissions (RBAC)"],
|
||||
summary="RBAC Phase 2 verification endpoint",
|
||||
)
|
||||
async def rbac_test_endpoint(
|
||||
current_user: User = Depends(get_current_user),
|
||||
_ = Depends(deps.RequirePermission("fleet:view")),
|
||||
):
|
||||
"""
|
||||
🔐 RBAC Phase 2 verification endpoint.
|
||||
|
||||
Protected by Depends(RequirePermission("fleet:view")).
|
||||
- SUPERADMIN: Always passes (bypass).
|
||||
- Users with fleet:view permission: Passes.
|
||||
- Users without fleet:view permission: 403 Forbidden.
|
||||
|
||||
Returns the current user info if permission is granted.
|
||||
"""
|
||||
return {
|
||||
"status": "ok",
|
||||
"message": "RBAC Phase 2 permission check passed.",
|
||||
"user_id": current_user.id,
|
||||
"user_role": current_user.role.value,
|
||||
"role_id": current_user.role_id,
|
||||
"permission_checked": "fleet:view",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user