RBAC, Permissions, and Export/Import
This commit is contained in:
131
auth.py
131
auth.py
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user