RBAC, Permissions, and Export/Import

This commit is contained in:
Koncept Kit
2025-12-16 20:03:50 +07:00
parent b268c3fff8
commit ed5526e27b
27 changed files with 10284 additions and 73 deletions

131
auth.py
View File

@@ -1,5 +1,5 @@
from datetime import datetime, timedelta, timezone
from typing import Optional
from typing import Optional, List
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session
import os
import secrets
from database import get_db
from models import User, UserRole
from models import User, UserRole, Permission, RolePermission, Role
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
security = HTTPBearer()
@@ -50,6 +50,24 @@ def verify_reset_token(token, db):
return user
def get_user_role_code(user: User) -> str:
"""
Get user's role code from either dynamic role system or legacy enum.
Supports backward compatibility during migration (Phase 3).
Args:
user: User object
Returns:
Role code string (e.g., "superadmin", "admin", "member", "guest")
"""
# Prefer dynamic role if set (Phase 3+)
if user.role_id is not None and user.role_obj is not None:
return user.role_obj.code
# Fallback to legacy enum (Phase 1-2)
return user.role.value
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
@@ -100,7 +118,9 @@ async def get_current_user(
return user
async def get_current_admin_user(current_user: User = Depends(get_current_user)) -> User:
if current_user.role != UserRole.admin:
"""Require user to be admin or superadmin"""
role_code = get_user_role_code(current_user)
if role_code not in ["admin", "superadmin"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
@@ -117,10 +137,113 @@ async def get_active_member(current_user: User = Depends(get_current_user)) -> U
detail="Active membership required. Please complete payment."
)
if current_user.role not in [UserRole.member, UserRole.admin]:
role_code = get_user_role_code(current_user)
if role_code not in ["member", "admin", "superadmin"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Member access only"
)
return current_user
# ============================================================
# RBAC Permission System
# ============================================================
async def get_user_permissions(user: User, db: Session) -> List[str]:
"""
Get all permission codes for user's role.
Superadmin automatically gets all permissions.
Uses request-level caching to avoid repeated DB queries.
Supports both dynamic roles (role_id) and legacy enum (role).
Args:
user: Current authenticated user
db: Database session
Returns:
List of permission code strings (e.g., ["users.view", "events.create"])
"""
# Check if permissions are already cached for this request
if hasattr(user, '_permission_cache'):
return user._permission_cache
# Get role code using helper
role_code = get_user_role_code(user)
# Superadmin gets all permissions automatically
if role_code == "superadmin":
all_perms = db.query(Permission.code).all()
permissions = [p[0] for p in all_perms]
else:
# Fetch permissions assigned to this role
# Prefer dynamic role_id, fallback to enum
if user.role_id is not None:
# Use role_id for dynamic roles
permissions = db.query(Permission.code)\
.join(RolePermission)\
.filter(RolePermission.role_id == user.role_id)\
.all()
else:
# Fallback to legacy enum
permissions = db.query(Permission.code)\
.join(RolePermission)\
.filter(RolePermission.role == user.role)\
.all()
permissions = [p[0] for p in permissions]
# Cache permissions on user object for this request
user._permission_cache = permissions
return permissions
def require_permission(permission_code: str):
"""
Dependency injection for permission-based access control.
Usage:
@app.get("/admin/users", dependencies=[Depends(require_permission("users.view"))])
async def get_users():
...
Args:
permission_code: Permission code to check (e.g., "users.create")
Returns:
Async function that checks if current user has the permission
Raises:
HTTPException 403 if user lacks the required permission
"""
async def permission_checker(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
) -> User:
# Get user's permissions
user_perms = await get_user_permissions(current_user, db)
# Check if user has the required permission
if permission_code not in user_perms:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Permission required: {permission_code}"
)
return current_user
return permission_checker
async def get_current_superadmin(current_user: User = Depends(get_current_user)) -> User:
"""
Require user to be superadmin.
Used for endpoints that should only be accessible to superadmins.
"""
role_code = get_user_role_code(current_user)
if role_code != "superadmin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Superadmin access required"
)
return current_user