Files
team-tryouts/docs/roles-and-permissions.md
GGThed 7a1dab21cd docs: les deux documents d exploitation qui manquaient
DOC-001 demandait quatre documents courts. Deux existaient — installation
(deployment.md) et restauration (database-restore.md). Les deux autres
n existaient pas.

docs/roles-and-permissions.md decrit le comportement **implemente**, pas
celui qu on souhaiterait, et note les endroits ou les deux divergent : un
gerant gere toutes les equipes mais pas toutes les selections, asymetrie
presente depuis toujours et ecrite nulle part ailleurs. Le piege du double
rattachement coach/equipe y est en toutes lettres, avec le motif a ne
jamais reintroduire.

Le document dit aussi qui a raison en cas de desaccord : les tests. Une
documentation d autorisation qui se contredit avec le code est pire que
pas de documentation, parce qu on la croit.

docs/incident-runbook.md part du symptome, pas du composant. Le reflexe
d ouverture est la reference affichee sur la page d erreur — c est ce que
l identifiant de requete rend possible, et sans un endroit qui le dise, la
fonctionnalite ne sert a personne.

Trois affirmations ont ete verifiees contre le code avant d etre ecrites,
et deux etaient fausses : le corps de /health en echec dit
database="error" et non "disconnected", et les evenements de verrouillage
s appellent login.failure et account.throttled. La liste complete des
quatorze evenements de auth.log est maintenant dans le manuel.

README : index des documents, et deux points d etat corriges — TRUSTED_PROXY
est desormais une variable, et l absence de validation des inscriptions par
le staff est nommee comme decision en attente.
2026-08-11 15:40:30 -04:00

6.7 KiB

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.

# ❌ 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.