# Rôles et permissions Qui peut faire quoi, et où c'est décidé. Ce document décrit le comportement **implémenté**, pas celui qu'on souhaiterait : quand les deux divergent, c'est noté. ## Les cinq rôles | Rôle | Nom dans le code | En français, dans l'interface | |---|---|---| | Président | `admin` | président | | Gérant | `manager` | gérant | | Coach | `coach` | coach | | Recruteur | `scout` | recruteur | | Joueur | `player` | joueur | Le rôle est une **colonne discriminante** : `users.role` décide de la classe Python que SQLAlchemy instancie (`Admin`, `Manager`, …). C'est pourquoi l'autorisation se fait par `isinstance()` et non par comparaison de chaînes, et pourquoi changer un rôle passe par un `UPDATE` explicite suivi d'une relecture de l'instance — voir le long commentaire dans `edit_user`. ## Capacités globales Lues sur l'objet utilisateur. Défaut hérité de `User` : **tout est `False`**, chaque sous-classe ouvre ce qui la concerne. Ajouter un rôle sans rien surcharger donne donc un compte qui ne peut rien, ce qui est le bon sens de défaut. | Capacité | Président | Gérant | Coach | Recruteur | Joueur | |---|:--:|:--:|:--:|:--:|:--:| | `can_manage_users` | ✅ | — | — | — | — | | `can_manage_teams` | ✅ | ✅ | — | — | — | | `can_manage_tryouts` | ✅ | ✅ | ✅ | — | — | | `can_schedule_matches` | ✅ | ✅ | ✅ | — | — | | `can_evaluate` | ✅ | ✅ | ✅ | ✅ | — | Le recruteur évalue et ne gère rien : c'est le seul rôle en lecture-plus- évaluation. ## Capacités portant sur un objet Les deux qui comptent, parce qu'elles répondent différemment selon l'objet. ### `can_manage_this_tryout(tryout)` | Rôle | Règle | |---|---| | Président | toujours vrai | | Gérant | `tryout.created_by == moi` **ou** `tryout.manager_id == moi` | | Coach | délègue à `coach_manages_tryout()` — voir plus bas | | Recruteur, Joueur | toujours faux | Un gérant ne gère donc **pas** toutes les sélections, alors qu'il gère toutes les équipes. L'asymétrie est dans le code depuis toujours ; elle est peut-être voulue, elle n'est écrite nulle part ailleurs qu'ici. ### `can_manage_this_org_team(org_team)` | Rôle | Règle | |---|---| | Président | toujours vrai | | Gérant | **toujours vrai** — toutes les équipes, pas seulement les siennes | | Coach | rattaché à l'équipe, par la relation m2m **ou** par la colonne héritée | | Recruteur, Joueur | toujours faux | ## Le piège : deux façons d'être rattaché C'est le point le plus coûteux de tout le modèle d'autorisation, et il est encore ouvert (`ARCH-001`). Un coach est rattaché à une équipe de **deux** manières : ``` OrgTeam.coach_id colonne d'origine, un seul coach OrgTeam.coaches relation many-to-many, ajoutée ensuite ``` **Les deux sont encore alimentées à l'écriture.** Ne lire que `coach_id` excluait silencieusement tout coach qui n'était pas le premier de son équipe ; y ajouter un `.first()` excluait un coach de toutes ses équipes sauf une. Les deux défauts étaient actifs en production. Même duplication pour les gérants (`manager_id` / `managers`) et pour les sélections (`Tryout.coach_id` / `Tryout.coaches` / `target_org_team_id`). **Règle absolue : ne jamais réintroduire ce motif.** ```python # ❌ jamais OrgTeam.query.filter_by(coach_id=user.id).first() # ✅ toujours from app.permissions import coach_org_teams coach_org_teams(user) ``` `app/permissions.py` est **le** point de vérité. Toute question « ce coach peut-il… ? » ou « quelles équipes… ? » passe par lui. Il considère les deux rattachements et toutes les équipes, ce qui rend la duplication inoffensive **en lecture**. L'écriture crée toujours les deux. `ARCH-001` fera disparaître les colonnes héritées — migration de données, donc bloqué sur Alembic (`DB-002`, voir `docs/database-schema.md`). ## Les fonctions à appeler Toutes dans `app/permissions.py`, toutes prenant l'utilisateur agissant en argument explicite plutôt que de lire `current_user` — ce qui les rend appelables depuis les modèles, depuis le bot Discord et depuis les tests, sans contexte de requête. | Fonction | Répond à | |---|---| | `coach_org_teams(coach)` | Les équipes de ce coach, les deux rattachements confondus | | `manager_org_teams(manager)` | Idem pour un gérant | | `attached_org_teams(user)` | Les équipes auxquelles cette personne est rattachée, quel que soit son rôle | | `visible_org_teams(user)` | Ce qu'elle a le droit de voir | | `can_manage_org_team(user, team)` | Peut-elle administrer cette équipe | | `coach_player_ids(coach)` | Les joueurs qu'il encadre | | `coach_can_access_player(coach, id)` | Peut-il lire la fiche de ce joueur | | `can_manage_player_contract(user, id)` | Peut-elle déposer un contrat pour lui | | `coach_tryouts(coach)` | Les sélections qu'il gère | | `coach_manages_tryout(coach, tryout)` | Gère-t-il celle-ci | ## Ce que voit chaque rôle `get_visible_tryouts()`, sur l'objet utilisateur : | Rôle | Sélections visibles | |---|---| | Président | toutes | | Recruteur | toutes | | Gérant | celles qu'il a créées ou dont il est le gérant | | Coach | celles qu'il gère, via `coach_tryouts()` | | Joueur | celles où il est inscrit, plus celles ouvertes à son équipe | ## Inscription et activation **Un compte créé par le formulaire d'inscription est actif immédiatement.** `is_active_account` vaut `True` par défaut ; il n'existe aucune étape de validation par le staff. Ce qui filtre les inscriptions aujourd'hui est un champ piège et un délai minimal (`SEC-AUTH-008`, `app/routes/auth.py`), plus une limite de débit de 20 par heure et par IP. C'est calibré contre le pourriel de masse, pas contre quelqu'un qui lit la page. **C'est une décision de produit en attente** : faut-il que le staff active les comptes ? Tant qu'elle n'est pas prise, n'importe qui peut créer un compte joueur et voir ce qu'un joueur voit. ## Désactiver un compte `is_active_account = False` prend effet **à la requête suivante** : `User.is_active` est surchargé pour le lire, et Flask-Login le consulte à chaque requête. Une session ouverte ne survit donc pas à la désactivation. ## Le dernier président Deux garde-fous, parce qu'aucune interface ne permet de les défaire ensuite : - un président ne peut pas changer **son propre** rôle ; - le **dernier président actif** ne peut pas être rétrogradé. ## Ce qui n'est pas couvert ici - La matrice détaillée par domaine fonctionnel de l'audit du 2026-08-07 vit dans `audit/2026-08-07-MATRICE-AUTORISATIONS.md`, non versionné. - Les tests d'autorisation sont dans `tests/test_authorization.py` et `tests/test_permissions.py`. Ils sont la référence exécutable : si ce document et eux divergent, **ce sont eux qui ont raison**.