""" RBAC Middleware for Session-Based Authentication Provides user type checking (Employee vs. Customer) and permission-based access control. Employee roles: Teacher-Rollen, Admin-Rollen Customer roles: parent, student, user Usage: @app.get("/api/protected/employee/grades") async def get_grades(session: Session = Depends(require_employee)): return {"grades": [...]} @app.get("/api/protected/endpoint") async def protected(session: Session = Depends(require_permission("grades:read"))): return {"data": [...]} """ import logging from typing import List, Callable from functools import wraps from fastapi import HTTPException, Depends from .session_store import Session, UserType from .session_middleware import get_current_session logger = logging.getLogger(__name__) # ============================================= # Permission Constants # ============================================= EMPLOYEE_PERMISSIONS = [ # Grades & Attendance "grades:read", "grades:write", "attendance:read", "attendance:write", # Student Management "students:read", "students:write", # Reports & Consent "reports:generate", "consent:admin", # Corrections "corrections:read", "corrections:write", # Classes "classes:read", "classes:write", # Timetable "timetable:read", "timetable:write", # Substitutions "substitutions:read", "substitutions:write", # Parent Communication "parent_communication:read", "parent_communication:write", ] CUSTOMER_PERMISSIONS = [ # Own Data Access "own_data:read", "own_grades:read", "own_attendance:read", # Consent Management "consent:manage", # Meetings & Communication "meetings:join", "messages:read", "messages:write", # Children (for parents) "children:read", "children:grades:read", "children:attendance:read", ] ADMIN_PERMISSIONS = [ # User Management "users:read", "users:write", "users:manage", # RBAC Management "rbac:read", "rbac:write", # Audit & Logs "audit:read", # System Settings "settings:read", "settings:write", # DSR Management "dsr:read", "dsr:process", ] # Roles that indicate employee user type EMPLOYEE_ROLES = { "admin", "schul_admin", "schulleitung", "pruefungsvorsitz", "klassenlehrer", "fachlehrer", "sekretariat", "erstkorrektor", "zweitkorrektor", "drittkorrektor", "teacher_assistant", "teacher", "lehrer", "data_protection_officer", } # Roles that indicate customer user type CUSTOMER_ROLES = { "parent", "student", "user", "guardian", } # ============================================= # User Type Dependencies # ============================================= async def require_employee(session: Session = Depends(get_current_session)) -> Session: """ Require user to be an employee (internal staff). Usage: @app.get("/api/protected/employee/grades") async def employee_only(session: Session = Depends(require_employee)): return {"grades": [...]} """ if not session.is_employee(): raise HTTPException( status_code=403, detail="Employee access required" ) return session async def require_customer(session: Session = Depends(get_current_session)) -> Session: """ Require user to be a customer (external user). Usage: @app.get("/api/protected/customer/my-grades") async def customer_only(session: Session = Depends(require_customer)): return {"my_grades": [...]} """ if not session.is_customer(): raise HTTPException( status_code=403, detail="Customer access required" ) return session def require_user_type(user_type: UserType): """ Factory for user type dependency. Usage: @app.get("/api/protected/endpoint") async def endpoint(session: Session = Depends(require_user_type(UserType.EMPLOYEE))): return {"data": [...]} """ async def user_type_checker(session: Session = Depends(get_current_session)) -> Session: if session.user_type != user_type: raise HTTPException( status_code=403, detail=f"User type '{user_type.value}' required" ) return session return user_type_checker # ============================================= # Permission Dependencies # ============================================= def require_permission(permission: str): """ Factory for permission-based access control. Usage: @app.get("/api/protected/grades") async def get_grades(session: Session = Depends(require_permission("grades:read"))): return {"grades": [...]} """ async def permission_checker(session: Session = Depends(get_current_session)) -> Session: if not session.has_permission(permission): logger.warning( f"Permission denied: user {session.user_id} lacks '{permission}'" ) raise HTTPException( status_code=403, detail=f"Permission '{permission}' required" ) return session return permission_checker def require_any_permission(permissions: List[str]): """ Require user to have at least one of the specified permissions. Usage: @app.get("/api/protected/data") async def get_data( session: Session = Depends(require_any_permission(["data:read", "admin"])) ): return {"data": [...]} """ async def any_permission_checker(session: Session = Depends(get_current_session)) -> Session: if not session.has_any_permission(permissions): raise HTTPException( status_code=403, detail=f"One of permissions {permissions} required" ) return session return any_permission_checker def require_all_permissions(permissions: List[str]): """ Require user to have all specified permissions. Usage: @app.get("/api/protected/sensitive") async def sensitive( session: Session = Depends(require_all_permissions(["grades:read", "grades:write"])) ): return {"data": [...]} """ async def all_permissions_checker(session: Session = Depends(get_current_session)) -> Session: if not session.has_all_permissions(permissions): missing = [p for p in permissions if not session.has_permission(p)] raise HTTPException( status_code=403, detail=f"Missing permissions: {missing}" ) return session return all_permissions_checker def require_role(role: str): """ Factory for role-based access control. Usage: @app.get("/api/admin/users") async def admin_users(session: Session = Depends(require_role("admin"))): return {"users": [...]} """ async def role_checker(session: Session = Depends(get_current_session)) -> Session: if not session.has_role(role): raise HTTPException( status_code=403, detail=f"Role '{role}' required" ) return session return role_checker def require_any_role(roles: List[str]): """ Require user to have at least one of the specified roles. Usage: @app.get("/api/management/data") async def management( session: Session = Depends(require_any_role(["admin", "schulleitung"])) ): return {"data": [...]} """ async def any_role_checker(session: Session = Depends(get_current_session)) -> Session: if not any(session.has_role(role) for role in roles): raise HTTPException( status_code=403, detail=f"One of roles {roles} required" ) return session return any_role_checker # ============================================= # Tenant Isolation # ============================================= def require_same_tenant(tenant_id_param: str = "tenant_id"): """ Ensure user can only access data within their tenant. Usage: @app.get("/api/protected/school/{tenant_id}/data") async def school_data( tenant_id: str, session: Session = Depends(require_same_tenant("tenant_id")) ): return {"data": [...]} """ async def tenant_checker( session: Session = Depends(get_current_session), **kwargs ) -> Session: request_tenant = kwargs.get(tenant_id_param) if request_tenant and session.tenant_id != request_tenant: # Check if user is super admin (can access all tenants) if not session.has_role("super_admin"): raise HTTPException( status_code=403, detail="Access denied to this tenant" ) return session return tenant_checker # ============================================= # Utility Functions # ============================================= def determine_user_type(roles: List[str]) -> UserType: """ Determine user type based on roles. Employee roles take precedence over customer roles. """ role_set = set(roles) # Check for employee roles if role_set & EMPLOYEE_ROLES: return UserType.EMPLOYEE # Check for customer roles if role_set & CUSTOMER_ROLES: return UserType.CUSTOMER # Default to customer return UserType.CUSTOMER def get_permissions_for_roles(roles: List[str], user_type: UserType) -> List[str]: """ Get permissions based on roles and user type. This is a basic implementation - in production, you'd query the RBAC database for role-permission mappings. """ permissions = set() # Base permissions based on user type if user_type == UserType.EMPLOYEE: permissions.update(EMPLOYEE_PERMISSIONS) else: permissions.update(CUSTOMER_PERMISSIONS) # Admin permissions role_set = set(roles) admin_roles = {"admin", "schul_admin", "super_admin", "data_protection_officer"} if role_set & admin_roles: permissions.update(ADMIN_PERMISSIONS) return list(permissions) def check_resource_ownership( session: Session, resource_user_id: str, allow_admin: bool = True ) -> bool: """ Check if user owns a resource or is admin. Usage: if not check_resource_ownership(session, grade.student_id): raise HTTPException(403, "Access denied") """ # User owns the resource if session.user_id == resource_user_id: return True # Admin can access all if allow_admin and (session.has_role("admin") or session.has_role("super_admin")): return True return False def check_parent_child_access( session: Session, student_id: str, parent_student_ids: List[str] ) -> bool: """ Check if parent has access to student's data. Usage: parent_children = get_parent_children(session.user_id) if not check_parent_child_access(session, student_id, parent_children): raise HTTPException(403, "Access denied") """ # User is the student if session.user_id == student_id: return True # User is parent of student if student_id in parent_student_ids: return True # Employee can access (with appropriate permissions) if session.is_employee() and session.has_permission("students:read"): return True return False