feat(obs): journaliser les evenements d'authentification
OBS-001. logging_config.py configurait un fichier auth.log avec rotation,
un logger nomme 'team_tryouts.auth' et un filtre de redaction. Mais
get_auth_logger n'etait importe nulle part : le fichier etait cree et
restait vide. Aucune connexion, aucun echec, aucun verrouillage, aucun
changement de role, aucune suppression de compte ne laissait de trace.
En cas de suspicion de compromission, il n'y avait rien a consulter.
Ajout de log_auth_event(event, **fields), qui emet des paires cle=valeur
ordonnees -- greppable sans dependance de journalisation JSON.
Evenements couverts
authentification login.success, login.failure,
login.failure.unknown_user, login.rejected.locked,
login.rejected.deactivated, account.locked, logout,
account.registered
administration account.created_by_admin, account.updated,
account.role_changed (avec ancien et nouveau role),
account.deleted, account.password_reset_by_admin
libre-service account.password_changed
Le champ ip vient de request.remote_addr, donc de X-Forwarded-For. Tant que
Waitress tourne avec trusted_proxy='*' (SEC-WEB-002), cette valeur est
choisie par l'appelant : c'est une indication, pas une preuve. Le point est
documente dans la docstring.
Un test a fait remonter ARCH-003, jusqu'ici classe comme fragilite latente
Journaliser en fin de edit_user levait DetachedInstanceError : le
changement de role appelle db.session.remove() en plein cycle de requete,
ce qui detache current_user de la session. Le code s'en tirait parce qu'il
redirigeait immediatement sans plus y toucher. L'identite de l'acteur est
desormais capturee en debut de traitement. Le constat est donc confirme
comme reel, et non plus seulement probable -- sa correction de fond reste
au programme.
15 tests, dont 4 sur le filtre de redaction lui-meme : c'est un controle de
securite, il doit etre verifie.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+26
-1
@@ -24,6 +24,7 @@ from app.validators import (
|
||||
CreateUserSchema, EditUserSchema, EditProfileSchema,
|
||||
UploadContractSchema,
|
||||
)
|
||||
from app.logging_config import log_auth_event
|
||||
import requests
|
||||
|
||||
ALLOWED_CONTRACT_EXTENSIONS = {'pdf'}
|
||||
@@ -128,6 +129,11 @@ def edit_user(user_id):
|
||||
user = User.query.get_or_404(user_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
# Captured up front, on purpose. A role change below calls
|
||||
# db.session.remove(), which detaches current_user from the session:
|
||||
# any later attribute access on it raises DetachedInstanceError.
|
||||
actor_name, actor_id = current_user.username, current_user.id
|
||||
|
||||
def _rerender():
|
||||
return render_template('pages/edit_user.html', user=user, roles=USER_TYPES,
|
||||
esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS,
|
||||
@@ -165,6 +171,10 @@ def edit_user(user_id):
|
||||
# change invalidates the identity map for this instance and anything
|
||||
# that references it via relationships.
|
||||
if user.role != role:
|
||||
log_auth_event('account.role_changed',
|
||||
actor=actor_name, actor_id=actor_id,
|
||||
target=user.username, target_id=user.id,
|
||||
previous_role=user.role, new_role=role)
|
||||
user_id_local = user.id
|
||||
db.session.execute(
|
||||
db.text("UPDATE users SET role = :role WHERE id = :id"),
|
||||
@@ -190,8 +200,14 @@ def edit_user(user_id):
|
||||
password = validated.get('password')
|
||||
if password:
|
||||
user.password_hash = hash_password(password)
|
||||
log_auth_event('account.password_reset_by_admin',
|
||||
actor=actor_name, actor_id=actor_id,
|
||||
target=user.username, target_id=user.id)
|
||||
|
||||
db.session.commit()
|
||||
log_auth_event('account.updated',
|
||||
actor=actor_name, actor_id=actor_id,
|
||||
target=user.username, target_id=user.id, active=is_active)
|
||||
flash(f'User {user.username} updated successfully!', 'success')
|
||||
return redirect(url_for('users.list_users'))
|
||||
|
||||
@@ -242,9 +258,13 @@ def delete_user(user_id):
|
||||
OrgTeam.query.filter_by(created_by=user_id).update({'created_by': current_user.id})
|
||||
Contract.query.filter_by(uploaded_by_id=user_id).update({'uploaded_by_id': current_user.id})
|
||||
|
||||
deleted_username, deleted_role = user.username, user.role
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
flash(f'User {user.username} has been removed.', 'success')
|
||||
log_auth_event('account.deleted',
|
||||
actor=current_user.username, actor_id=current_user.id,
|
||||
target=deleted_username, target_id=user_id, role=deleted_role)
|
||||
flash(f'User {deleted_username} has been removed.', 'success')
|
||||
return redirect(url_for('users.list_users'))
|
||||
|
||||
|
||||
@@ -289,6 +309,9 @@ def create_user():
|
||||
)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
log_auth_event('account.created_by_admin',
|
||||
actor=current_user.username, actor_id=current_user.id,
|
||||
target=user.username, target_id=user.id, role=role)
|
||||
flash(f'User {full_name} created as {role}!', 'success')
|
||||
return redirect(url_for('users.list_users'))
|
||||
|
||||
@@ -373,6 +396,8 @@ def edit_profile():
|
||||
password = validated.get('password')
|
||||
if password:
|
||||
current_user.password_hash = hash_password(password)
|
||||
log_auth_event('account.password_changed',
|
||||
username=current_user.username, user_id=current_user.id)
|
||||
|
||||
db.session.commit()
|
||||
flash('Profile updated successfully!', 'success')
|
||||
|
||||
Reference in New Issue
Block a user