Files
TimeTracker/app/utils/permissions_seed.py
Dries Peeters 944b69a7fc feat: implement full permission enforcement and enhanced UI visibility
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
2025-10-24 12:49:54 +02:00

290 lines
12 KiB
Python

"""Utility module for seeding default permissions and roles"""
from app import db
from app.models import Permission, Role, User
from sqlalchemy.exc import IntegrityError
# Define all available permissions organized by category
DEFAULT_PERMISSIONS = [
# Time Entry Permissions
{'name': 'view_own_time_entries', 'description': 'View own time entries', 'category': 'time_entries'},
{'name': 'view_all_time_entries', 'description': 'View all time entries from all users', 'category': 'time_entries'},
{'name': 'create_time_entries', 'description': 'Create time entries', 'category': 'time_entries'},
{'name': 'edit_own_time_entries', 'description': 'Edit own time entries', 'category': 'time_entries'},
{'name': 'edit_all_time_entries', 'description': 'Edit time entries from all users', 'category': 'time_entries'},
{'name': 'delete_own_time_entries', 'description': 'Delete own time entries', 'category': 'time_entries'},
{'name': 'delete_all_time_entries', 'description': 'Delete time entries from all users', 'category': 'time_entries'},
# Project Permissions
{'name': 'view_projects', 'description': 'View projects', 'category': 'projects'},
{'name': 'create_projects', 'description': 'Create new projects', 'category': 'projects'},
{'name': 'edit_projects', 'description': 'Edit project details', 'category': 'projects'},
{'name': 'delete_projects', 'description': 'Delete projects', 'category': 'projects'},
{'name': 'archive_projects', 'description': 'Archive/unarchive projects', 'category': 'projects'},
{'name': 'manage_project_costs', 'description': 'Manage project costs and budgets', 'category': 'projects'},
# Task Permissions
{'name': 'view_own_tasks', 'description': 'View own tasks', 'category': 'tasks'},
{'name': 'view_all_tasks', 'description': 'View all tasks', 'category': 'tasks'},
{'name': 'create_tasks', 'description': 'Create tasks', 'category': 'tasks'},
{'name': 'edit_own_tasks', 'description': 'Edit own tasks', 'category': 'tasks'},
{'name': 'edit_all_tasks', 'description': 'Edit all tasks', 'category': 'tasks'},
{'name': 'delete_own_tasks', 'description': 'Delete own tasks', 'category': 'tasks'},
{'name': 'delete_all_tasks', 'description': 'Delete all tasks', 'category': 'tasks'},
{'name': 'assign_tasks', 'description': 'Assign tasks to users', 'category': 'tasks'},
# Client Permissions
{'name': 'view_clients', 'description': 'View clients', 'category': 'clients'},
{'name': 'create_clients', 'description': 'Create new clients', 'category': 'clients'},
{'name': 'edit_clients', 'description': 'Edit client details', 'category': 'clients'},
{'name': 'delete_clients', 'description': 'Delete clients', 'category': 'clients'},
{'name': 'manage_client_notes', 'description': 'Manage client notes', 'category': 'clients'},
# Invoice Permissions
{'name': 'view_own_invoices', 'description': 'View own invoices', 'category': 'invoices'},
{'name': 'view_all_invoices', 'description': 'View all invoices', 'category': 'invoices'},
{'name': 'create_invoices', 'description': 'Create invoices', 'category': 'invoices'},
{'name': 'edit_invoices', 'description': 'Edit invoices', 'category': 'invoices'},
{'name': 'delete_invoices', 'description': 'Delete invoices', 'category': 'invoices'},
{'name': 'send_invoices', 'description': 'Send invoices to clients', 'category': 'invoices'},
{'name': 'manage_payments', 'description': 'Manage invoice payments', 'category': 'invoices'},
# Report Permissions
{'name': 'view_own_reports', 'description': 'View own reports', 'category': 'reports'},
{'name': 'view_all_reports', 'description': 'View reports for all users', 'category': 'reports'},
{'name': 'export_reports', 'description': 'Export reports to CSV/PDF', 'category': 'reports'},
{'name': 'create_saved_reports', 'description': 'Create and save custom reports', 'category': 'reports'},
# User Management Permissions
{'name': 'view_users', 'description': 'View users list', 'category': 'users'},
{'name': 'create_users', 'description': 'Create new users', 'category': 'users'},
{'name': 'edit_users', 'description': 'Edit user details', 'category': 'users'},
{'name': 'delete_users', 'description': 'Delete users', 'category': 'users'},
{'name': 'manage_user_roles', 'description': 'Assign roles to users', 'category': 'users'},
# System Permissions
{'name': 'manage_settings', 'description': 'Manage system settings', 'category': 'system'},
{'name': 'view_system_info', 'description': 'View system information', 'category': 'system'},
{'name': 'manage_backups', 'description': 'Create and restore backups', 'category': 'system'},
{'name': 'manage_telemetry', 'description': 'Manage telemetry settings', 'category': 'system'},
{'name': 'view_audit_logs', 'description': 'View audit logs', 'category': 'system'},
# Role & Permission Management (Super Admin only)
{'name': 'manage_roles', 'description': 'Create, edit, and delete roles', 'category': 'administration'},
{'name': 'manage_permissions', 'description': 'Assign permissions to roles', 'category': 'administration'},
{'name': 'view_permissions', 'description': 'View permissions and roles', 'category': 'administration'},
]
# Define default roles with their permissions
DEFAULT_ROLES = {
'super_admin': {
'description': 'Super Administrator with full system access',
'is_system_role': True,
'permissions': [p['name'] for p in DEFAULT_PERMISSIONS] # All permissions
},
'admin': {
'description': 'Administrator with most privileges',
'is_system_role': True,
'permissions': [
# Time entries
'view_all_time_entries', 'create_time_entries', 'edit_all_time_entries', 'delete_all_time_entries',
# Projects
'view_projects', 'create_projects', 'edit_projects', 'delete_projects', 'archive_projects', 'manage_project_costs',
# Tasks
'view_all_tasks', 'create_tasks', 'edit_all_tasks', 'delete_all_tasks', 'assign_tasks',
# Clients
'view_clients', 'create_clients', 'edit_clients', 'delete_clients', 'manage_client_notes',
# Invoices
'view_all_invoices', 'create_invoices', 'edit_invoices', 'delete_invoices', 'send_invoices', 'manage_payments',
# Reports
'view_all_reports', 'export_reports', 'create_saved_reports',
# Users
'view_users', 'create_users', 'edit_users', 'delete_users',
# System
'manage_settings', 'view_system_info', 'manage_backups', 'manage_telemetry', 'view_audit_logs',
]
},
'manager': {
'description': 'Team Manager with oversight capabilities',
'is_system_role': True,
'permissions': [
# Time entries
'view_all_time_entries', 'create_time_entries', 'edit_own_time_entries', 'delete_own_time_entries',
# Projects
'view_projects', 'create_projects', 'edit_projects', 'manage_project_costs',
# Tasks
'view_all_tasks', 'create_tasks', 'edit_all_tasks', 'assign_tasks',
# Clients
'view_clients', 'create_clients', 'edit_clients', 'manage_client_notes',
# Invoices
'view_all_invoices', 'create_invoices', 'edit_invoices', 'send_invoices',
# Reports
'view_all_reports', 'export_reports', 'create_saved_reports',
# Users
'view_users',
]
},
'user': {
'description': 'Standard User',
'is_system_role': True,
'permissions': [
# Time entries
'view_own_time_entries', 'create_time_entries', 'edit_own_time_entries', 'delete_own_time_entries',
# Projects
'view_projects',
# Tasks
'view_own_tasks', 'create_tasks', 'edit_own_tasks', 'delete_own_tasks',
# Clients
'view_clients',
# Invoices
'view_own_invoices',
# Reports
'view_own_reports', 'export_reports',
]
},
'viewer': {
'description': 'Read-only User',
'is_system_role': True,
'permissions': [
'view_own_time_entries',
'view_projects',
'view_own_tasks',
'view_clients',
'view_own_invoices',
'view_own_reports',
]
}
}
def seed_permissions():
"""Seed default permissions into the database"""
print("Seeding permissions...")
created_count = 0
existing_count = 0
for perm_data in DEFAULT_PERMISSIONS:
# Check if permission already exists
existing = Permission.query.filter_by(name=perm_data['name']).first()
if existing:
existing_count += 1
# Update description if it changed
if existing.description != perm_data['description']:
existing.description = perm_data['description']
existing.category = perm_data['category']
continue
# Create new permission
permission = Permission(
name=perm_data['name'],
description=perm_data['description'],
category=perm_data['category']
)
db.session.add(permission)
created_count += 1
try:
db.session.commit()
print(f"Permissions seeded: {created_count} created, {existing_count} already existed")
return True
except IntegrityError as e:
db.session.rollback()
print(f"Error seeding permissions: {e}")
return False
def seed_roles():
"""Seed default roles with their permissions"""
print("Seeding roles...")
created_count = 0
existing_count = 0
for role_name, role_data in DEFAULT_ROLES.items():
# Check if role already exists
existing = Role.query.filter_by(name=role_name).first()
if existing:
existing_count += 1
# Update description if it changed
if existing.description != role_data['description']:
existing.description = role_data['description']
role = existing
else:
# Create new role
role = Role(
name=role_name,
description=role_data['description'],
is_system_role=role_data['is_system_role']
)
db.session.add(role)
created_count += 1
# Assign permissions to role
for perm_name in role_data['permissions']:
permission = Permission.query.filter_by(name=perm_name).first()
if permission and not role.has_permission(perm_name):
role.add_permission(permission)
try:
db.session.commit()
print(f"Roles seeded: {created_count} created, {existing_count} already existed")
return True
except IntegrityError as e:
db.session.rollback()
print(f"Error seeding roles: {e}")
return False
def migrate_legacy_users():
"""Migrate users with legacy 'role' field to new role system"""
print("Migrating legacy users to new role system...")
migrated_count = 0
# Get all users
users = User.query.all()
for user in users:
# Skip if user already has roles assigned
if len(user.roles) > 0:
continue
# Map legacy role to new role system
if user.role == 'admin':
admin_role = Role.query.filter_by(name='admin').first()
if admin_role:
user.add_role(admin_role)
migrated_count += 1
else: # user.role == 'user' or any other value
user_role = Role.query.filter_by(name='user').first()
if user_role:
user.add_role(user_role)
migrated_count += 1
try:
db.session.commit()
print(f"Migrated {migrated_count} users to new role system")
return True
except Exception as e:
db.session.rollback()
print(f"Error migrating users: {e}")
return False
def seed_all():
"""Seed all permissions, roles, and migrate users"""
print("Starting permission system seeding...")
if not seed_permissions():
return False
if not seed_roles():
return False
if not migrate_legacy_users():
return False
print("Permission system seeding completed successfully!")
return True