mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-02-13 07:44:03 -06:00
BREAKING CHANGE: Permission system now actively enforced across all routes ## Summary Complete implementation of advanced role-based access control (RBAC) system with full route protection, UI conditionals, and enhanced management interface. ## Route Protection - Updated all admin routes to use @admin_or_permission_required decorator - Replaced inline admin checks with granular permission checks in: * Admin routes: user management, settings, backups, telemetry, OIDC * Project routes: create, edit, delete, archive, bulk operations * Client routes: create, edit, delete, archive, bulk operations - Maintained backward compatibility with existing @admin_required decorator ## UI Permission Integration - Added template helpers (has_permission, has_any_permission) to all templates - Navigation conditionally shows admin/OIDC links based on permissions - Action buttons (Edit, Delete, Archive) conditional on user permissions - Project and client pages respect permission requirements - Create buttons visible only with appropriate permissions ## Enhanced Roles & Permissions UI - Added statistics dashboard showing: * Total roles, system roles, custom roles, assigned users - Implemented expandable permission details in roles list * Click to view all permissions grouped by category * Visual checkmarks for assigned permissions - Enhanced user list with role visibility: * Shows all assigned roles as color-coded badges * Blue badges for system roles, gray for custom roles * Yellow badges for legacy roles with migration prompt * Merged legacy role column into unified "Roles & Permissions" - User count per role now clickable and accurate ## Security Improvements - Added CSRF tokens to all new permission system forms: * Role creation/edit form * Role deletion form * User role assignment form - All POST requests now protected against CSRF attacks ## Technical Details - Fixed SQLAlchemy relationship query issues (AppenderQuery) - Proper use of .count() for relationship aggregation - Jinja2 namespace for accumulating counts in templates - Responsive grid layouts for statistics and permission cards ## Documentation - Created comprehensive implementation guides - Added permission enforcement documentation - Documented UI enhancements and features - Included CSRF protection review ## Impact - Permissions are now actively enforced, not just defined - Admins can easily see who has what access - Clear visual indicators of permission assignments - Secure forms with CSRF protection - Production-ready permission system
178 lines
5.1 KiB
Python
178 lines
5.1 KiB
Python
"""Utilities for permission checking and decorators"""
|
|
from functools import wraps
|
|
from flask import flash, redirect, url_for, abort
|
|
from flask_login import current_user
|
|
from flask_babel import gettext as _
|
|
|
|
|
|
def permission_required(*permissions, require_all=False):
|
|
"""
|
|
Decorator to require one or more permissions.
|
|
|
|
Args:
|
|
*permissions: Permission name(s) required
|
|
require_all: If True, user must have ALL permissions. If False, user needs ANY permission.
|
|
|
|
Example:
|
|
@permission_required('edit_projects')
|
|
def edit_project():
|
|
...
|
|
|
|
@permission_required('view_reports', 'export_reports', require_all=True)
|
|
def export_report():
|
|
...
|
|
"""
|
|
def decorator(f):
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if not current_user.is_authenticated:
|
|
flash(_('Please log in to access this page'), 'error')
|
|
return redirect(url_for('auth.login'))
|
|
|
|
# Check if user has required permissions
|
|
if require_all:
|
|
has_access = current_user.has_all_permissions(*permissions)
|
|
else:
|
|
has_access = current_user.has_any_permission(*permissions)
|
|
|
|
if not has_access:
|
|
flash(_('You do not have permission to access this page'), 'error')
|
|
abort(403)
|
|
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
return decorator
|
|
|
|
|
|
def admin_or_permission_required(*permissions):
|
|
"""
|
|
Decorator that allows access if user is an admin OR has any of the specified permissions.
|
|
This is useful for gradual migration from admin-only to permission-based access.
|
|
|
|
Example:
|
|
@admin_or_permission_required('delete_projects')
|
|
def delete_project():
|
|
...
|
|
"""
|
|
def decorator(f):
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if not current_user.is_authenticated:
|
|
flash(_('Please log in to access this page'), 'error')
|
|
return redirect(url_for('auth.login'))
|
|
|
|
# Allow access if user is admin or has any of the required permissions
|
|
if current_user.is_admin or current_user.has_any_permission(*permissions):
|
|
return f(*args, **kwargs)
|
|
|
|
flash(_('You do not have permission to access this page'), 'error')
|
|
abort(403)
|
|
|
|
return decorated_function
|
|
return decorator
|
|
|
|
|
|
def check_permission(user, permission_name):
|
|
"""
|
|
Check if a user has a specific permission.
|
|
|
|
Args:
|
|
user: User object
|
|
permission_name: Name of the permission to check
|
|
|
|
Returns:
|
|
bool: True if user has the permission, False otherwise
|
|
"""
|
|
if not user or not user.is_authenticated:
|
|
return False
|
|
|
|
return user.has_permission(permission_name)
|
|
|
|
|
|
def check_any_permission(user, *permission_names):
|
|
"""
|
|
Check if a user has any of the specified permissions.
|
|
|
|
Args:
|
|
user: User object
|
|
*permission_names: Names of permissions to check
|
|
|
|
Returns:
|
|
bool: True if user has any of the permissions, False otherwise
|
|
"""
|
|
if not user or not user.is_authenticated:
|
|
return False
|
|
|
|
return user.has_any_permission(*permission_names)
|
|
|
|
|
|
def check_all_permissions(user, *permission_names):
|
|
"""
|
|
Check if a user has all of the specified permissions.
|
|
|
|
Args:
|
|
user: User object
|
|
*permission_names: Names of permissions to check
|
|
|
|
Returns:
|
|
bool: True if user has all of the permissions, False otherwise
|
|
"""
|
|
if not user or not user.is_authenticated:
|
|
return False
|
|
|
|
return user.has_all_permissions(*permission_names)
|
|
|
|
|
|
def get_user_permissions(user):
|
|
"""
|
|
Get all permissions for a user.
|
|
|
|
Args:
|
|
user: User object
|
|
|
|
Returns:
|
|
list: List of Permission objects
|
|
"""
|
|
if not user:
|
|
return []
|
|
|
|
return user.get_all_permissions()
|
|
|
|
|
|
def get_user_permission_names(user):
|
|
"""
|
|
Get all permission names for a user.
|
|
|
|
Args:
|
|
user: User object
|
|
|
|
Returns:
|
|
list: List of permission name strings
|
|
"""
|
|
if not user:
|
|
return []
|
|
|
|
permissions = user.get_all_permissions()
|
|
return [p.name for p in permissions]
|
|
|
|
|
|
# Template helper functions (register these in app context)
|
|
def init_permission_helpers(app):
|
|
"""
|
|
Initialize permission helper functions for use in templates.
|
|
|
|
Usage in templates:
|
|
{% if has_permission('edit_projects') %}
|
|
<button>Edit Project</button>
|
|
{% endif %}
|
|
"""
|
|
@app.context_processor
|
|
def inject_permission_helpers():
|
|
return {
|
|
'has_permission': lambda perm: check_permission(current_user, perm),
|
|
'has_any_permission': lambda *perms: check_any_permission(current_user, *perms),
|
|
'has_all_permissions': lambda *perms: check_all_permissions(current_user, *perms),
|
|
'get_user_permissions': lambda: get_user_permission_names(current_user) if current_user.is_authenticated else [],
|
|
}
|
|
|