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
+101
View File
@@ -482,3 +482,104 @@ class TestTeamDeletionGuard:
with app.app_context():
assert db.session.get(OrgTeam, team_id) is not None
class TestTeamStaffAssignment:
"""SEC-16 — teams.py was the one route module wave G's validation pass
did not reach, and it showed in two ways.
`int(request.form.get('coach_id'))` raised on anything non-numeric, so a
hand-made POST was a 500. And the id it produced was looked up without
checking the account's role in two of the three places that used it:
`edit_team`'s sync_staff branch tested `isinstance(user, Coach)`, its
other branch did not, and `create_team` did not either. The same file
disagreeing with itself — and the same defect wave G fixed in tryouts.py.
"""
def test_a_non_numeric_id_is_refused_not_a_500(self, app, client, as_role):
from app.models import OrgTeam
as_role('manager')
response = client.post(
'/teams/create',
data={'name': 'Varsity', 'coach_id': 'not-a-number'},
follow_redirects=False,
)
assert response.status_code < 500, 'a forged field must not be a server error'
with app.app_context():
assert OrgTeam.query.filter_by(name='Varsity').first() is None
def test_a_player_cannot_be_named_coach_of_a_team(self, app, client, as_role, make_user):
"""The one that matters. The id comes from a <select> the browser
rendered, so it is a value the client chooses."""
from app.models import OrgTeam
player_id = make_user('player')
as_role('manager')
client.post(
'/teams/create',
data={'name': 'Varsity', 'coach_id': str(player_id)},
follow_redirects=False,
)
with app.app_context():
team = OrgTeam.query.filter_by(name='Varsity').first()
assert team is not None, 'the team should still be created, without the bad staff'
assert team.coach_id is None
assert team.coaches.all() == []
def test_a_real_coach_is_still_assigned(self, app, client, as_role, make_user):
"""The premise. Without it the test above passes against a route that
assigns nobody at all."""
from app.models import OrgTeam
coach_id = make_user('coach')
as_role('manager')
client.post(
'/teams/create',
data={'name': 'Varsity', 'coach_id': str(coach_id)},
follow_redirects=False,
)
with app.app_context():
team = OrgTeam.query.filter_by(name='Varsity').first()
assert team.coach_id == coach_id
assert [user.id for user in team.coaches.all()] == [coach_id]
def test_editing_cannot_slip_a_player_in_either(self, app, client, as_role, make_user):
"""The branch that had no role check at all."""
from app.models import OrgTeam
coach_id = make_user('coach')
player_id = make_user('player')
as_role('manager')
client.post('/teams/create', data={'name': 'Varsity', 'coach_id': str(coach_id)})
with app.app_context():
team_id = OrgTeam.query.filter_by(name='Varsity').first().id
client.post(
f'/teams/{team_id}/edit',
data={'name': 'Varsity', 'coach_id': str(player_id)},
follow_redirects=False,
)
with app.app_context():
team = db.session.get(OrgTeam, team_id)
assert player_id not in [user.id for user in team.coaches.all()]
assert team.coach_id != player_id
def test_a_team_still_needs_a_name(self, app, client, as_role):
from app.models import OrgTeam
as_role('manager')
response = client.post('/teams/create', data={'name': ''}, follow_redirects=False)
assert response.status_code < 500
with app.app_context():
assert OrgTeam.query.count() == 0