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]>
65 lines
2.7 KiB
Python
65 lines
2.7 KiB
Python
"""Contract documents for players to sign."""
|
|
from app.extensions import db
|
|
from datetime import datetime
|
|
|
|
|
|
class Contract(db.Model):
|
|
"""Contract documents for players to sign."""
|
|
__tablename__ = 'contracts'
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
|
team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=True)
|
|
uploaded_by_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
|
|
|
original_filename = db.Column(db.String(255), nullable=False)
|
|
stored_filename = db.Column(db.String(255), nullable=False)
|
|
file_path = db.Column(db.String(500), nullable=False)
|
|
|
|
signed_filename = db.Column(db.String(255), nullable=True)
|
|
signed_file_path = db.Column(db.String(500), nullable=True)
|
|
|
|
status = db.Column(db.String(20), default='pending')
|
|
notes = db.Column(db.Text, nullable=True)
|
|
uploaded_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
signed_at = db.Column(db.DateTime, nullable=True)
|
|
|
|
player = db.relationship('User', foreign_keys=[player_id], backref='contracts')
|
|
team = db.relationship('OrgTeam', foreign_keys=[team_id])
|
|
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.permissions import coach_can_access_player
|
|
|
|
if isinstance(user, Admin):
|
|
return True
|
|
if isinstance(user, Manager):
|
|
player = User.query.get(self.player_id)
|
|
if player and player.get_org_teams():
|
|
return True
|
|
if isinstance(user, Coach):
|
|
return coach_can_access_player(user, self.player_id)
|
|
return False
|
|
|
|
def can_upload_signed(self, user):
|
|
return user.id == self.player_id |