fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
428
backend/session/rbac_middleware.py
Normal file
428
backend/session/rbac_middleware.py
Normal file
@@ -0,0 +1,428 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user