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

173 lines
6.7 KiB
Markdown

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