refactor(rbac): Migrate legacy is_admin checks to role-based has_role()/set_role()
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

Replace ~20 remaining is_admin references across backend, templates and scripts
with proper SystemRole checks. Column is_admin stays as deprecated (synced by
set_role()) until DB migration removes it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-05 21:06:22 +01:00
parent 85e14bb4bf
commit c0d60481f0
16 changed files with 47 additions and 38 deletions

5
app.py
View File

@ -162,7 +162,8 @@ from database import (
HourlyActivity,
AuditLog,
SecurityAlert,
ZOPKNews
ZOPKNews,
SystemRole
)
# Import services
@ -291,7 +292,7 @@ def is_admin_exempt():
"""Exempt logged-in admins from rate limiting."""
from flask_login import current_user
try:
return current_user.is_authenticated and current_user.is_admin
return current_user.is_authenticated and current_user.has_role(SystemRole.ADMIN)
except Exception:
return False

View File

@ -158,7 +158,7 @@ def admin_users():
companies = db.query(Company).order_by(Company.name).all()
total_users = len(users)
admin_count = sum(1 for u in users if u.is_admin)
admin_count = sum(1 for u in users if u.has_role(SystemRole.ADMIN))
verified_count = sum(1 for u in users if u.is_verified)
unverified_count = total_users - verified_count
@ -203,12 +203,13 @@ def admin_user_add():
password_hash=password_hash,
name=data.get('name', '').strip() or None,
company_id=data.get('company_id') or None,
is_admin=data.get('is_admin', False),
is_verified=data.get('is_verified', True),
is_active=True
)
db.add(new_user)
if data.get('is_admin', False):
new_user.set_role(SystemRole.ADMIN)
db.commit()
db.refresh(new_user)
@ -243,15 +244,20 @@ def admin_user_toggle_admin(user_id):
if not user:
return jsonify({'success': False, 'error': 'Użytkownik nie znaleziony'}), 404
user.is_admin = not user.is_admin
if user.has_role(SystemRole.ADMIN):
user.set_role(SystemRole.MEMBER)
else:
user.set_role(SystemRole.ADMIN)
db.commit()
logger.info(f"Admin {current_user.email} {'granted' if user.is_admin else 'revoked'} admin for user {user.email}")
is_now_admin = user.has_role(SystemRole.ADMIN)
logger.info(f"Admin {current_user.email} {'granted' if is_now_admin else 'revoked'} admin for user {user.email}")
return jsonify({
'success': True,
'is_admin': user.is_admin,
'message': f"{'Nadano' if user.is_admin else 'Odebrano'} uprawnienia admina"
'is_admin': is_now_admin,
'role': user.role,
'message': f"{'Nadano' if is_now_admin else 'Odebrano'} uprawnienia admina"
})
finally:
db.close()

View File

@ -504,7 +504,7 @@ def admin_company_users(company_id):
'id': u.id,
'name': u.name,
'email': u.email,
'is_admin': u.is_admin,
'role': u.role,
'is_verified': u.is_verified
} for u in users]

View File

@ -67,7 +67,7 @@ def admin_security():
User.totp_enabled == True
).count()
total_admins = db.query(User).filter(
User.is_admin == True
User.role == 'ADMIN'
).count()
# Alert type breakdown

View File

@ -307,7 +307,7 @@ def admin_status():
# Users statistics
try:
app_metrics['admins'] = db.query(User).filter(User.is_admin == True).count()
app_metrics['admins'] = db.query(User).filter(User.role == 'ADMIN').count()
app_metrics['users_with_2fa'] = db.query(User).filter(User.totp_enabled == True).count()
except Exception:
db.rollback()

View File

@ -48,7 +48,7 @@ INSTRUKCJE:
- email (WYMAGANY - jeśli brak prawidłowego emaila, pomiń użytkownika)
- imię i nazwisko (jeśli dostępne)
- firma (dopasuj do listy dostępnych firm po nazwie, nawet częściowej)
- rola: jeśli tekst zawiera słowa "admin", "administrator", "zarząd" przy danej osobie - ustaw is_admin na true
- rola: jeśli tekst zawiera słowa "admin", "administrator", "zarząd" przy danej osobie - ustaw role na "ADMIN", w przeciwnym razie "MEMBER"
3. Jeśli email jest niepoprawny (brak @), dodaj ostrzeżenie
4. Jeśli firma nie pasuje do żadnej z listy, ustaw company_id na null
@ -61,7 +61,7 @@ ZWRÓĆ TYLKO CZYSTY JSON w dokładnie takim formacie (bez żadnego tekstu przed
"name": "Imię Nazwisko lub null",
"company_id": 123,
"company_name": "Nazwa dopasowanej firmy lub null",
"is_admin": false,
"role": "MEMBER",
"warnings": []
}}
]
@ -95,7 +95,7 @@ ZWRÓĆ TYLKO CZYSTY JSON w dokładnie takim formacie (bez żadnego tekstu przed
"name": "Imię Nazwisko lub null",
"company_id": 123,
"company_name": "Nazwa dopasowanej firmy lub null",
"is_admin": false,
"role": "MEMBER",
"warnings": []
}}
]
@ -267,11 +267,14 @@ def admin_users_bulk_create():
password_hash=password_hash,
name=user_data.get('name', '').strip() or None,
company_id=company_id,
is_admin=user_data.get('is_admin', False),
is_verified=True,
is_active=True
)
db.add(new_user)
# Set role based on AI parse result (supports both old is_admin and new role field)
ai_role = user_data.get('role', 'MEMBER')
if ai_role == 'ADMIN' or user_data.get('is_admin', False):
new_user.set_role(SystemRole.ADMIN)
db.flush() # Get the ID
created.append({

View File

@ -512,7 +512,7 @@ def api_enrich_company_ai(company_id):
}), 404
# Check permissions: user with company edit rights
logger.info(f"Permission check: user={current_user.email}, is_admin={current_user.is_admin}, user_company_id={current_user.company_id}, target_company_id={company.id}")
logger.info(f"Permission check: user={current_user.email}, role={current_user.role}, user_company_id={current_user.company_id}, target_company_id={company.id}")
if not current_user.can_edit_company(company.id):
return jsonify({
'success': False,

View File

@ -1066,7 +1066,7 @@ def report_content():
# Notify admins about the report
try:
admin_users = db.query(User).filter(User.is_admin == True, User.is_active == True).all()
admin_users = db.query(User).filter(User.role == 'ADMIN', User.is_active == True).all()
admin_ids = [u.id for u in admin_users]
reporter_name = current_user.name or current_user.email.split('@')[0]
create_forum_report_notification(

View File

@ -400,7 +400,7 @@ def accept_changes(app_id):
application.proposed_changes_comment = None
# Create notification for admins
admins = db.query(User).filter(User.is_admin == True).all()
admins = db.query(User).filter(User.role == 'ADMIN').all()
for admin in admins:
notification = UserNotification(
user_id=admin.id,
@ -500,7 +500,7 @@ def reject_changes(app_id):
application.updated_at = datetime.now()
# Create notification for admins
admins = db.query(User).filter(User.is_admin == True).all()
admins = db.query(User).filter(User.role == 'ADMIN').all()
for admin in admins:
notification = UserNotification(
user_id=admin.id,

View File

@ -1923,8 +1923,8 @@ class NordaEvent(Base):
if not user or not user.is_authenticated:
return False
# Admins can see everything
if user.is_admin or user.has_role(SystemRole.OFFICE_MANAGER):
# Admins and office managers can see everything
if user.has_role(SystemRole.OFFICE_MANAGER):
return True
access = self.access_level or 'members_only'
@ -1948,8 +1948,8 @@ class NordaEvent(Base):
if not user or not user.is_authenticated:
return False
# Admins can attend everything
if user.is_admin or user.has_role(SystemRole.OFFICE_MANAGER):
# Admins and office managers can attend everything
if user.has_role(SystemRole.OFFICE_MANAGER):
return True
access = self.access_level or 'members_only'
@ -1972,8 +1972,8 @@ class NordaEvent(Base):
if not user or not user.is_authenticated:
return False
# Admins can see attendees
if user.is_admin or user.has_role(SystemRole.OFFICE_MANAGER):
# Admins and office managers can see attendees
if user.has_role(SystemRole.OFFICE_MANAGER):
return True
access = self.access_level or 'members_only'

View File

@ -52,7 +52,6 @@ def main():
name=name,
password_hash=generate_password_hash(temp_password),
is_active=True,
is_admin=False,
company_id=company.id if company else None,
created_at=datetime.now()
)

View File

@ -27,7 +27,7 @@ def main():
db = SessionLocal()
try:
company = db.query(Company).filter_by(id=12).first()
admin_user = db.query(User).filter_by(is_admin=True).first()
admin_user = db.query(User).filter_by(role='ADMIN').first()
if not company:
print('Firma nie znaleziona')

View File

@ -555,7 +555,7 @@
<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
</svg>
</button>
{% if company.status == 'archived' and current_user.is_admin %}
{% if company.status == 'archived' and current_user.can_manage_users() %}
<button class="btn-icon danger" onclick="hardDeleteCompany({{ company.id }}, '{{ company.name|e }}')" title="Trwale usuń">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>

View File

@ -1122,7 +1122,7 @@
<tbody>
{% for user in users %}
<tr data-user-id="{{ user.id }}"
data-is-admin="{{ 'true' if user.is_admin else 'false' }}"
data-role="{{ user.role }}"
data-is-verified="{{ 'true' if user.is_verified else 'false' }}">
<td>{{ user.id }}</td>
<td>
@ -1158,7 +1158,7 @@
{{ user.created_at.strftime('%d.%m.%Y %H:%M') }}
</td>
<td>
{% if user.is_admin %}
{% if user.can_manage_users() %}
<span class="badge badge-admin">Admin</span>
{% endif %}
{% if user.is_rada_member %}
@ -1182,9 +1182,9 @@
</button>
<!-- Toggle Admin -->
<button class="btn-icon admin-toggle {{ 'active' if user.is_admin else '' }}"
<button class="btn-icon admin-toggle {{ 'active' if user.can_manage_users() else '' }}"
onclick="toggleAdmin({{ user.id }})"
title="{{ 'Odbierz uprawnienia admina' if user.is_admin else 'Nadaj uprawnienia admina' }}"
title="{{ 'Odbierz uprawnienia admina' if user.can_manage_users() else 'Nadaj uprawnienia admina' }}"
{% if user.id == current_user.id %}disabled{% endif %}>
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
@ -1744,7 +1744,7 @@ Lub format CSV, Excel, lista emaili..."></textarea>
const filter = this.dataset.filter;
document.querySelectorAll('[data-user-id]').forEach(row => {
const isAdmin = row.dataset.isAdmin === 'true';
const isAdmin = row.dataset.role === 'ADMIN';
const isVerified = row.dataset.isVerified === 'true';
let show = false;
@ -2434,7 +2434,7 @@ Lub format CSV, Excel, lista emaili..."></textarea>
</td>
<td>${user.name || '-'}</td>
<td>${user.company_name || '-'}</td>
<td>${user.is_admin ? 'Tak' : 'Nie'}</td>
<td>${user.role === 'ADMIN' ? 'Tak' : 'Nie'}</td>
<td>
${hasWarnings ? `
<div class="ai-user-warning">

View File

@ -684,7 +684,7 @@
<div class="info-box">
<p>
<strong>{{ reviewer.name if reviewer else 'Biuro Izby NORDA' }}</strong>
{% if reviewer and reviewer.is_admin %}<span class="reviewer-role">Biuro Izby NORDA</span>{% endif %}
{% if reviewer and reviewer.can_access_admin_panel() %}<span class="reviewer-role">Biuro Izby NORDA</span>{% endif %}
zaproponował(a) aktualizację danych na podstawie oficjalnych danych z
{% if application.registry_source == 'KRS' %}
Krajowego Rejestru Sądowego (KRS).
@ -730,7 +730,7 @@
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
</svg>
Komentarz od: <strong>{{ reviewer.name if reviewer else 'Biuro Izby NORDA' }}</strong>
{% if reviewer and reviewer.is_admin %}<span class="reviewer-role">Biuro Izby NORDA</span>{% endif %}
{% if reviewer and reviewer.can_access_admin_panel() %}<span class="reviewer-role">Biuro Izby NORDA</span>{% endif %}
</div>
<p>{{ application.proposed_changes_comment }}</p>
</div>

View File

@ -287,9 +287,9 @@ def admin_required(f):
if not current_user.is_authenticated:
return redirect(url_for('auth.login'))
# Use new role system, fallback to is_admin for backward compatibility
# Use role system (is_admin fallback removed — role is source of truth)
SystemRole = _get_system_role()
if not (current_user.has_role(SystemRole.ADMIN) or current_user.is_admin):
if not current_user.has_role(SystemRole.ADMIN):
flash('Brak uprawnień administratora.', 'error')
return redirect(url_for('public.index'))
return f(*args, **kwargs)