manage-your-printer/utils/permissions.py
2025-06-04 10:03:22 +02:00

637 lines
22 KiB
Python

#!/usr/bin/env python3
"""
Erweiterte Berechtigungsverwaltung für MYP Platform
Granulare Rollen und Permissions für feingranulare Zugriffskontrolle
"""
from enum import Enum
from functools import wraps
from typing import List, Dict, Set, Optional
from flask import request, jsonify, abort
from flask_login import login_required, current_user
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, Table, DateTime, MetaData
from sqlalchemy.orm import relationship
from datetime import datetime, timedelta
from utils.logging_config import get_logger
logger = get_logger("permissions")
# ===== PERMISSION DEFINITIONS =====
class Permission(Enum):
"""Alle verfügbaren Berechtigungen im System"""
# Basis-Berechtigungen
LOGIN = "login"
VIEW_DASHBOARD = "view_dashboard"
# Drucker-Berechtigungen
VIEW_PRINTERS = "view_printers"
CREATE_PRINTER = "create_printer"
EDIT_PRINTER = "edit_printer"
DELETE_PRINTER = "delete_printer"
CONTROL_PRINTER = "control_printer" # Ein-/Ausschalten
VIEW_PRINTER_DETAILS = "view_printer_details"
# Job-Berechtigungen
VIEW_JOBS = "view_jobs"
CREATE_JOB = "create_job"
EDIT_OWN_JOB = "edit_own_job"
EDIT_ALL_JOBS = "edit_all_jobs"
DELETE_OWN_JOB = "delete_own_job"
DELETE_ALL_JOBS = "delete_all_jobs"
EXTEND_JOB = "extend_job"
CANCEL_JOB = "cancel_job"
VIEW_JOB_HISTORY = "view_job_history"
APPROVE_JOBS = "approve_jobs" # Berechtigung zum Genehmigen und Verwalten von Jobs
# Benutzer-Berechtigungen
VIEW_USERS = "view_users"
CREATE_USER = "create_user"
EDIT_USER = "edit_user"
DELETE_USER = "delete_user"
MANAGE_ROLES = "manage_roles"
VIEW_USER_DETAILS = "view_user_details"
# Admin-Berechtigungen
VIEW_ADMIN_PANEL = "view_admin_panel"
MANAGE_SYSTEM = "manage_system"
VIEW_LOGS = "view_logs"
EXPORT_DATA = "export_data"
BACKUP_DATABASE = "backup_database"
MANAGE_SETTINGS = "manage_settings"
ADMIN = "admin" # Allgemeine Admin-Berechtigung für administrative Funktionen
# Gast-Berechtigungen
VIEW_GUEST_REQUESTS = "view_guest_requests"
CREATE_GUEST_REQUEST = "create_guest_request"
APPROVE_GUEST_REQUEST = "approve_guest_request"
DENY_GUEST_REQUEST = "deny_guest_request"
MANAGE_GUEST_REQUESTS = "manage_guest_requests"
# Statistik-Berechtigungen
VIEW_STATS = "view_stats"
VIEW_DETAILED_STATS = "view_detailed_stats"
EXPORT_STATS = "export_stats"
# Kalender-Berechtigungen
VIEW_CALENDAR = "view_calendar"
EDIT_CALENDAR = "edit_calendar"
MANAGE_SHIFTS = "manage_shifts"
# Wartung-Berechtigungen
SCHEDULE_MAINTENANCE = "schedule_maintenance"
VIEW_MAINTENANCE = "view_maintenance"
PERFORM_MAINTENANCE = "perform_maintenance"
class Role(Enum):
"""Vordefinierte Rollen mit Standard-Berechtigungen"""
GUEST = "guest"
USER = "user"
POWER_USER = "power_user"
TECHNICIAN = "technician"
SUPERVISOR = "supervisor"
ADMIN = "admin"
SUPER_ADMIN = "super_admin"
# ===== ROLE PERMISSIONS MAPPING =====
ROLE_PERMISSIONS = {
Role.GUEST: {
Permission.LOGIN,
Permission.VIEW_PRINTERS,
Permission.CREATE_GUEST_REQUEST,
Permission.VIEW_CALENDAR,
},
Role.USER: {
Permission.LOGIN,
Permission.VIEW_DASHBOARD,
Permission.VIEW_PRINTERS,
Permission.VIEW_JOBS,
Permission.CREATE_JOB,
Permission.EDIT_OWN_JOB,
Permission.DELETE_OWN_JOB,
Permission.EXTEND_JOB,
Permission.CANCEL_JOB,
Permission.VIEW_STATS,
Permission.VIEW_CALENDAR,
Permission.CREATE_GUEST_REQUEST,
},
}
# Power User erweitert User-Permissions
ROLE_PERMISSIONS[Role.POWER_USER] = ROLE_PERMISSIONS[Role.USER] | {
Permission.VIEW_PRINTER_DETAILS,
Permission.VIEW_JOB_HISTORY,
Permission.VIEW_DETAILED_STATS,
Permission.EXPORT_STATS,
Permission.VIEW_GUEST_REQUESTS,
}
# Technician erweitert Power User-Permissions
ROLE_PERMISSIONS[Role.TECHNICIAN] = ROLE_PERMISSIONS[Role.POWER_USER] | {
Permission.CONTROL_PRINTER,
Permission.EDIT_PRINTER,
Permission.SCHEDULE_MAINTENANCE,
Permission.VIEW_MAINTENANCE,
Permission.PERFORM_MAINTENANCE,
Permission.EDIT_CALENDAR,
}
# Supervisor erweitert Technician-Permissions
ROLE_PERMISSIONS[Role.SUPERVISOR] = ROLE_PERMISSIONS[Role.TECHNICIAN] | {
Permission.CREATE_PRINTER,
Permission.EDIT_ALL_JOBS,
Permission.DELETE_ALL_JOBS,
Permission.VIEW_USERS,
Permission.APPROVE_GUEST_REQUEST,
Permission.DENY_GUEST_REQUEST,
Permission.MANAGE_GUEST_REQUESTS,
Permission.MANAGE_SHIFTS,
Permission.VIEW_USER_DETAILS,
Permission.APPROVE_JOBS, # Jobs genehmigen und verwalten
}
# Admin erweitert Supervisor-Permissions
ROLE_PERMISSIONS[Role.ADMIN] = ROLE_PERMISSIONS[Role.SUPERVISOR] | {
Permission.DELETE_PRINTER,
Permission.VIEW_ADMIN_PANEL,
Permission.CREATE_USER,
Permission.EDIT_USER,
Permission.DELETE_USER,
Permission.EXPORT_DATA,
Permission.VIEW_LOGS,
Permission.MANAGE_SETTINGS,
Permission.ADMIN, # Allgemeine Admin-Berechtigung hinzufügen
}
# Super Admin hat alle Berechtigungen
ROLE_PERMISSIONS[Role.SUPER_ADMIN] = {perm for perm in Permission}
# ===== DATABASE MODELS EXTENSIONS =====
# Metadata für die Tabellen erstellen
metadata = MetaData()
# Many-to-Many Tabelle für User-Permissions
user_permissions = Table('user_permissions', metadata,
Column('user_id', Integer, ForeignKey('users.id'), primary_key=True),
Column('permission_id', Integer, ForeignKey('permissions.id'), primary_key=True)
)
# Many-to-Many Tabelle für User-Roles
user_roles = Table('user_roles', metadata,
Column('user_id', Integer, ForeignKey('users.id'), primary_key=True),
Column('role_id', Integer, ForeignKey('roles.id'), primary_key=True)
)
class PermissionModel:
"""Datenbank-Model für Berechtigungen"""
__tablename__ = 'permissions'
id = Column(Integer, primary_key=True)
name = Column(String(100), unique=True, nullable=False)
description = Column(String(255))
category = Column(String(50)) # Gruppierung von Berechtigungen
created_at = Column(DateTime, default=datetime.now)
class RoleModel:
"""Datenbank-Model für Rollen"""
__tablename__ = 'roles'
id = Column(Integer, primary_key=True)
name = Column(String(50), unique=True, nullable=False)
display_name = Column(String(100))
description = Column(String(255))
is_system_role = Column(Boolean, default=False) # System-Rollen können nicht gelöscht werden
created_at = Column(DateTime, default=datetime.now)
# Relationships
permissions = relationship("PermissionModel", secondary="role_permissions", back_populates="roles")
class UserPermissionOverride:
"""Temporäre oder spezielle Berechtigungsüberschreibungen"""
__tablename__ = 'user_permission_overrides'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
permission = Column(String(100), nullable=False)
granted = Column(Boolean, nullable=False) # True = gewährt, False = verweigert
reason = Column(String(255))
granted_by = Column(Integer, ForeignKey('users.id'))
expires_at = Column(DateTime, nullable=True) # NULL = permanent
created_at = Column(DateTime, default=datetime.now)
# ===== PERMISSION CHECKER CLASS =====
class PermissionChecker:
"""Hauptklasse für Berechtigungsprüfungen"""
def __init__(self, user=None):
self.user = user or current_user
self._permission_cache = {}
self._cache_timeout = timedelta(minutes=5)
self._cache_timestamp = None
def has_permission(self, permission: Permission) -> bool:
"""
Prüft ob der Benutzer eine bestimmte Berechtigung hat
Args:
permission: Die zu prüfende Berechtigung
Returns:
bool: True wenn Berechtigung vorhanden
"""
if not self.user or not self.user.is_authenticated:
return False
# Cache prüfen
if self._is_cache_valid() and permission.value in self._permission_cache:
return self._permission_cache[permission.value]
# Berechtigungen neu berechnen
has_perm = self._calculate_permission(permission)
# Cache aktualisieren
self._update_cache(permission.value, has_perm)
return has_perm
def _calculate_permission(self, permission: Permission) -> bool:
"""Berechnet ob eine Berechtigung vorhanden ist"""
# Super Admin hat alle Rechte
if hasattr(self.user, 'is_super_admin') and self.user.is_super_admin:
return True
# Explizite Überschreibungen prüfen
override = self._check_permission_override(permission)
if override is not None:
return override
# Rollen-basierte Berechtigungen prüfen
user_roles = self._get_user_roles()
for role in user_roles:
if permission in ROLE_PERMISSIONS.get(role, set()):
return True
# Direkte Benutzer-Berechtigungen prüfen
if hasattr(self.user, 'permissions'):
user_permissions = [Permission(p.name) for p in self.user.permissions if hasattr(Permission, p.name.upper())]
if permission in user_permissions:
return True
return False
def _check_permission_override(self, permission: Permission) -> Optional[bool]:
"""Prüft ob es eine Berechtigungsüberschreibung gibt"""
if not hasattr(self.user, 'permission_overrides'):
return None
now = datetime.now()
for override in self.user.permission_overrides:
if (override.permission == permission.value and
(override.expires_at is None or override.expires_at > now)):
logger.info(f"Permission override angewendet: {permission.value} = {override.granted} für User {self.user.id}")
return override.granted
return None
def _get_user_roles(self) -> List[Role]:
"""Holt die Rollen des Benutzers"""
roles = []
# Legacy Admin-Check
if hasattr(self.user, 'is_admin') and self.user.is_admin:
roles.append(Role.ADMIN)
# Neue Rollen-System
if hasattr(self.user, 'roles'):
for role_model in self.user.roles:
try:
role = Role(role_model.name)
roles.append(role)
except ValueError:
logger.warning(f"Unbekannte Rolle: {role_model.name}")
# Standard-Rolle wenn keine andere definiert
if not roles:
roles.append(Role.USER)
return roles
def _is_cache_valid(self) -> bool:
"""Prüft ob der Permission-Cache noch gültig ist"""
if self._cache_timestamp is None:
return False
return datetime.now() - self._cache_timestamp < self._cache_timeout
def _update_cache(self, permission: str, has_permission: bool):
"""Aktualisiert den Permission-Cache"""
if self._cache_timestamp is None or not self._is_cache_valid():
self._permission_cache = {}
self._cache_timestamp = datetime.now()
self._permission_cache[permission] = has_permission
def get_all_permissions(self) -> Set[Permission]:
"""Gibt alle Berechtigungen des Benutzers zurück"""
permissions = set()
for permission in Permission:
if self.has_permission(permission):
permissions.add(permission)
return permissions
def can_access_resource(self, resource_type: str, resource_id: int = None, action: str = "view") -> bool:
"""
Prüft Zugriff auf spezifische Ressourcen
Args:
resource_type: Art der Ressource (job, printer, user, etc.)
resource_id: ID der Ressource (optional)
action: Aktion (view, edit, delete, etc.)
Returns:
bool: True wenn Zugriff erlaubt
"""
# Resource-spezifische Logik
if resource_type == "job":
return self._check_job_access(resource_id, action)
elif resource_type == "printer":
return self._check_printer_access(resource_id, action)
elif resource_type == "user":
return self._check_user_access(resource_id, action)
return False
def _check_job_access(self, job_id: int, action: str) -> bool:
"""Prüft Job-spezifische Zugriffsrechte"""
if action == "view":
if self.has_permission(Permission.VIEW_JOBS):
return True
elif action == "edit":
if self.has_permission(Permission.EDIT_ALL_JOBS):
return True
if self.has_permission(Permission.EDIT_OWN_JOB) and job_id:
# Prüfen ob eigener Job (vereinfacht)
return self._is_own_job(job_id)
elif action == "delete":
if self.has_permission(Permission.DELETE_ALL_JOBS):
return True
if self.has_permission(Permission.DELETE_OWN_JOB) and job_id:
return self._is_own_job(job_id)
return False
def _check_printer_access(self, printer_id: int, action: str) -> bool:
"""Prüft Drucker-spezifische Zugriffsrechte"""
if action == "view":
return self.has_permission(Permission.VIEW_PRINTERS)
elif action == "edit":
return self.has_permission(Permission.EDIT_PRINTER)
elif action == "delete":
return self.has_permission(Permission.DELETE_PRINTER)
elif action == "control":
return self.has_permission(Permission.CONTROL_PRINTER)
return False
def _check_user_access(self, user_id: int, action: str) -> bool:
"""Prüft Benutzer-spezifische Zugriffsrechte"""
if action == "view":
if self.has_permission(Permission.VIEW_USERS):
return True
# Eigenes Profil ansehen
if user_id == self.user.id:
return True
elif action == "edit":
if self.has_permission(Permission.EDIT_USER):
return True
# Eigenes Profil bearbeiten (begrenzt)
if user_id == self.user.id:
return True
elif action == "delete":
if self.has_permission(Permission.DELETE_USER) and user_id != self.user.id:
return True
return False
def _is_own_job(self, job_id: int) -> bool:
"""Hilfsfunktion um zu prüfen ob Job dem Benutzer gehört"""
# Vereinfachte Implementierung - sollte mit DB-Query implementiert werden
try:
from models import Job, get_db_session
db_session = get_db_session()
job = db_session.query(Job).filter(Job.id == job_id).first()
db_session.close()
return job and (job.user_id == self.user.id or job.owner_id == self.user.id)
except Exception as e:
logger.error(f"Fehler bei Job-Ownership-Check: {e}")
return False
# ===== DECORATORS =====
def require_permission(permission: Permission):
"""
Decorator der eine bestimmte Berechtigung erfordert
Args:
permission: Die erforderliche Berechtigung
"""
def decorator(f):
@wraps(f)
@login_required
def wrapper(*args, **kwargs):
checker = PermissionChecker()
if not checker.has_permission(permission):
logger.warning(f"Zugriff verweigert: User {current_user.id} hat keine Berechtigung {permission.value}")
if request.path.startswith('/api/'):
return jsonify({
'error': 'Insufficient permissions',
'message': f'Berechtigung "{permission.value}" erforderlich',
'required_permission': permission.value
}), 403
else:
abort(403)
return f(*args, **kwargs)
return wrapper
return decorator
def require_role(role: Role):
"""
Decorator der eine bestimmte Rolle erfordert
Args:
role: Die erforderliche Rolle
"""
def decorator(f):
@wraps(f)
@login_required
def wrapper(*args, **kwargs):
checker = PermissionChecker()
user_roles = checker._get_user_roles()
if role not in user_roles:
logger.warning(f"Zugriff verweigert: User {current_user.id} hat nicht die Rolle {role.value}")
if request.path.startswith('/api/'):
return jsonify({
'error': 'Insufficient role',
'message': f'Rolle "{role.value}" erforderlich',
'required_role': role.value
}), 403
else:
abort(403)
return f(*args, **kwargs)
return wrapper
return decorator
def require_resource_access(resource_type: str, action: str = "view"):
"""
Decorator für ressourcen-spezifische Berechtigungsprüfung
Args:
resource_type: Art der Ressource
action: Erforderliche Aktion
"""
def decorator(f):
@wraps(f)
@login_required
def wrapper(*args, **kwargs):
# Resource ID aus URL-Parametern extrahieren
resource_id = kwargs.get('id') or kwargs.get(f'{resource_type}_id')
checker = PermissionChecker()
if not checker.can_access_resource(resource_type, resource_id, action):
logger.warning(f"Ressourcen-Zugriff verweigert: User {current_user.id}, {resource_type}:{resource_id}, Action: {action}")
if request.path.startswith('/api/'):
return jsonify({
'error': 'Resource access denied',
'message': f'Zugriff auf {resource_type} nicht erlaubt',
'resource_type': resource_type,
'action': action
}), 403
else:
abort(403)
return f(*args, **kwargs)
return wrapper
return decorator
# ===== UTILITY FUNCTIONS =====
def check_permission(permission: Permission, user=None) -> bool:
"""
Standalone-Funktion zur Berechtigungsprüfung
Args:
permission: Die zu prüfende Berechtigung
user: Benutzer (optional, default: current_user)
Returns:
bool: True wenn Berechtigung vorhanden
"""
checker = PermissionChecker(user)
return checker.has_permission(permission)
def get_user_permissions(user=None) -> Set[Permission]:
"""
Gibt alle Berechtigungen eines Benutzers zurück
Args:
user: Benutzer (optional, default: current_user)
Returns:
Set[Permission]: Alle Berechtigungen des Benutzers
"""
checker = PermissionChecker(user)
return checker.get_all_permissions()
def grant_temporary_permission(user_id: int, permission: Permission, duration_hours: int = 24, reason: str = "", granted_by_id: int = None):
"""
Gewährt temporäre Berechtigung
Args:
user_id: ID des Benutzers
permission: Die zu gewährende Berechtigung
duration_hours: Dauer in Stunden
reason: Begründung
granted_by_id: ID des gewährenden Benutzers
"""
try:
from models import get_db_session
db_session = get_db_session()
override = UserPermissionOverride(
user_id=user_id,
permission=permission.value,
granted=True,
reason=reason,
granted_by=granted_by_id or (current_user.id if current_user.is_authenticated else None),
expires_at=datetime.now() + timedelta(hours=duration_hours)
)
db_session.add(override)
db_session.commit()
db_session.close()
logger.info(f"Temporäre Berechtigung gewährt: {permission.value} für User {user_id} ({duration_hours}h)")
except Exception as e:
logger.error(f"Fehler beim Gewähren temporärer Berechtigung: {e}")
# ===== TEMPLATE HELPERS =====
def init_permission_helpers(app):
"""
Registriert Template-Helper für Berechtigungen
Args:
app: Flask-App-Instanz
"""
@app.template_global()
def has_permission(permission_name: str) -> bool:
"""Template Helper für Berechtigungsprüfung"""
try:
permission = Permission(permission_name)
return check_permission(permission)
except ValueError:
return False
@app.template_global()
def has_role(role_name: str) -> bool:
"""Template Helper für Rollenprüfung"""
try:
role = Role(role_name)
checker = PermissionChecker()
return role in checker._get_user_roles()
except ValueError:
return False
@app.template_global()
def can_access(resource_type: str, resource_id: int = None, action: str = "view") -> bool:
"""Template Helper für Ressourcen-Zugriff"""
checker = PermissionChecker()
return checker.can_access_resource(resource_type, resource_id, action)
logger.info("🔐 Permission Template Helpers registriert")