Files
pvemanager/backend/app/rbac/middleware.py
Dilshod Rasulov d0badd0a2e refactor(rbac): drop runtime legacy-format resolution from engine
Roles are stored in the new resource:action format and any legacy keys are
converted once at startup by the RBAC v2 DB migration (migrate_rbac_v2), so
the permission engine no longer needs to resolve legacy codes on every check.

- get_user_permissions: store role permission codes as-is (no dual add).
- has_permission: match new-format codes only; drop resolve_permission and the
  dotted-code fallbacks.
- Remove now-unused resolve_permission import in middleware.
- LEGACY_PERMISSION_MAP / convert_* are retained as one-shot DB-migration
  utilities (upgrade path), not a runtime bridge.
- Tests: adapt fixtures to new-format role permissions; assert legacy keys are
  no longer honored at request time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 02:08:19 +05:00

231 lines
7.7 KiB
Python

"""
Authorization Middleware - FastAPI dependencies for permission checks
Replaces old PermissionChecker with enhanced functionality
"""
from typing import Optional, List, Union
from fastapi import Depends, HTTPException, status, Request
from sqlalchemy.orm import Session
from .engine import PermissionEngine
from .permissions import ScopeType
class PermissionChecker:
"""
FastAPI dependency for checking permissions.
Usage:
@router.get("/vms")
async def list_vms(user = Depends(PermissionChecker("vm:view"))):
...
# Multiple permissions (any)
@router.post("/vms")
async def create_vm(user = Depends(PermissionChecker(["vm:create", "lxc:create"]))):
...
"""
def __init__(
self,
permission: Union[str, List[str]],
require_all: bool = False,
scope: ScopeType = ScopeType.GLOBAL
):
"""
Args:
permission: Permission code or list of codes
require_all: If True, requires all permissions (AND), else any (OR)
scope: Permission scope level
"""
if isinstance(permission, str):
self.permissions = [permission]
else:
self.permissions = permission
self.require_all = require_all
self.scope = scope
def __call__(self, request: Request, db: Session = None):
"""Check permissions for current user"""
# Import here to avoid circular imports
from ..auth import get_current_user
from ..db import get_db
# Get user from request state if available
user = getattr(request.state, 'user', None)
if user is None:
# Fall back to standard auth
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from ..auth import decode_access_token
from ..models import User
from sqlalchemy.orm import joinedload
security = HTTPBearer()
# Get token from header
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
token = auth_header.split(" ")[1]
payload = decode_access_token(token)
username = payload.get("sub")
if db is None:
# Get DB session
from ..db import SessionLocal
db = SessionLocal()
try:
user = db.query(User).options(joinedload(User.role)).filter(
User.username == username
).first()
finally:
db.close()
else:
user = db.query(User).options(joinedload(User.role)).filter(
User.username == username
).first()
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found",
)
# Check permissions
if self.require_all:
# Must have all permissions
for perm in self.permissions:
if not PermissionEngine.has_permission(user, perm, self.scope):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Permission denied: {perm}"
)
else:
# Must have at least one permission
has_any = False
for perm in self.permissions:
if PermissionEngine.has_permission(user, perm, self.scope):
has_any = True
break
if not has_any:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Permission denied: requires one of {self.permissions}"
)
return user
class ScopedPermissionChecker:
"""
Permission checker with resource-level scope.
Usage:
@router.get("/vms/{vm_id}")
async def get_vm(
vm_id: int,
user = Depends(ScopedPermissionChecker("vm:view", resource_param="vm_id"))
):
...
"""
def __init__(
self,
permission: str,
scope: ScopeType = ScopeType.INSTANCE,
resource_param: str = "resource_id",
organization_param: Optional[str] = None,
workspace_param: Optional[str] = None
):
self.permission = permission
self.scope = scope
self.resource_param = resource_param
self.organization_param = organization_param
self.workspace_param = workspace_param
def __call__(self, request: Request):
"""Check scoped permission"""
from ..auth import get_current_user
from ..db import SessionLocal
from ..models import User
from sqlalchemy.orm import joinedload
# Get resource ID from path params
resource_id = request.path_params.get(self.resource_param)
organization_id = request.path_params.get(self.organization_param) if self.organization_param else None
workspace_id = request.path_params.get(self.workspace_param) if self.workspace_param else None
# Get user
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
)
token = auth_header.split(" ")[1]
from ..auth import decode_access_token
payload = decode_access_token(token)
username = payload.get("sub")
db = SessionLocal()
try:
user = db.query(User).options(joinedload(User.role)).filter(
User.username == username
).first()
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found",
)
# Check permission with scope
if not PermissionEngine.has_permission(
user,
self.permission,
self.scope,
resource_id=int(resource_id) if resource_id else None,
organization_id=int(organization_id) if organization_id else None,
workspace_id=int(workspace_id) if workspace_id else None
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Permission denied: {self.permission}"
)
return user
finally:
db.close()
class ResourceAccessChecker:
"""
Check access to specific resource based on ownership/membership.
Future use for organization/workspace scoping.
"""
def __init__(
self,
resource_type: str,
permission: str,
id_param: str = "id"
):
self.resource_type = resource_type
self.permission = permission
self.id_param = id_param
def __call__(self, request: Request):
"""Check resource access"""
# Placeholder for future organization/workspace checks
# For now, just check permission
checker = PermissionChecker(self.permission)
return checker(request)