mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-02-09 05:38:55 -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
290 lines
12 KiB
Python
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
|
|
|