feat: Implement user permission management with presets and dynamic access control

This commit is contained in:
2026-04-17 21:14:30 +02:00
parent 88a67160f2
commit 5cf9a4f1dd
4 changed files with 625 additions and 6 deletions
+198 -1
View File
@@ -145,6 +145,79 @@ _NOTIFICATION_CACHE_LOCK = threading.Lock()
SCHOOL_PERIODS = cfg.SCHOOL_PERIODS 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 # Apply the configuration for general use throughout the app
APP_VERSION = __version__ APP_VERSION = __version__
RELEASE_STATE_FILE = os.path.join(os.path.dirname(BASE_DIR), '.release-version') RELEASE_STATE_FILE = os.path.join(os.path.dirname(BASE_DIR), '.release-version')
@@ -199,6 +272,47 @@ def _enforce_csrf_protection():
return None 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(): def _get_asset_version():
"""Return a cache-busting asset version tied to deployment state.""" """Return a cache-busting asset version tied to deployment state."""
env_version = os.getenv('INVENTAR_ASSET_VERSION', '').strip() env_version = os.getenv('INVENTAR_ASSET_VERSION', '').strip()
@@ -237,6 +351,37 @@ def _get_current_module(path):
return 'inventory' 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): def _append_audit_event(db, event_type, payload):
"""Write an audit entry; never break business flow on audit failures.""" """Write an audit entry; never break business flow on audit failures."""
try: try:
@@ -845,12 +990,18 @@ def inject_version():
asset_version = _get_asset_version() asset_version = _get_asset_version()
csrf_token = _get_csrf_token() csrf_token = _get_csrf_token()
unread_notification_count = 0 unread_notification_count = 0
current_permissions = us.build_default_permission_payload('standard_user')
if 'username' in session: if 'username' in session:
try: try:
is_admin = us.check_admin(session['username']) is_admin = us.check_admin(session['username'])
except Exception: except Exception:
is_admin = False 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 client = None
try: try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT) client = MongoClient(MONGODB_HOST, MONGODB_PORT)
@@ -874,6 +1025,10 @@ def inject_version():
'student_cards_module_enabled': cfg.STUDENT_CARDS_MODULE_ENABLED, 'student_cards_module_enabled': cfg.STUDENT_CARDS_MODULE_ENABLED,
'is_admin': is_admin, 'is_admin': is_admin,
'unread_notification_count': unread_notification_count, '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 # Create necessary directories at startup
@@ -6577,6 +6732,10 @@ def user_del():
break break
if username and username != session['username']: 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: try:
name = us.get_name(username) name = us.get_name(username)
last_name = us.get_last_name(username) last_name = us.get_last_name(username)
@@ -6597,7 +6756,10 @@ def user_del():
'admin': user.get('Admin', False), 'admin': user.get('Admin', False),
'fullname': fullname, 'fullname': fullname,
'name': name, '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( return render_template(
@@ -7614,6 +7776,41 @@ def admin_update_user_name():
return redirect(url_for('user_del')) 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') @app.route('/logs')
def logs(): def logs():
""" """
+56 -4
View File
@@ -769,18 +769,24 @@
</button> </button>
<div class="collapse navbar-collapse" id="inventoryNavContent"> <div class="collapse navbar-collapse" id="inventoryNavContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="inventoryNavList"> <ul class="navbar-nav me-auto mb-2 mb-lg-0" id="inventoryNavList">
{% if current_permissions.pages.get('home', True) %}
<li class="nav-item" data-nav-fixed="true"> <li class="nav-item" data-nav-fixed="true">
<a class="nav-link {% if current_path == url_for('home') %}nav-active{% endif %}" href="{{ url_for('home') }}">Artikel</a> <a class="nav-link {% if current_path == url_for('home') %}nav-active{% endif %}" href="{{ url_for('home') }}">Artikel</a>
</li> </li>
{% endif %}
{% if 'username' in session %} {% if 'username' in session %}
{% if current_permissions.pages.get('my_borrowed_items', True) %}
<li class="nav-item" data-nav-fixed="true"> <li class="nav-item" data-nav-fixed="true">
<a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}">Meine Ausleihen</a> <a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}">Meine Ausleihen</a>
</li> </li>
{% endif %}
{% if current_permissions.pages.get('tutorial_page', True) %}
<li class="nav-item" data-nav-fixed="true"> <li class="nav-item" data-nav-fixed="true">
<a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}">Tutorial</a> <a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}">Tutorial</a>
</li> </li>
{% endif %} {% endif %}
{% if 'username' in session and (session.get('admin', False) or is_admin) %} {% endif %}
{% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.pages.get('upload_admin', True) and current_permissions.actions.get('can_manage_settings', True) %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link nav-priority-link {% if current_path == url_for('upload_admin') %}nav-active{% endif %}" href="{{ url_for('upload_admin') }}"> Hochladen</a> <a class="nav-link nav-priority-link {% if current_path == url_for('upload_admin') %}nav-active{% endif %}" href="{{ url_for('upload_admin') }}"> Hochladen</a>
</li> </li>
@@ -791,24 +797,46 @@
</a> </a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invMoreDropdown"> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invMoreDropdown">
{% if 'username' in session %} {% if 'username' in session %}
{% if current_permissions.pages.get('my_borrowed_items', True) %}
<li><a class="dropdown-item" href="{{ url_for('my_borrowed_items') }}">Meine Ausleihen</a></li> <li><a class="dropdown-item" href="{{ url_for('my_borrowed_items') }}">Meine Ausleihen</a></li>
{% endif %}
{% if current_permissions.pages.get('tutorial_page', True) %}
<li><a class="dropdown-item" href="{{ url_for('tutorial_page') }}">Tutorial</a></li> <li><a class="dropdown-item" href="{{ url_for('tutorial_page') }}">Tutorial</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
{% endif %} {% endif %}
{% if 'username' in session and (session.get('admin', False) or is_admin) %} {% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.actions.get('can_manage_settings', True) %}
<li><h6 class="dropdown-header">Verwaltung</h6></li> <li><h6 class="dropdown-header">Verwaltung</h6></li>
{% if current_permissions.pages.get('manage_filters', True) %}
<li><a class="dropdown-item" href="{{ url_for('manage_filters') }}">Filter verwalten</a></li> <li><a class="dropdown-item" href="{{ url_for('manage_filters') }}">Filter verwalten</a></li>
{% endif %}
{% if current_permissions.pages.get('manage_locations', True) %}
<li><a class="dropdown-item" href="{{ url_for('manage_locations') }}">Orte verwalten</a></li> <li><a class="dropdown-item" href="{{ url_for('manage_locations') }}">Orte verwalten</a></li>
{% endif %}
{% if current_permissions.pages.get('admin_borrowings', True) %}
<li><a class="dropdown-item" href="{{ url_for('admin_borrowings') }}">Ausleihen</a></li> <li><a class="dropdown-item" href="{{ url_for('admin_borrowings') }}">Ausleihen</a></li>
{% endif %}
{% if current_permissions.pages.get('admin_damaged_items', True) %}
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li> <li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li>
{% endif %}
{% if current_permissions.actions.get('can_view_logs', True) and current_permissions.pages.get('admin_audit_dashboard', True) %}
<li><a class="dropdown-item" href="{{ url_for('admin_audit_dashboard') }}">Audit Dashboard</a></li> <li><a class="dropdown-item" href="{{ url_for('admin_audit_dashboard') }}">Audit Dashboard</a></li>
{% endif %}
{% if current_permissions.actions.get('can_view_logs', True) and current_permissions.pages.get('logs', True) %}
<li><a class="dropdown-item" href="{{ url_for('logs') }}">Logs</a></li> <li><a class="dropdown-item" href="{{ url_for('logs') }}">Logs</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
{% if current_permissions.actions.get('can_manage_users', True) %}
<li><h6 class="dropdown-header">System</h6></li> <li><h6 class="dropdown-header">System</h6></li>
{% if current_permissions.pages.get('user_del', True) %}
<li><a class="dropdown-item" href="{{ url_for('user_del') }}">Benutzer verwalten</a></li> <li><a class="dropdown-item" href="{{ url_for('user_del') }}">Benutzer verwalten</a></li>
{% endif %}
{% if current_permissions.pages.get('register', True) %}
<li><a class="dropdown-item" href="{{ url_for('register') }}">Neuer Benutzer</a></li> <li><a class="dropdown-item" href="{{ url_for('register') }}">Neuer Benutzer</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
{% endif %} {% endif %}
{% endif %}
<li><a class="dropdown-item" href="{{ url_for('impressum') }}">Impressum</a></li> <li><a class="dropdown-item" href="{{ url_for('impressum') }}">Impressum</a></li>
<li><a class="dropdown-item" href="{{ url_for('license') }}">Lizenz</a></li> <li><a class="dropdown-item" href="{{ url_for('license') }}">Lizenz</a></li>
</ul> </ul>
@@ -837,7 +865,9 @@
<span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span> <span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span>
</button> </button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invUserMenuDropdown"> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invUserMenuDropdown">
{% if current_permissions.pages.get('notifications_view', True) %}
<li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li> <li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('change_password') }}">Passwort ändern</a></li> <li><a class="dropdown-item" href="{{ url_for('change_password') }}">Passwort ändern</a></li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
@@ -860,18 +890,24 @@
</button> </button>
<div class="collapse navbar-collapse" id="libraryNavContent"> <div class="collapse navbar-collapse" id="libraryNavContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="libraryNavList"> <ul class="navbar-nav me-auto mb-2 mb-lg-0" id="libraryNavList">
{% if current_permissions.pages.get('library_view', True) %}
<li class="nav-item" data-nav-fixed="true"> <li class="nav-item" data-nav-fixed="true">
<a class="nav-link {% if current_path == url_for('library_view') %}nav-active{% endif %}" href="{{ url_for('library_view') }}">Medien</a> <a class="nav-link {% if current_path == url_for('library_view') %}nav-active{% endif %}" href="{{ url_for('library_view') }}">Medien</a>
</li> </li>
{% endif %}
{% if 'username' in session %} {% if 'username' in session %}
{% if current_permissions.pages.get('my_borrowed_items', True) %}
<li class="nav-item" data-nav-fixed="true"> <li class="nav-item" data-nav-fixed="true">
<a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}">Meine Medien</a> <a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}">Meine Medien</a>
</li> </li>
{% endif %}
{% if current_permissions.pages.get('tutorial_page', True) %}
<li class="nav-item" data-nav-fixed="true"> <li class="nav-item" data-nav-fixed="true">
<a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}">Tutorial</a> <a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}">Tutorial</a>
</li> </li>
{% endif %} {% endif %}
{% if 'username' in session and (session.get('admin', False) or is_admin) %} {% endif %}
{% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.actions.get('can_manage_settings', True) and current_permissions.pages.get('library_admin', True) %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link nav-priority-link {% if current_path == url_for('library_admin') %}nav-active{% endif %}" href="{{ url_for('library_admin') }}">📖 Hochladen</a> <a class="nav-link nav-priority-link {% if current_path == url_for('library_admin') %}nav-active{% endif %}" href="{{ url_for('library_admin') }}">📖 Hochladen</a>
</li> </li>
@@ -882,23 +918,37 @@
</a> </a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="libMoreDropdown"> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="libMoreDropdown">
{% if 'username' in session %} {% if 'username' in session %}
{% if current_permissions.pages.get('my_borrowed_items', True) %}
<li><a class="dropdown-item" href="{{ url_for('my_borrowed_items') }}">Meine Medien</a></li> <li><a class="dropdown-item" href="{{ url_for('my_borrowed_items') }}">Meine Medien</a></li>
{% endif %}
{% if current_permissions.pages.get('tutorial_page', True) %}
<li><a class="dropdown-item" href="{{ url_for('tutorial_page') }}">Tutorial</a></li> <li><a class="dropdown-item" href="{{ url_for('tutorial_page') }}">Tutorial</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
{% endif %} {% endif %}
{% if 'username' in session and (session.get('admin', False) or is_admin) %} {% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.actions.get('can_manage_settings', True) %}
<li><h6 class="dropdown-header">Bibliotheks-Verwaltung</h6></li> <li><h6 class="dropdown-header">Bibliotheks-Verwaltung</h6></li>
{% if current_permissions.pages.get('library_loans_admin', True) %}
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Ausleihen</a></li> <li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Ausleihen</a></li>
{% endif %}
{% if current_permissions.pages.get('admin_damaged_items', True) %}
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li> <li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li>
{% endif %}
{% if student_cards_module_enabled %} {% if student_cards_module_enabled %}
<li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li> <li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li>
{% endif %} {% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
{% if current_permissions.actions.get('can_manage_users', True) %}
<li><h6 class="dropdown-header">System</h6></li> <li><h6 class="dropdown-header">System</h6></li>
{% if current_permissions.pages.get('user_del', True) %}
<li><a class="dropdown-item" href="{{ url_for('user_del') }}">Benutzer verwalten</a></li> <li><a class="dropdown-item" href="{{ url_for('user_del') }}">Benutzer verwalten</a></li>
{% endif %}
{% if current_permissions.pages.get('register', True) %}
<li><a class="dropdown-item" href="{{ url_for('register') }}">Neuer Benutzer</a></li> <li><a class="dropdown-item" href="{{ url_for('register') }}">Neuer Benutzer</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
{% endif %} {% endif %}
{% endif %}
<li><a class="dropdown-item" href="{{ url_for('impressum') }}">Impressum</a></li> <li><a class="dropdown-item" href="{{ url_for('impressum') }}">Impressum</a></li>
<li><a class="dropdown-item" href="{{ url_for('license') }}">Lizenz</a></li> <li><a class="dropdown-item" href="{{ url_for('license') }}">Lizenz</a></li>
</ul> </ul>
@@ -927,7 +977,9 @@
<span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span> <span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span>
</button> </button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="libUserMenuDropdown"> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="libUserMenuDropdown">
{% if current_permissions.pages.get('notifications_view', True) %}
<li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li> <li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('change_password') }}">Passwort ändern</a></li> <li><a class="dropdown-item" href="{{ url_for('change_password') }}">Passwort ändern</a></li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
+150
View File
@@ -62,6 +62,7 @@
<th>Vorname</th> <th>Vorname</th>
<th>Nachname</th> <th>Nachname</th>
<th>Administrator</th> <th>Administrator</th>
<th>Rechte-Preset</th>
<th>Aktionen</th> <th>Aktionen</th>
</tr> </tr>
</thead> </thead>
@@ -72,6 +73,7 @@
<td>{{ user.name if user.name else user.username }}</td> <td>{{ user.name if user.name else user.username }}</td>
<td>{{ user.last_name if user.last_name else '' }}</td> <td>{{ user.last_name if user.last_name else '' }}</td>
<td>{{ "Ja" if user.admin else "Nein" }}</td> <td>{{ "Ja" if user.admin else "Nein" }}</td>
<td>{{ permission_presets.get(user.permission_preset, {}).get('label', user.permission_preset) }}</td>
<td class="actions"> <td class="actions">
<form method="POST" action="{{ url_for('delete_user') }}" class="d-inline"> <form method="POST" action="{{ url_for('delete_user') }}" class="d-inline">
<input type="hidden" name="username" value="{{ user.username }}"> <input type="hidden" name="username" value="{{ user.username }}">
@@ -90,6 +92,14 @@
onclick="openResetPasswordModal('{{ user.username }}')"> onclick="openResetPasswordModal('{{ user.username }}')">
Passwort zurücksetzen Passwort zurücksetzen
</button> </button>
<button type="button" class="btn btn-info btn-sm"
data-username="{{ user.username }}"
data-preset="{{ user.permission_preset }}"
data-action-permissions='{{ user.action_permissions | tojson }}'
data-page-permissions='{{ user.page_permissions | tojson }}'
onclick="openPermissionsModal(this)">
Berechtigungen
</button>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -99,6 +109,62 @@
</div> </div>
</div> </div>
<!-- Permission Modal -->
<div class="modal fade" id="permissionsModal" tabindex="-1" aria-labelledby="permissionsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="permissionsModalLabel">Berechtigungen bearbeiten</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" action="{{ url_for('admin_update_user_permissions') }}">
<div class="modal-body permissions-modal-body">
<input type="hidden" id="perm-username" name="username">
<div class="mb-3">
<label for="perm-username-display" class="form-label">Benutzer</label>
<input type="text" class="form-control" id="perm-username-display" disabled>
</div>
<div class="mb-3">
<label for="permission-preset" class="form-label">Preset</label>
<select class="form-select" id="permission-preset" name="permission_preset">
{% for preset_key, preset_value in permission_presets.items() %}
<option value="{{ preset_key }}">{{ preset_value.label }}</option>
{% endfor %}
</select>
</div>
<div class="permissions-grid">
<div class="permissions-group">
<h6>Aktionsrechte</h6>
{% for action_key, action_label in permission_action_options %}
<div class="form-check">
<input class="form-check-input permission-action-checkbox" type="checkbox" id="action-{{ action_key }}" name="action_{{ action_key }}">
<label class="form-check-label" for="action-{{ action_key }}">{{ action_label }}</label>
</div>
{% endfor %}
</div>
<div class="permissions-group">
<h6>Seitenrechte</h6>
{% for endpoint_name, endpoint_label in permission_page_options %}
<div class="form-check">
<input class="form-check-input permission-page-checkbox" type="checkbox" id="page-{{ endpoint_name }}" name="page_{{ endpoint_name }}">
<label class="form-check-label" for="page-{{ endpoint_name }}">{{ endpoint_label }}</label>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">Berechtigungen speichern</button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit User Modal --> <!-- Edit User Modal -->
<div class="modal fade" id="editUserModal" tabindex="-1" aria-labelledby="editUserModalLabel" aria-hidden="true"> <div class="modal fade" id="editUserModal" tabindex="-1" aria-labelledby="editUserModalLabel" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
@@ -165,6 +231,24 @@
</div> </div>
<script> <script>
var permissionPresets = {{ permission_presets | tojson }};
function applyPresetToPermissionForm(presetKey) {
var preset = permissionPresets[presetKey] || {};
var actionDefaults = preset.actions || {};
var pageDefaults = preset.pages || {};
document.querySelectorAll('.permission-action-checkbox').forEach(function (checkbox) {
var key = checkbox.name.replace('action_', '');
checkbox.checked = !!actionDefaults[key];
});
document.querySelectorAll('.permission-page-checkbox').forEach(function (checkbox) {
var key = checkbox.name.replace('page_', '');
checkbox.checked = !!pageDefaults[key];
});
}
function applyFilters() { function applyFilters() {
var search = document.getElementById('filter-search').value.toLowerCase(); var search = document.getElementById('filter-search').value.toLowerCase();
var adminFilter = document.getElementById('filter-admin').value.toLowerCase(); var adminFilter = document.getElementById('filter-admin').value.toLowerCase();
@@ -229,6 +313,13 @@
document.getElementById('filter-admin').addEventListener('change', applyFilters); document.getElementById('filter-admin').addEventListener('change', applyFilters);
document.getElementById('filter-column').addEventListener('change', applyFilters); document.getElementById('filter-column').addEventListener('change', applyFilters);
document.getElementById('filter-direction').addEventListener('change', applyFilters); document.getElementById('filter-direction').addEventListener('change', applyFilters);
var presetSelect = document.getElementById('permission-preset');
if (presetSelect) {
presetSelect.addEventListener('change', function () {
applyPresetToPermissionForm(this.value);
});
}
}); });
function openEditUserModal(button) { function openEditUserModal(button) {
@@ -253,6 +344,42 @@
var modal = new bootstrap.Modal(document.getElementById('resetPasswordModal')); var modal = new bootstrap.Modal(document.getElementById('resetPasswordModal'));
modal.show(); modal.show();
} }
function openPermissionsModal(button) {
var username = button.getAttribute('data-username') || '';
var preset = button.getAttribute('data-preset') || 'standard_user';
var actionPermissions = {};
var pagePermissions = {};
try {
actionPermissions = JSON.parse(button.getAttribute('data-action-permissions') || '{}');
} catch (err) {
actionPermissions = {};
}
try {
pagePermissions = JSON.parse(button.getAttribute('data-page-permissions') || '{}');
} catch (err) {
pagePermissions = {};
}
document.getElementById('perm-username').value = username;
document.getElementById('perm-username-display').value = username;
document.getElementById('permission-preset').value = preset;
document.querySelectorAll('.permission-action-checkbox').forEach(function (checkbox) {
var key = checkbox.name.replace('action_', '');
checkbox.checked = !!actionPermissions[key];
});
document.querySelectorAll('.permission-page-checkbox').forEach(function (checkbox) {
var key = checkbox.name.replace('page_', '');
checkbox.checked = !!pagePermissions[key];
});
var modal = new bootstrap.Modal(document.getElementById('permissionsModal'));
modal.show();
}
</script> </script>
<style> <style>
@@ -286,5 +413,28 @@
.password-requirements p { .password-requirements p {
margin-bottom: 5px; margin-bottom: 5px;
} }
.permissions-modal-body {
max-height: 70vh;
overflow-y: auto;
}
.permissions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 18px;
}
.permissions-group {
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 12px;
background: #f8fafc;
}
.permissions-group h6 {
font-weight: 700;
margin-bottom: 10px;
}
</style> </style>
{% endblock %} {% endblock %}
+221 -1
View File
@@ -11,6 +11,7 @@ Provides methods for creating, validating, and retrieving user information.
For commercial licensing inquiries: https://github.com/AIIrondev For commercial licensing inquiries: https://github.com/AIIrondev
''' '''
import hashlib import hashlib
import copy
from bson.objectid import ObjectId from bson.objectid import ObjectId
import settings as cfg import settings as cfg
from settings import MongoClient from settings import MongoClient
@@ -23,6 +24,220 @@ def normalize_student_card_id(card_id):
return str(card_id).strip().upper() return str(card_id).strip().upper()
ACTION_PERMISSION_KEYS = (
'can_borrow',
'can_insert',
'can_edit',
'can_delete',
'can_manage_users',
'can_manage_settings',
'can_view_logs',
)
DEFAULT_ACTION_PERMISSIONS = {
'can_borrow': True,
'can_insert': False,
'can_edit': False,
'can_delete': False,
'can_manage_users': False,
'can_manage_settings': False,
'can_view_logs': False,
}
DEFAULT_PAGE_PERMISSIONS = {
'home': True,
'tutorial_page': True,
'my_borrowed_items': True,
'notifications_view': True,
'impressum': True,
'license': True,
'library_view': True,
'terminplan': True,
'home_admin': False,
'upload_admin': False,
'library_admin': False,
'admin_borrowings': False,
'library_loans_admin': False,
'admin_damaged_items': False,
'admin_audit_dashboard': False,
'logs': False,
'user_del': False,
'register': False,
'manage_filters': False,
'manage_locations': False,
}
PERMISSION_PRESETS = {
'standard_user': {
'label': 'Standard (Ausleihe)',
'actions': {
'can_borrow': True,
},
'pages': {
'home': True,
'tutorial_page': True,
'my_borrowed_items': True,
'notifications_view': True,
'impressum': True,
'license': True,
'library_view': True,
'terminplan': True,
},
},
'editor': {
'label': 'Editor (Einfügen/Bearbeiten)',
'actions': {
'can_borrow': True,
'can_insert': True,
'can_edit': True,
},
'pages': {
'home': True,
'tutorial_page': True,
'my_borrowed_items': True,
'notifications_view': True,
'impressum': True,
'license': True,
'library_view': True,
'terminplan': True,
'upload_admin': True,
'library_admin': True,
},
},
'manager': {
'label': 'Manager (inkl. Löschen)',
'actions': {
'can_borrow': True,
'can_insert': True,
'can_edit': True,
'can_delete': True,
'can_manage_settings': True,
'can_view_logs': True,
},
'pages': {
'home': True,
'tutorial_page': True,
'my_borrowed_items': True,
'notifications_view': True,
'impressum': True,
'license': True,
'library_view': True,
'terminplan': True,
'home_admin': True,
'upload_admin': True,
'library_admin': True,
'admin_borrowings': True,
'library_loans_admin': True,
'admin_damaged_items': True,
'admin_audit_dashboard': True,
'logs': True,
'manage_filters': True,
'manage_locations': True,
},
},
'full_access': {
'label': 'Vollzugriff',
'actions': {
'can_borrow': True,
'can_insert': True,
'can_edit': True,
'can_delete': True,
'can_manage_users': True,
'can_manage_settings': True,
'can_view_logs': True,
},
'pages': {
'home': True,
'tutorial_page': True,
'my_borrowed_items': True,
'notifications_view': True,
'impressum': True,
'license': True,
'library_view': True,
'terminplan': True,
'home_admin': True,
'upload_admin': True,
'library_admin': True,
'admin_borrowings': True,
'library_loans_admin': True,
'admin_damaged_items': True,
'admin_audit_dashboard': True,
'logs': True,
'user_del': True,
'register': True,
'manage_filters': True,
'manage_locations': True,
},
},
}
def _normalize_bool_map(source, defaults):
result = dict(defaults)
if isinstance(source, dict):
for key, value in source.items():
result[str(key)] = bool(value)
return result
def get_permission_preset_definitions():
return copy.deepcopy(PERMISSION_PRESETS)
def build_default_permission_payload(preset_key='standard_user'):
selected_key = preset_key if preset_key in PERMISSION_PRESETS else 'standard_user'
preset = PERMISSION_PRESETS.get(selected_key, {})
action_defaults = _normalize_bool_map(preset.get('actions', {}), DEFAULT_ACTION_PERMISSIONS)
page_defaults = _normalize_bool_map(preset.get('pages', {}), DEFAULT_PAGE_PERMISSIONS)
return {
'preset': selected_key,
'actions': action_defaults,
'pages': page_defaults,
}
def get_effective_permissions(username):
user = get_user(username)
if not user:
return build_default_permission_payload('standard_user')
preset_key = user.get('PermissionPreset') or 'standard_user'
payload = build_default_permission_payload(preset_key)
payload['actions'] = _normalize_bool_map(user.get('ActionPermissions', {}), payload['actions'])
payload['pages'] = _normalize_bool_map(user.get('PagePermissions', {}), payload['pages'])
return payload
def update_user_permissions(username, preset_key, action_permissions=None, page_permissions=None):
selected_key = preset_key if preset_key in PERMISSION_PRESETS else 'standard_user'
payload = build_default_permission_payload(selected_key)
if isinstance(action_permissions, dict):
for key, value in action_permissions.items():
payload['actions'][str(key)] = bool(value)
if isinstance(page_permissions, dict):
for key, value in page_permissions.items():
payload['pages'][str(key)] = bool(value)
update_data = {
'PermissionPreset': payload['preset'],
'ActionPermissions': payload['actions'],
'PagePermissions': payload['pages'],
}
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
users = db['users']
result = users.update_one({'Username': username}, {'$set': update_data})
if result.matched_count == 0:
result = users.update_one({'username': username}, {'$set': update_data})
client.close()
return result.matched_count > 0
# === FAVORITES MANAGEMENT === # === FAVORITES MANAGEMENT ===
def get_favorites(username): def get_favorites(username):
"""Return a list of favorite item ObjectId strings for the user.""" """Return a list of favorite item ObjectId strings for the user."""
@@ -144,6 +359,8 @@ def add_user(username, password, name, last_name, is_student=False, student_card
users = db['users'] users = db['users']
if not check_password_strength(password): if not check_password_strength(password):
return False return False
permission_defaults = build_default_permission_payload('standard_user')
user_doc = { user_doc = {
'Username': username, 'Username': username,
'Password': hashing(password), 'Password': hashing(password),
@@ -151,7 +368,10 @@ def add_user(username, password, name, last_name, is_student=False, student_card
'active_ausleihung': None, 'active_ausleihung': None,
'name': name, 'name': name,
'last_name': last_name, 'last_name': last_name,
'IsStudent': bool(is_student) 'IsStudent': bool(is_student),
'PermissionPreset': permission_defaults['preset'],
'ActionPermissions': permission_defaults['actions'],
'PagePermissions': permission_defaults['pages'],
} }
normalized_card = normalize_student_card_id(student_card_id) normalized_card = normalize_student_card_id(student_card_id)