feat(M2): configuration, gestion des secrets et logging
- config/settings.py : modèles Pydantic Settings (Pronote, CalDAV, XMPP, AI, Blog, App) avec SecretStr pour les mots de passe et clés API - config/env.py : fonction load_settings() pour le chargement du .env - utils/redaction.py : redact_url, redact_secrets, redact_exception (masquage des tokens icalsecurise, mots de passe et URLs sensibles) - utils/logging.py : setup_logging + RedactingFormatter (masquage automatique des secrets dans les logs) - utils/uid.py : normalize_pronote_uid (suppression suffixes temporels) et generate_deterministic_uid (hash SHA-1 usedforsecurity=False) - .env.example : aligné sur les modèles finaux (XMPP_TO, XMPP_HOST, XMPP_ENABLED, BLOG_ENABLED, etc.) - .pre-commit-config.yaml : ajout pydantic + pydantic-settings aux additional_dependencies du hook mypy - TODO.md : items M2 cochés Décisions d'architecture (@architect) : - XmppSettings : modèle complet §10.2.3, tous champs optionnels - sync_past_days/future_days déplacés vers AppSettings (sans préfixe) - AISettings.enabled = False par défaut - BlogSettings inclus dès M2 - redact_exception comme fonction module (pas méthode) - normalize_pronote_uid (nom du guide et des tests) Validations : - ruff check : PASS - ruff format --check : PASS - mypy strict : PASS (7 fichiers) - bandit : PASS (0 issue) - import settings : OK (toutes valeurs par défaut) - redact_secrets/icalsecurise : masqué en REDACTED - logging : secret masqué dans la sortie - uid normalize : idempotent, deterministic OK - SecretStr : pas de fuite dans repr - pytest : 0 test (infrastructure OK) Co-authored-by: OpenCode/orchestrator <opencode-orchestrator@agents.invalid>
This commit is contained in:
67
pronote_sync/utils/logging.py
Normal file
67
pronote_sync/utils/logging.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""Configuration de la journalisation avec masquage automatique des secrets.
|
||||
|
||||
Ce module fournit un formateur de logs qui rédige les secrets (tokens, mots
|
||||
de passe, URLs sensibles) ainsi que l'initialisation du logging global du
|
||||
pipeline ``pronote-sync``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from pronote_sync.utils.redaction import redact_secrets
|
||||
|
||||
_LOG_FORMAT = "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
|
||||
_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
|
||||
class RedactingFormatter(logging.Formatter):
|
||||
"""Formateur de logs qui masque les secrets des messages et des arguments."""
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
"""Formate un enregistrement de log en masquant les secrets.
|
||||
|
||||
Le message et chaque argument textuel de l'enregistrement sont rédigés
|
||||
avant le formatage final effectué par :class:`logging.Formatter`.
|
||||
|
||||
:param record: Enregistrement de log à formater.
|
||||
:return: Message formaté, avec les secrets remplacés par ``REDACTED``.
|
||||
:rtype: str
|
||||
"""
|
||||
record.msg = redact_secrets(str(record.msg))
|
||||
args = record.args
|
||||
if args:
|
||||
if isinstance(args, tuple):
|
||||
record.args = tuple(
|
||||
redact_secrets(arg) if isinstance(arg, str) else arg for arg in args
|
||||
)
|
||||
else:
|
||||
record.args = {
|
||||
key: redact_secrets(value) if isinstance(value, str) else value
|
||||
for key, value in args.items()
|
||||
}
|
||||
return super().format(record)
|
||||
|
||||
|
||||
def setup_logging(level: str = "INFO") -> None:
|
||||
"""Configure la journalisation globale avec masquage des secrets.
|
||||
|
||||
Les gestionnaires existants du logger racine sont supprimés, puis un
|
||||
gestionnaire unique écrivant sur ``sys.stdout`` est installé. Les loggers
|
||||
tiers ``urllib3`` et ``slixmpp`` sont ramenés au niveau ``WARNING``.
|
||||
|
||||
:param level: Nom du niveau de log (ex: ``"DEBUG"``, ``"INFO"``) ;
|
||||
les noms inconnus sont ignorés au profit de ``INFO``.
|
||||
:rtype: None
|
||||
"""
|
||||
numeric_level: int = logging.getLevelNamesMapping().get(level.upper(), logging.INFO)
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setFormatter(RedactingFormatter(fmt=_LOG_FORMAT, datefmt=_DATE_FORMAT))
|
||||
root = logging.getLogger()
|
||||
for existing in list(root.handlers):
|
||||
root.removeHandler(existing)
|
||||
root.setLevel(numeric_level)
|
||||
root.addHandler(handler)
|
||||
for noisy_name in ("urllib3", "slixmpp"):
|
||||
logging.getLogger(noisy_name).setLevel(logging.WARNING)
|
||||
Reference in New Issue
Block a user