feat: authentification QR code / token pour Pronote
Ajoute le mode d'authentification PRONOTE_AUTH_MODE=qr_token comme alternative au mode password pour les instances Pronote utilisant HubEduConnect/EduConnect où l'authentification par mot de passe échoue (CAPTCHA, MFA, flux SAML). Nouveaux éléments : - PronoteSettings : auth_mode, qr_code_file, qr_pin (SecretStr) - PronoteAuthState : persistance du token rotatif dans .pronote_auth_state.json (écriture atomique, permissions 0600, symlink-safe via O_EXCL|O_NOFOLLOW) - PronoteClient._connect_qr_token() : token_login avec creds persistés, qrcode_login pour l'enrôlement initial, export_credentials persisté après chaque login réussi - PronoteAuthRotationError : levée en cas d'échec de rotation du token, propagée sans wrapping à travers PronoteFetcher et fetch_step jusqu'à PipelineRunner.run() qui notifie via XMPP (si canal disponible et dry_run inactif) - _is_pronotepy_configured() mode-aware : qr_token ne requiert que PRONOTE_URL - _collect_auth_secrets() : redaction des secrets explicites (token, PIN, jeton QR) dans tous les logs du chemin d'authentification Documentation : - .env.example : PRONOTE_AUTH_MODE, PRONOTE_QR_CODE_FILE, PRONOTE_QR_PIN - AGENTS.md : contrat d'authentification QR code / token - Wiki GuidePronote : section enrôlement, exécutions suivantes, ré-enrôlement Tests (686 passés, couverture 94.87%) : - 5 tests config QR, 9 tests auth_state, 10 tests client QR, 3 tests propagation, 4 tests intégration rotation end-to-end, 4 tests fallback mode-aware - Tests de non-fuite : sentinelles distinctes pour token, PIN, jeton QR Co-authored-by: coder/litellm/coder <coder@agents.invalid>
This commit is contained in:
@@ -23,7 +23,7 @@ from enum import StrEnum
|
||||
from typing import Literal, Protocol
|
||||
|
||||
from pronote_sync.config.settings import Settings
|
||||
from pronote_sync.errors import PipelineCriticalError
|
||||
from pronote_sync.errors import PipelineCriticalError, PronoteAuthRotationError
|
||||
from pronote_sync.models.agenda import Lesson, SchoolEvent
|
||||
from pronote_sync.models.homework import Homework
|
||||
from pronote_sync.models.message import Message
|
||||
@@ -147,13 +147,19 @@ class PronoteFetcher:
|
||||
return self._settings.pronote.ical_url is not None
|
||||
|
||||
def _is_pronotepy_configured(self) -> bool:
|
||||
"""Vérifie que la source pronotepy est entièrement configurée.
|
||||
"""Vérifie si la source pronotepy est utilisable selon le mode d'authentification.
|
||||
|
||||
:return: ``True`` si ``url``, ``username`` et ``password``
|
||||
sont tous définis, ``False`` sinon.
|
||||
:return: ``True`` si pronotepy est configuré pour le mode
|
||||
d'authentification actif, ``False`` sinon.
|
||||
:rtype: bool
|
||||
"""
|
||||
pronote = self._settings.pronote
|
||||
if pronote.auth_mode == "qr_token":
|
||||
# En mode qr_token, seul PRONOTE_URL est requis.
|
||||
# Le QR code et le PIN ne sont nécessaires que pour l'enrôlement initial.
|
||||
# Les exécutions suivantes utilisent le token persisté.
|
||||
return pronote.url is not None
|
||||
# En mode password, URL + identifiant + mot de passe sont requis.
|
||||
return (
|
||||
pronote.url is not None
|
||||
and pronote.username is not None
|
||||
@@ -249,10 +255,14 @@ class PronoteFetcher:
|
||||
:return: Tuple ``(cours, événements scolaires)``.
|
||||
:rtype: tuple[list[Lesson], list[SchoolEvent]]
|
||||
:raises PipelineCriticalError: Si toutes les sources tentées échouent.
|
||||
:raises PronoteAuthRotationError: Si une rotation du token d'authentification
|
||||
pronotepy est nécessaire : propagée telle quelle, sans repli.
|
||||
"""
|
||||
primary, fallback = self._agenda_sources()
|
||||
try:
|
||||
return self._fetch_agenda_source(primary)
|
||||
except PronoteAuthRotationError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"Échec de la récupération %s pour l'agenda : %s",
|
||||
@@ -266,6 +276,8 @@ class PronoteFetcher:
|
||||
logger.info("Repli sur %s pour l'agenda.", fallback)
|
||||
try:
|
||||
lessons, school_events = self._fetch_agenda_source(fallback)
|
||||
except PronoteAuthRotationError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"Échec de la récupération %s pour l'agenda : %s",
|
||||
@@ -370,10 +382,14 @@ class PronoteFetcher:
|
||||
:return: Liste des devoirs.
|
||||
:rtype: list[Homework]
|
||||
:raises PipelineCriticalError: Si toutes les sources tentées échouent.
|
||||
:raises PronoteAuthRotationError: Si une rotation du token d'authentification
|
||||
pronotepy est nécessaire : propagée telle quelle, sans repli.
|
||||
"""
|
||||
primary, fallback = self._homework_sources()
|
||||
try:
|
||||
return self._fetch_homework_source(primary, target_date)
|
||||
except PronoteAuthRotationError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"Échec de la récupération %s pour les devoirs : %s",
|
||||
@@ -387,6 +403,8 @@ class PronoteFetcher:
|
||||
logger.info("Repli sur %s pour les devoirs.", fallback)
|
||||
try:
|
||||
homeworks = self._fetch_homework_source(fallback, target_date)
|
||||
except PronoteAuthRotationError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"Échec de la récupération %s pour les devoirs : %s",
|
||||
|
||||
Reference in New Issue
Block a user