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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user