Initialiser le catalogue RBAC au setup
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
This commit is contained in:
parent
9a540c600f
commit
d58b5aed0b
3 changed files with 116 additions and 23 deletions
|
|
@ -14,8 +14,8 @@ from ..models.user import User
|
||||||
from ..models.college import College, Building, Zone, Room
|
from ..models.college import College, Building, Zone, Room
|
||||||
from ..models.equipment import EquipmentCategory
|
from ..models.equipment import EquipmentCategory
|
||||||
from ..models.maintenance import Lot
|
from ..models.maintenance import Lot
|
||||||
from ..models.rbac import Role, Permission, RolePermission, UserRole
|
from ..models.rbac import Role
|
||||||
from ..services.rbac import replace_user_roles
|
from ..services.rbac import ensure_rbac_catalog, replace_user_roles
|
||||||
from ..authorization import has_permission
|
from ..authorization import has_permission
|
||||||
from ..models.planning import WorkSchedule, CollegeClosure, ClosureWorkDay
|
from ..models.planning import WorkSchedule, CollegeClosure, ClosureWorkDay
|
||||||
from ..setup_catalog import EQUIPMENT_CATEGORIES, LOTS, grouped_catalog
|
from ..setup_catalog import EQUIPMENT_CATEGORIES, LOTS, grouped_catalog
|
||||||
|
|
@ -271,25 +271,12 @@ def api_save_step(step):
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
# Le premier compte est le compte de récupération de l'installation
|
# Le premier compte est le compte de récupération de l'installation.
|
||||||
# et doit recevoir explicitement le rôle protégé super_admin.
|
# Le bootstrap crée aussi tout le catalogue RBAC (rôles système,
|
||||||
super_role = Role.query.filter_by(slug='super_admin', is_active=True).first()
|
# permissions et associations), afin qu'une installation neuve
|
||||||
if not super_role:
|
# soit immédiatement administrable après le wizard.
|
||||||
super_role = Role(slug='super_admin', name='Super administrateur',
|
role_map, _permission_map = ensure_rbac_catalog()
|
||||||
description="Rôle système protégé donnant un accès complet à l'application.",
|
super_role = role_map['super_admin']
|
||||||
is_system=True, is_active=True)
|
|
||||||
db.session.add(super_role)
|
|
||||||
db.session.flush()
|
|
||||||
wildcard = Permission.query.filter_by(code='*').first()
|
|
||||||
if not wildcard:
|
|
||||||
wildcard = Permission(code='*', name="Accès complet à l'application",
|
|
||||||
module='system', action='all',
|
|
||||||
description="Accès complet réservé au super administrateur.",
|
|
||||||
is_active=True)
|
|
||||||
db.session.add(wildcard)
|
|
||||||
db.session.flush()
|
|
||||||
if not RolePermission.query.filter_by(role_id=super_role.id, permission_id=wildcard.id).first():
|
|
||||||
db.session.add(RolePermission(role_id=super_role.id, permission_id=wildcard.id, effect='allow'))
|
|
||||||
replace_user_roles(user, [super_role.id])
|
replace_user_roles(user, [super_role.id])
|
||||||
|
|
||||||
progress.admin_username = username
|
progress.admin_username = username
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,87 @@ Cela permet aux routes d'enregistrer simultanément l'utilisateur et ses accès.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from ...extensions import db
|
from ...extensions import db
|
||||||
from ..models.rbac import Role, UserRole
|
from ..models.rbac import Role, Permission, UserRole, RolePermission
|
||||||
from ..models.user import User
|
from ..models.user import User
|
||||||
|
from ..authorization import PERMISSIONS_BY_ROLE, ROLE_LABELS, GRANULAR_PERMISSION_CODES, permission_metadata
|
||||||
|
|
||||||
|
|
||||||
class LastSuperAdminError(ValueError):
|
class LastSuperAdminError(ValueError):
|
||||||
"""Opération qui supprimerait le dernier super administrateur actif."""
|
"""Opération qui supprimerait le dernier super administrateur actif."""
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_rbac_catalog():
|
||||||
|
"""Initialise idempotemment le catalogue RBAC pour une installation neuve.
|
||||||
|
|
||||||
|
Cette fonction ne supprime aucun rôle, permission ou association existante.
|
||||||
|
Elle complète uniquement les rôles système et les permissions manquantes.
|
||||||
|
La transaction appelante reste responsable du commit.
|
||||||
|
"""
|
||||||
|
role_map = {}
|
||||||
|
for slug, codes in PERMISSIONS_BY_ROLE.items():
|
||||||
|
role = Role.query.filter_by(slug=slug).first()
|
||||||
|
if not role:
|
||||||
|
role = Role(
|
||||||
|
slug=slug,
|
||||||
|
name=ROLE_LABELS.get(slug, slug),
|
||||||
|
description={
|
||||||
|
'super_admin': "Rôle système protégé donnant un accès complet à l'application.",
|
||||||
|
'admin': "Rôle système destiné à l'administration courante de la GMAO. Ses permissions sont configurables.",
|
||||||
|
'responsable_gmao': "Rôle système destiné à la supervision et à la gestion générale de la GMAO.",
|
||||||
|
'technicien': "Rôle système destiné aux opérations de maintenance.",
|
||||||
|
'assistant_prevention': "Rôle système donnant accès aux fonctions liées à la prévention.",
|
||||||
|
'demandeur': "Rôle système destiné aux utilisateurs pouvant créer et suivre des demandes.",
|
||||||
|
'lecture': "Rôle système destiné aux utilisateurs autorisés uniquement à consulter certaines informations.",
|
||||||
|
}.get(slug, 'Rôle système fourni par l’application.'),
|
||||||
|
is_system=True,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
db.session.add(role)
|
||||||
|
db.session.flush()
|
||||||
|
else:
|
||||||
|
role.is_system = True
|
||||||
|
role.is_active = True
|
||||||
|
if not role.name or role.name in {slug, 'Administrateur système'}:
|
||||||
|
role.name = ROLE_LABELS.get(slug, role.name or slug)
|
||||||
|
role_map[slug] = role
|
||||||
|
|
||||||
|
codes = set(GRANULAR_PERMISSION_CODES)
|
||||||
|
codes.update(code for values in PERMISSIONS_BY_ROLE.values() for code in values)
|
||||||
|
codes.add('*')
|
||||||
|
permission_map = {}
|
||||||
|
for code in sorted(codes):
|
||||||
|
permission = Permission.query.filter_by(code=code).first()
|
||||||
|
if not permission:
|
||||||
|
module, _, action = code.partition('.')
|
||||||
|
name, description = permission_metadata(code)
|
||||||
|
permission = Permission(
|
||||||
|
code=code,
|
||||||
|
name=name,
|
||||||
|
module=module or 'system',
|
||||||
|
action=action or 'all',
|
||||||
|
description=description,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
db.session.add(permission)
|
||||||
|
db.session.flush()
|
||||||
|
else:
|
||||||
|
permission.is_active = True
|
||||||
|
name, description = permission_metadata(code)
|
||||||
|
permission.name = name
|
||||||
|
permission.description = description
|
||||||
|
permission_map[code] = permission
|
||||||
|
|
||||||
|
for slug, codes_for_role in PERMISSIONS_BY_ROLE.items():
|
||||||
|
role = role_map[slug]
|
||||||
|
for code in codes_for_role:
|
||||||
|
permission = permission_map[code]
|
||||||
|
link = RolePermission.query.filter_by(role_id=role.id, permission_id=permission.id).first()
|
||||||
|
if not link:
|
||||||
|
db.session.add(RolePermission(role_id=role.id, permission_id=permission.id, effect='allow'))
|
||||||
|
|
||||||
|
return role_map, permission_map
|
||||||
|
|
||||||
|
|
||||||
def ensure_super_admin_survives(user, *, will_be_active=None, deleting=False):
|
def ensure_super_admin_survives(user, *, will_be_active=None, deleting=False):
|
||||||
"""Bloque la désactivation/suppression du dernier super-admin actif."""
|
"""Bloque la désactivation/suppression du dernier super-admin actif."""
|
||||||
if not any(link.role and link.role.slug == "super_admin" and link.role.is_active for link in user.role_links):
|
if not any(link.role and link.role.slug == "super_admin" and link.role.is_active for link in user.role_links):
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,43 @@ import pytest
|
||||||
from app_new.extensions import db
|
from app_new.extensions import db
|
||||||
from app_new.core.models import User, Role, Permission, RolePermission, UserPermission, UserRole
|
from app_new.core.models import User, Role, Permission, RolePermission, UserPermission, UserRole
|
||||||
from app_new.core.authorization import has_permission
|
from app_new.core.authorization import has_permission
|
||||||
from app_new.core.services.rbac import replace_user_roles
|
from app_new.core.services.rbac import replace_user_roles, ensure_rbac_catalog
|
||||||
from app_new.core.services.rbac import ensure_super_admin_survives, LastSuperAdminError
|
from app_new.core.services.rbac import ensure_super_admin_survives, LastSuperAdminError
|
||||||
|
|
||||||
|
|
||||||
|
def test_rbac_catalog_bootstrap_is_complete_and_idempotent(app):
|
||||||
|
"""Le wizard doit pouvoir amorcer une base RBAC vierge sans doublons."""
|
||||||
|
with app.app_context():
|
||||||
|
role_map, permission_map = ensure_rbac_catalog()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
expected_roles = {
|
||||||
|
'super_admin', 'admin', 'responsable_gmao', 'technicien',
|
||||||
|
'assistant_prevention', 'demandeur', 'lecture',
|
||||||
|
}
|
||||||
|
assert expected_roles.issubset(role_map)
|
||||||
|
assert '*' in permission_map
|
||||||
|
assert RolePermission.query.filter_by(
|
||||||
|
role_id=role_map['super_admin'].id,
|
||||||
|
permission_id=permission_map['*'].id,
|
||||||
|
).first() is not None
|
||||||
|
assert RolePermission.query.filter_by(
|
||||||
|
role_id=role_map['admin'].id,
|
||||||
|
permission_id=permission_map['*'].id,
|
||||||
|
).first() is None
|
||||||
|
|
||||||
|
counts_before = {
|
||||||
|
'roles': Role.query.filter(Role.slug.in_(expected_roles)).count(),
|
||||||
|
'permissions': Permission.query.count(),
|
||||||
|
'links': RolePermission.query.count(),
|
||||||
|
}
|
||||||
|
ensure_rbac_catalog()
|
||||||
|
db.session.commit()
|
||||||
|
assert Role.query.filter(Role.slug.in_(expected_roles)).count() == counts_before['roles']
|
||||||
|
assert Permission.query.count() == counts_before['permissions']
|
||||||
|
assert RolePermission.query.count() == counts_before['links']
|
||||||
|
|
||||||
|
|
||||||
def _role_with_permissions(slug, permissions):
|
def _role_with_permissions(slug, permissions):
|
||||||
role = Role(slug=slug, name=slug, is_system=False, is_active=True)
|
role = Role(slug=slug, name=slug, is_system=False, is_active=True)
|
||||||
db.session.add(role)
|
db.session.add(role)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue