fix(authz): teams.py etait le module que la validation n avait pas atteint

SEC-16, de l'audit anterieur. La vague G a pose un schema a la frontiere de
matches, team_matches, tryouts et evaluations, et le HANDOFF en a tire une
regle : tout champ de formulaire passe par un schema de app/validators.py,
pas par request.form.get. teams.py ne l'avait jamais appliquee.

Deux defauts, pas un.

int(request.form.get('coach_id')) leve sur une valeur non numerique : une
soumission fabriquee etait un 500.

Et l'identifiant obtenu etait ensuite resolu sans verifier le role du compte,
dans deux des trois endroits qui le faisaient. La branche sync_staff
d'edit_team testait isinstance(user, Coach) ; son autre branche non, et
create_team non plus. Le meme fichier en desaccord avec lui-meme, sur
exactement le defaut que la vague G avait corrige dans tryouts.py -- une
soumission fabriquee pouvait nommer un joueur parmi les coachs d'une equipe.
L'identifiant vient d'un <select> rendu par le navigateur : c'est une valeur
que le client choisit.

_staff_member est la reponse unique, et OrgTeamSchema garantit que les
identifiants arrivent en entiers. Les deux tests qui epinglent le role
tombent si la verification saute : verifie par mutation.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
GGThed
2026-08-11 19:37:13 -04:00
co-authored by Claude Opus 5
parent ad3dea6a15
commit dd8b9671c6
3 changed files with 211 additions and 51 deletions
+43
View File
@@ -716,6 +716,49 @@ class TryoutSchema(StripMixin):
)
class OrgTeamSchema(StripMixin):
"""An organisation team, created or edited (SEC-16).
`teams.py` was the one route module wave G's validation pass did not
reach, and it still read every field through `request.form.get` and
converted with a bare `int()`. Two consequences, both reachable from a
hand-made POST by anyone allowed to manage teams:
- `int('abc')` raises, so a non-numeric `coach_id` was a 500;
- `int('-1')` was accepted, and the id was then looked up without ever
checking what role the account had.
The second is the same defect wave G found in `tryouts.py`, where a
forged submission could name a *player* as coach. Here it survived in
two of the three branches of the same file: `edit_team`'s `sync_staff`
path checks `isinstance(user, Coach)`, its other path does not, and
`create_team` does not either. Role checking belongs with the lookup,
not with the schema, so it lives in `_staff_member` in the route — but
the ids have to survive the trip as integers first.
"""
name = fields.String(
required=True,
validate=validate.Length(min=1, max=100, error=_l('Team name is required.')),
error_messages={'required': _l('Team name is required.')},
)
coach_id = fields.Integer(
allow_none=True,
load_default=None,
validate=validate.Range(min=1),
error_messages={'invalid': _l('Invalid coach selection.')},
)
manager_id = fields.Integer(
allow_none=True,
load_default=None,
validate=validate.Range(min=1),
error_messages={'invalid': _l('Invalid manager selection.')},
)
coach_ids = fields.List(fields.Integer(validate=validate.Range(min=1)), load_default=list)
manager_ids = fields.List(fields.Integer(validate=validate.Range(min=1)), load_default=list)
sync_staff = fields.String(allow_none=True, load_default=None)
def score_field():
"""One evaluation criterion: 1 to 10, or not scored at all."""
return fields.Integer(