Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Previously only SUPERADMIN could access audit pages (SEO, GBP, Social Media, IT). Now MANAGER+ of a company can view audits for their own company. Route-level can_edit_company() check still restricts to own company only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
364 lines
11 KiB
Python
364 lines
11 KiB
Python
"""
|
|
Custom Decorators
|
|
=================
|
|
|
|
Reusable decorators for access control and validation.
|
|
|
|
Role-based decorators (new system):
|
|
- @role_required(SystemRole.OFFICE_MANAGER) - Minimum role check
|
|
- @company_permission('edit') - Company-specific permissions
|
|
- @member_required - Shortcut for role >= MEMBER
|
|
|
|
Legacy decorators (backward compatibility):
|
|
- @admin_required - Alias for @role_required(SystemRole.ADMIN)
|
|
- @company_owner_or_admin - Uses new can_edit_company() method
|
|
"""
|
|
|
|
from functools import wraps
|
|
from flask import abort, flash, redirect, url_for, request
|
|
from flask_login import current_user
|
|
|
|
def is_audit_owner():
|
|
"""True for SUPERADMIN or company MANAGER+ — can view audits.
|
|
SUPERADMIN sees all audits. MANAGER sees only own company (enforced in routes via can_edit_company).
|
|
"""
|
|
if not current_user.is_authenticated:
|
|
return False
|
|
from database import SystemRole
|
|
if current_user.has_role(SystemRole.SUPERADMIN):
|
|
return True
|
|
# MANAGER of any company can access audit dashboards (route-level check restricts to own company)
|
|
return current_user.can_edit_company()
|
|
|
|
# Import role enums (lazy import to avoid circular dependencies)
|
|
def _get_system_role():
|
|
from database import SystemRole
|
|
return SystemRole
|
|
|
|
|
|
# ============================================================
|
|
# NEW ROLE-BASED DECORATORS
|
|
# ============================================================
|
|
|
|
def role_required(min_role):
|
|
"""
|
|
Decorator that requires user to have at least the specified role.
|
|
|
|
Args:
|
|
min_role: Minimum SystemRole required (e.g., SystemRole.OFFICE_MANAGER)
|
|
|
|
Usage:
|
|
@bp.route('/admin/companies')
|
|
@login_required
|
|
@role_required(SystemRole.OFFICE_MANAGER)
|
|
def admin_companies():
|
|
...
|
|
|
|
Note: Always use @login_required BEFORE @role_required
|
|
"""
|
|
def decorator(f):
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if not current_user.is_authenticated:
|
|
return redirect(url_for('auth.login'))
|
|
|
|
if not current_user.has_role(min_role):
|
|
SystemRole = _get_system_role()
|
|
role_names = {
|
|
SystemRole.MEMBER: 'członka Izby',
|
|
SystemRole.EMPLOYEE: 'pracownika firmy',
|
|
SystemRole.MANAGER: 'kadry zarządzającej',
|
|
SystemRole.OFFICE_MANAGER: 'kierownika biura',
|
|
SystemRole.ADMIN: 'administratora',
|
|
}
|
|
role_name = role_names.get(min_role, 'wyższych uprawnień')
|
|
flash(f'Ta strona wymaga uprawnień {role_name}.', 'error')
|
|
return redirect(url_for('public.index'))
|
|
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
return decorator
|
|
|
|
|
|
def member_required(f):
|
|
"""
|
|
Decorator that requires user to be at least a MEMBER.
|
|
Shortcut for @role_required(SystemRole.MEMBER).
|
|
|
|
Usage:
|
|
@bp.route('/forum')
|
|
@login_required
|
|
@member_required
|
|
def forum_index():
|
|
...
|
|
"""
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if not current_user.is_authenticated:
|
|
return redirect(url_for('auth.login'))
|
|
|
|
SystemRole = _get_system_role()
|
|
if not current_user.has_role(SystemRole.MEMBER):
|
|
flash('Ta strona jest dostępna tylko dla członków Izby NORDA.', 'warning')
|
|
return redirect(url_for('public.index'))
|
|
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
|
|
def company_permission(permission_type='edit'):
|
|
"""
|
|
Decorator that checks user's permission for a company.
|
|
|
|
Args:
|
|
permission_type: 'view', 'edit', or 'manage'
|
|
|
|
The company_id is extracted from:
|
|
1. URL parameter 'company_id'
|
|
2. Query parameter 'company_id'
|
|
3. User's own company_id
|
|
|
|
Usage:
|
|
@bp.route('/company/<int:company_id>/edit')
|
|
@login_required
|
|
@company_permission('edit')
|
|
def edit_company(company_id):
|
|
...
|
|
|
|
@bp.route('/company/<int:company_id>/users')
|
|
@login_required
|
|
@company_permission('manage')
|
|
def manage_company_users(company_id):
|
|
...
|
|
"""
|
|
def decorator(f):
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if not current_user.is_authenticated:
|
|
return redirect(url_for('auth.login'))
|
|
|
|
# Get company_id from various sources
|
|
company_id = (
|
|
kwargs.get('company_id') or
|
|
request.args.get('company_id', type=int) or
|
|
current_user.company_id
|
|
)
|
|
|
|
if company_id is None:
|
|
flash('Nie określono firmy.', 'error')
|
|
return redirect(url_for('public.index'))
|
|
|
|
# Check permission based on type
|
|
has_permission = False
|
|
if permission_type == 'view':
|
|
# Anyone can view public companies, but for dashboard check company membership
|
|
has_permission = (
|
|
current_user.can_access_admin_panel() or
|
|
current_user.company_id == company_id
|
|
)
|
|
elif permission_type == 'edit':
|
|
has_permission = current_user.can_edit_company(company_id)
|
|
elif permission_type == 'manage':
|
|
has_permission = current_user.can_manage_company(company_id)
|
|
else:
|
|
abort(500, f"Unknown permission type: {permission_type}")
|
|
|
|
if not has_permission:
|
|
flash('Nie masz uprawnień do tej operacji.', 'error')
|
|
return redirect(url_for('public.index'))
|
|
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
return decorator
|
|
|
|
|
|
def office_manager_required(f):
|
|
"""
|
|
Decorator that requires user to be at least OFFICE_MANAGER.
|
|
Shortcut for @role_required(SystemRole.OFFICE_MANAGER).
|
|
|
|
Usage:
|
|
@bp.route('/admin/companies')
|
|
@login_required
|
|
@office_manager_required
|
|
def admin_companies():
|
|
...
|
|
"""
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if not current_user.is_authenticated:
|
|
return redirect(url_for('auth.login'))
|
|
|
|
SystemRole = _get_system_role()
|
|
if not current_user.has_role(SystemRole.OFFICE_MANAGER):
|
|
flash('Ta strona wymaga uprawnień kierownika biura.', 'error')
|
|
return redirect(url_for('public.index'))
|
|
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
|
|
def forum_access_required(f):
|
|
"""
|
|
Decorator that requires user to have forum access.
|
|
Members and above can access the forum.
|
|
|
|
Usage:
|
|
@bp.route('/forum/topic/<int:topic_id>')
|
|
@login_required
|
|
@forum_access_required
|
|
def view_topic(topic_id):
|
|
...
|
|
"""
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if not current_user.is_authenticated:
|
|
return redirect(url_for('auth.login'))
|
|
|
|
if not current_user.can_access_forum():
|
|
flash('Forum jest dostępne tylko dla członków Izby NORDA.', 'warning')
|
|
return redirect(url_for('public.index'))
|
|
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
|
|
def moderator_required(f):
|
|
"""
|
|
Decorator that requires forum moderator permissions.
|
|
OFFICE_MANAGER and ADMIN roles have this permission.
|
|
|
|
Usage:
|
|
@bp.route('/forum/topic/<int:topic_id>/delete')
|
|
@login_required
|
|
@moderator_required
|
|
def delete_topic(topic_id):
|
|
...
|
|
"""
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if not current_user.is_authenticated:
|
|
return redirect(url_for('auth.login'))
|
|
|
|
if not current_user.can_moderate_forum():
|
|
flash('Ta akcja wymaga uprawnień moderatora.', 'error')
|
|
return redirect(url_for('public.index'))
|
|
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
|
|
def rada_member_required(f):
|
|
"""
|
|
Decorator that requires user to be a member of Rada Izby (Board Council).
|
|
OFFICE_MANAGER and ADMIN roles also have access for management purposes.
|
|
|
|
Usage:
|
|
@bp.route('/rada/')
|
|
@login_required
|
|
@rada_member_required
|
|
def board_index():
|
|
...
|
|
"""
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if not current_user.is_authenticated:
|
|
return redirect(url_for('auth.login'))
|
|
|
|
SystemRole = _get_system_role()
|
|
# Allow access if: is_rada_member OR has OFFICE_MANAGER/ADMIN role
|
|
if not current_user.is_rada_member and not current_user.has_role(SystemRole.OFFICE_MANAGER):
|
|
flash('Strefa RADA jest dostępna tylko dla członków Rady Izby.', 'warning')
|
|
return redirect(url_for('public.index'))
|
|
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
|
|
# ============================================================
|
|
# LEGACY DECORATORS (backward compatibility)
|
|
# ============================================================
|
|
|
|
def admin_required(f):
|
|
"""
|
|
Decorator that requires user to be logged in AND be an admin.
|
|
|
|
DEPRECATED: Use @role_required(SystemRole.ADMIN) instead.
|
|
|
|
Usage:
|
|
@bp.route('/admin/users')
|
|
@login_required
|
|
@admin_required
|
|
def admin_users():
|
|
...
|
|
|
|
Note: Always use @login_required BEFORE @admin_required
|
|
"""
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if not current_user.is_authenticated:
|
|
return redirect(url_for('auth.login'))
|
|
|
|
# Use role system (is_admin fallback removed — role is source of truth)
|
|
SystemRole = _get_system_role()
|
|
if not current_user.has_role(SystemRole.ADMIN):
|
|
flash('Brak uprawnień administratora.', 'error')
|
|
return redirect(url_for('public.index'))
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
|
|
def verified_required(f):
|
|
"""
|
|
Decorator that requires user to have verified email.
|
|
|
|
Usage:
|
|
@bp.route('/forum/new')
|
|
@login_required
|
|
@verified_required
|
|
def new_topic():
|
|
...
|
|
"""
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if not current_user.is_authenticated:
|
|
return redirect(url_for('auth.login'))
|
|
if not current_user.is_verified:
|
|
flash('Musisz zweryfikować swój email, aby wykonać tę akcję.', 'warning')
|
|
return redirect(url_for('auth.resend_verification'))
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
|
|
def company_owner_or_admin(f):
|
|
"""
|
|
Decorator for routes that accept company_id.
|
|
Allows access only if user is admin OR owns the company.
|
|
|
|
DEPRECATED: Use @company_permission('edit') instead.
|
|
|
|
Usage:
|
|
@bp.route('/company/<int:company_id>/edit')
|
|
@login_required
|
|
@company_owner_or_admin
|
|
def edit_company(company_id):
|
|
...
|
|
"""
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if not current_user.is_authenticated:
|
|
return redirect(url_for('auth.login'))
|
|
|
|
company_id = kwargs.get('company_id')
|
|
if company_id is None:
|
|
abort(400)
|
|
|
|
# Use new permission system
|
|
if current_user.can_edit_company(company_id):
|
|
return f(*args, **kwargs)
|
|
|
|
flash('Nie masz uprawnień do tej firmy.', 'error')
|
|
return redirect(url_for('public.index'))
|
|
|
|
return decorated_function
|