diff --git a/Web/app.py b/Web/app.py index 3247a60..3c259a1 100755 --- a/Web/app.py +++ b/Web/app.py @@ -145,6 +145,79 @@ _NOTIFICATION_CACHE_LOCK = threading.Lock() SCHOOL_PERIODS = cfg.SCHOOL_PERIODS +PERMISSION_ACTION_OPTIONS = [ + ('can_borrow', 'Ausleihe erlauben'), + ('can_insert', 'Einfügen/Hochladen erlauben'), + ('can_edit', 'Bearbeiten erlauben'), + ('can_delete', 'Löschen erlauben'), + ('can_manage_users', 'Benutzerverwaltung erlauben'), + ('can_manage_settings', 'Systemverwaltung erlauben'), + ('can_view_logs', 'Logs/Audit einsehen erlauben'), +] + +PERMISSION_PAGE_OPTIONS = [ + ('home', 'Artikel (Inventar)'), + ('tutorial_page', 'Tutorial'), + ('my_borrowed_items', 'Meine Ausleihen'), + ('notifications_view', 'Benachrichtigungen'), + ('impressum', 'Impressum'), + ('license', 'Lizenz'), + ('library_view', 'Bibliothek (Medien)'), + ('terminplan', 'Terminplan'), + ('home_admin', 'Admin Startseite'), + ('upload_admin', 'Admin Upload Inventar'), + ('library_admin', 'Admin Upload Bibliothek'), + ('admin_borrowings', 'Admin Ausleihen'), + ('library_loans_admin', 'Admin Bibliotheks-Ausleihen'), + ('admin_damaged_items', 'Admin Defekte Items'), + ('admin_audit_dashboard', 'Admin Audit Dashboard'), + ('logs', 'System-Logs'), + ('user_del', 'Benutzerverwaltung'), + ('register', 'Benutzer anlegen'), + ('manage_filters', 'Filter verwalten'), + ('manage_locations', 'Orte verwalten'), +] + +PERMISSION_EXEMPT_ENDPOINTS = { + 'static', + 'login', + 'logout', + 'impressum', + 'license', + 'uploaded_file', + 'thumbnail_file', + 'preview_file', +} + +PERMISSION_ACTION_ENDPOINTS = { + 'upload_item': 'can_insert', + 'upload_inventory_excel': 'can_insert', + 'upload_library_excel': 'can_insert', + 'upload_student_cards_excel': 'can_insert', + 'edit_item': 'can_edit', + 'api_library_item_update': 'can_edit', + 'admin_update_user_name': 'can_edit', + 'delete_item': 'can_delete', + 'delete_user': 'can_delete', + 'ausleihen': 'can_borrow', + 'zurueckgeben': 'can_borrow', + 'api_library_scan_action': 'can_borrow', + 'user_del': 'can_manage_users', + 'register': 'can_manage_users', + 'admin_reset_user_password': 'can_manage_users', + 'admin_update_user_permissions': 'can_manage_users', + 'home_admin': 'can_manage_settings', + 'upload_admin': 'can_manage_settings', + 'library_admin': 'can_manage_settings', + 'admin_borrowings': 'can_manage_settings', + 'library_loans_admin': 'can_manage_settings', + 'admin_damaged_items': 'can_manage_settings', + 'manage_filters': 'can_manage_settings', + 'manage_locations': 'can_manage_settings', + 'admin_audit_dashboard': 'can_view_logs', + 'logs': 'can_view_logs', +} + # Apply the configuration for general use throughout the app APP_VERSION = __version__ RELEASE_STATE_FILE = os.path.join(os.path.dirname(BASE_DIR), '.release-version') @@ -199,6 +272,47 @@ def _enforce_csrf_protection(): return None +@app.before_request +def _enforce_user_permissions(): + endpoint = request.endpoint + if not endpoint: + return None + + if endpoint == 'static' or endpoint.startswith('static'): + return None + + if endpoint in PERMISSION_EXEMPT_ENDPOINTS: + return None + + if 'username' not in session: + return None + + permissions = _get_current_user_permissions() + if not permissions: + return None + + if not _page_access_allowed(permissions, endpoint): + message = 'Diese Seite ist für Ihren Benutzer aktuell gesperrt.' + if request.path.startswith('/api/') or request.is_json: + return jsonify({'ok': False, 'message': message}), 403 + + flash(message, 'error') + fallback_endpoint = _permission_denied_fallback_endpoint(permissions) + return redirect(url_for(fallback_endpoint)) + + action_key = PERMISSION_ACTION_ENDPOINTS.get(endpoint) + if action_key and not _action_access_allowed(permissions, action_key): + message = 'Für diese Aktion fehlen Ihnen die erforderlichen Berechtigungen.' + if request.path.startswith('/api/') or request.is_json: + return jsonify({'ok': False, 'message': message}), 403 + + flash(message, 'error') + fallback_endpoint = _permission_denied_fallback_endpoint(permissions) + return redirect(url_for(fallback_endpoint)) + + return None + + def _get_asset_version(): """Return a cache-busting asset version tied to deployment state.""" env_version = os.getenv('INVENTAR_ASSET_VERSION', '').strip() @@ -237,6 +351,37 @@ def _get_current_module(path): return 'inventory' +def _get_current_user_permissions(): + username = session.get('username') + if not username: + return None + try: + return us.get_effective_permissions(username) + except Exception: + return us.build_default_permission_payload('standard_user') + + +def _page_access_allowed(permissions, endpoint): + if not permissions or not endpoint: + return True + page_permissions = permissions.get('pages', {}) + return bool(page_permissions.get(endpoint, True)) + + +def _action_access_allowed(permissions, action_key): + if not permissions or not action_key: + return True + action_permissions = permissions.get('actions', {}) + return bool(action_permissions.get(action_key, True)) + + +def _permission_denied_fallback_endpoint(permissions): + for candidate in ('home', 'my_borrowed_items', 'tutorial_page', 'notifications_view', 'impressum'): + if _page_access_allowed(permissions, candidate): + return candidate + return 'logout' + + def _append_audit_event(db, event_type, payload): """Write an audit entry; never break business flow on audit failures.""" try: @@ -845,12 +990,18 @@ def inject_version(): asset_version = _get_asset_version() csrf_token = _get_csrf_token() unread_notification_count = 0 + current_permissions = us.build_default_permission_payload('standard_user') if 'username' in session: try: is_admin = us.check_admin(session['username']) except Exception: is_admin = False + try: + current_permissions = us.get_effective_permissions(session['username']) + except Exception: + current_permissions = us.build_default_permission_payload('standard_user') + client = None try: client = MongoClient(MONGODB_HOST, MONGODB_PORT) @@ -874,6 +1025,10 @@ def inject_version(): 'student_cards_module_enabled': cfg.STUDENT_CARDS_MODULE_ENABLED, 'is_admin': is_admin, 'unread_notification_count': unread_notification_count, + 'current_permissions': current_permissions, + 'permission_action_options': PERMISSION_ACTION_OPTIONS, + 'permission_page_options': PERMISSION_PAGE_OPTIONS, + 'permission_presets': us.get_permission_preset_definitions(), } # Create necessary directories at startup @@ -6577,6 +6732,10 @@ def user_del(): break if username and username != session['username']: + try: + permissions_payload = us.get_effective_permissions(username) + except Exception: + permissions_payload = us.build_default_permission_payload('standard_user') try: name = us.get_name(username) last_name = us.get_last_name(username) @@ -6597,7 +6756,10 @@ def user_del(): 'admin': user.get('Admin', False), 'fullname': fullname, 'name': name, - 'last_name': last_name + 'last_name': last_name, + 'permission_preset': permissions_payload.get('preset', 'standard_user'), + 'action_permissions': permissions_payload.get('actions', {}), + 'page_permissions': permissions_payload.get('pages', {}), }) return render_template( @@ -7614,6 +7776,41 @@ def admin_update_user_name(): return redirect(url_for('user_del')) +@app.route('/admin_update_user_permissions', methods=['POST']) +def admin_update_user_permissions(): + """Admin route to update permission preset and per-endpoint overrides for a user.""" + if 'username' not in session or not us.check_admin(session['username']): + flash('Nicht autorisierter Zugriff', 'error') + return redirect(url_for('login')) + + username = request.form.get('username', '').strip() + preset_key = request.form.get('permission_preset', 'standard_user').strip() + + if not username: + flash('Kein Benutzer ausgewählt', 'error') + return redirect(url_for('user_del')) + + target_user = us.get_user(username) + if not target_user: + flash(f'Benutzer {username} nicht gefunden', 'error') + return redirect(url_for('user_del')) + + action_permissions = {} + for action_key, _ in PERMISSION_ACTION_OPTIONS: + action_permissions[action_key] = request.form.get(f'action_{action_key}') == 'on' + + page_permissions = {} + for endpoint_name, _ in PERMISSION_PAGE_OPTIONS: + page_permissions[endpoint_name] = request.form.get(f'page_{endpoint_name}') == 'on' + + if us.update_user_permissions(username, preset_key, action_permissions, page_permissions): + flash(f'Berechtigungen für {username} wurden aktualisiert.', 'success') + else: + flash('Fehler beim Aktualisieren der Berechtigungen.', 'error') + + return redirect(url_for('user_del')) + + @app.route('/logs') def logs(): """ diff --git a/Web/templates/base.html b/Web/templates/base.html index 96579fd..5950132 100755 --- a/Web/templates/base.html +++ b/Web/templates/base.html @@ -769,18 +769,24 @@ + + +