- 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>
66 lines
2.4 KiB
Python
66 lines
2.4 KiB
Python
"""Utilitaires de masquage des secrets dans les URLs, textes et exceptions.
|
|
|
|
Ce module centralise la rédaction des données sensibles (tokens, mots de
|
|
passe, clés d'accès) afin qu'aucun secret ne soit exposé dans les logs,
|
|
les messages d'erreur ou les traces du pipeline ``pronote-sync``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
|
|
|
_SENSITIVE_QUERY_KEYS = frozenset({"icalsecurise", "token", "key", "password", "secret"})
|
|
_URL_PATTERN = re.compile(r"https?://[^\s]+")
|
|
_ISOLATED_SECRET_PATTERN = re.compile(
|
|
r"\b(icalsecurise|token|password|secret|key)\s*=\s*[^\s&]+",
|
|
re.IGNORECASE,
|
|
)
|
|
_REDACTED = "REDACTED"
|
|
_REDACTED_URL = "REDACTED_URL"
|
|
|
|
|
|
def redact_url(url: str) -> str:
|
|
"""Masque les paramètres sensibles dans une URL.
|
|
|
|
:param url: URL pouvant contenir des paramètres sensibles (ex: ``icalsecurise``).
|
|
:return: URL avec les paramètres sensibles remplacés par ``REDACTED``,
|
|
ou ``REDACTED_URL`` si le traitement échoue.
|
|
:rtype: str
|
|
"""
|
|
try:
|
|
parts = urlsplit(url)
|
|
query: list[tuple[str, str]] = parse_qsl(parts.query, keep_blank_values=True)
|
|
redacted_query = [
|
|
(key, _REDACTED if key.lower() in _SENSITIVE_QUERY_KEYS else value)
|
|
for key, value in query
|
|
]
|
|
return urlunsplit(parts._replace(query=urlencode(redacted_query, doseq=True)))
|
|
except Exception:
|
|
return _REDACTED_URL
|
|
|
|
|
|
def redact_secrets(text: str) -> str:
|
|
"""Masque les secrets présents dans un texte arbitraire.
|
|
|
|
Les URLs sont d'abord traitées par :func:`redact_url`, puis les affectations
|
|
isolées de type ``cle=valeur`` (ex: ``icalsecurise=XXX``) sont masquées,
|
|
sans distinction de casse.
|
|
|
|
:param text: Texte pouvant contenir des URLs ou des secrets en clair.
|
|
:return: Texte avec les secrets remplacés par ``REDACTED``.
|
|
:rtype: str
|
|
"""
|
|
redacted = _URL_PATTERN.sub(lambda match: redact_url(match.group(0)), text)
|
|
return _ISOLATED_SECRET_PATTERN.sub(r"\1=REDACTED", redacted)
|
|
|
|
|
|
def redact_exception(exc: Exception) -> str:
|
|
"""Masque les secrets dans la représentation textuelle d'une exception.
|
|
|
|
:param exc: Exception dont le message doit être rédigé.
|
|
:return: Représentation textuelle de l'exception avec les secrets masqués.
|
|
:rtype: str
|
|
"""
|
|
return redact_secrets(str(exc))
|