fix(authz): aligner la suppression d equipe, sans elargir l acces

SEC-AUTHZ-006. delete_team etait la seule des dix operations d'equipe
gardee par la capacite globale can_manage_teams() ; les neuf autres passent
par can_manage_this_org_team(team). Le constat avait raison sur
l'incoherence et tort sur le correctif.

Appliquer sa recommandation telle quelle -- remplacer par la verification
par objet -- ELARGIT l'acces. Coach repond False a la capacite globale et
True pour ses propres equipes : la substitution donnait donc a chaque coach
le pouvoir de supprimer l'equipe qu'il entraine, avec ses notes d'equipe et
son historique de matchs. Le constat raisonnait sur Manager, ou les deux
repondent True, et a manque le role ou elles divergent.

Les deux sont donc exigees. Le comportement d'aujourd'hui est preserve a
l'identique (administrateurs et gerants oui, coachs non) et la dette que le
constat visait est bien fermee : le jour ou Manager.can_manage_this_org_team
sera resserre -- ce qui est souhaitable -- la suppression se resserrera avec
lui au lieu de rester la seule porte ouverte.

C'est la quatrieme recommandation d'audit qu'il faut corriger avant de
l'appliquer. Le test qui l'epingle echoue si quelqu'un refait la
simplification : verifie par mutation.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
GGThed
2026-08-11 18:35:05 -04:00
co-authored by Claude Opus 5
parent bda23dbb67
commit e76cd7bb23
2 changed files with 89 additions and 3 deletions
+68
View File
@@ -414,3 +414,71 @@ class TestCorsPolicy:
)
assert response.headers.get('Access-Control-Allow-Origin') == 'https://trusted.test'
class TestTeamDeletionGuard:
"""SEC-AUTHZ-006 — the one team operation guarded differently.
`delete_team` used the global `can_manage_teams()` while the other nine
team operations use `can_manage_this_org_team(team)`. The constat asked
for a straight swap; a straight swap would have handed every coach the
power to delete the team they coach, because `Coach` answers False to
the first and True to the second. It now requires both, which keeps
today's answers and narrows automatically if the per-object rule is ever
tightened.
These tests are what stops the "simplification" from being reapplied.
"""
@pytest.fixture
def org_team(self, app, make_user):
from app.models import OrgTeam
admin_id = make_user('admin')
coach_id = make_user('coach')
with app.app_context():
team = OrgTeam(name='Varsity', created_by=admin_id, coach_id=coach_id)
db.session.add(team)
db.session.commit()
return team.id, coach_id
def test_a_coach_cannot_delete_the_team_they_coach(self, app, client, org_team, login):
"""The regression a straight swap would have introduced."""
from app.models import OrgTeam
team_id, coach_id = org_team
with app.app_context():
username = db.session.get(User, coach_id).username
login(username)
response = client.post(f'/teams/{team_id}/delete', follow_redirects=False)
assert response.status_code < 500
with app.app_context():
assert db.session.get(OrgTeam, team_id) is not None, (
'a coach deleted an organisation team, its notes and its match history'
)
def test_a_manager_still_can(self, app, client, org_team, as_role):
"""The premise. Without it the test above passes against a route
that refuses everyone."""
from app.models import OrgTeam
team_id, _coach_id = org_team
as_role('manager')
client.post(f'/teams/{team_id}/delete', follow_redirects=False)
with app.app_context():
assert db.session.get(OrgTeam, team_id) is None
def test_a_player_cannot(self, app, client, org_team, as_role):
from app.models import OrgTeam
team_id, _coach_id = org_team
as_role('player')
client.post(f'/teams/{team_id}/delete', follow_redirects=False)
with app.app_context():
assert db.session.get(OrgTeam, team_id) is not None