fix(authz): une seule regle pour l'acces coach vers joueur

SEC-AUTHZ-004 et SEC-AUTHZ-005. La meme question -- ce coach peut-il agir
sur ce joueur ? -- recevait cinq reponses differentes selon la route :

  teams.py:add_player_note     verifiait l'appartenance via TeamPlayer
  users.py, 4 routes de notes  ne verifiaient rien au-dela d'isinstance
  contract.py:can_view         interrogeait la colonne heritee coach_id, et
                               traitait un team_id nul comme un joker

Consequences levees
  - tout coach pouvait ecrire une note nominative sur tout joueur du club.
    Ces notes sont visibles par le joueur concerne.
  - tout coach figurant dans OrgTeam.coach_id pouvait lire n'importe quel
    contrat sans equipe rattachee. Or upload_contract laisse team_id nul des
    que le joueur n'appartient a aucune equipe : la condition
    `not self.team_id or ...` ouvrait donc largement.
  - symetriquement, un coach rattache uniquement par la relation
    many-to-many ne voyait aucun contrat.

app/permissions.py
  Premier pas concret vers ARCH-002, sans refonte : un module unique, pas
  une couche de services. coach_org_team_ids() lit la relation m2m ET la
  colonne heritee, donc le deuxieme coach d'une equipe cesse d'etre
  invisible. coach_can_access_player() accorde l'acces si le joueur est sur
  une equipe du coach, ou inscrit a un tryout qu'il gere, ou participant a
  un match de ce tryout.

14 tests, dont deux verifient que les chemins legitimes fonctionnent
toujours : un coach note bien son propre joueur, et voit bien son contrat.

Note : la regle metier retenue -- equipe OU tryout -- est une lecture du
comportement existant, pas une decision produit. Si le club attend autre
chose, c'est desormais un seul endroit a changer.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
GGThed
2026-08-07 20:17:49 -04:00
co-authored by Claude Opus 5
parent 7bf428f9a0
commit afab7070fb
4 changed files with 375 additions and 4 deletions
+18 -4
View File
@@ -28,13 +28,29 @@ class Contract(db.Model):
uploader = db.relationship('User', foreign_keys=[uploaded_by_id])
def can_view(self, user):
"""Whether this user may read the contract and download its files.
Two defects used to sit in the coach branch:
- `not self.team_id` acted as a wildcard, so any coach listed in the
legacy OrgTeam.coach_id column could read every contract with no
team attached — and upload_contract leaves team_id null whenever
the player belongs to no team.
- the lookup went through OrgTeam.coach_id only, so a coach attached
through the many-to-many relationship saw nothing at all.
Access now follows the same rule as everywhere else: the coach and
the player must actually work together.
"""
if user.id == self.player_id:
return True
from app.models.user_model.admin import Admin
from app.models.user_model.manager import Manager
from app.models.user_model.coach import Coach
from app.models.user_model.user import User
from app.models.org_team.org_team import OrgTeam
from app.permissions import coach_can_access_player
if isinstance(user, Admin):
return True
if isinstance(user, Manager):
@@ -42,9 +58,7 @@ class Contract(db.Model):
if player and player.get_org_teams():
return True
if isinstance(user, Coach):
org_team = OrgTeam.query.filter_by(coach_id=user.id).first()
if org_team and (not self.team_id or self.team_id == org_team.id):
return True
return coach_can_access_player(user, self.player_id)
return False
def can_upload_signed(self, user):