Aller au contenu

PD-46 — Spécification canonique contractuelle pour le téléchargement sécurisé des documents

1. Objectif

Permettre le téléchargement sécurisé de documents stockés sur S3, garanti par une intégrité probatoire et soutenu par un mécanisme de pre-signed URLs avec vérification des droits au moment du téléchargement effectif.

2. Périmètre / Hors périmètre

Inclus

  • Sécurisation du téléchargement des documents sur S3 via pre-signed URL
  • Vérification des droits d'accès et permissions aux documents lors de la génération de l'URL et à l'instant exact du téléchargement
  • Journalisation des événements dans un registre probatoire append-only afin de garantir la traçabilité
  • Support pour iOS, PWA et API B2B incluant le modèle d'authentification approprié (JWT ProbatioVault pour iOS/PWA et OAuth 2.0 / OIDC pour l'API B2B)

Exclu

  • Resume partiel (Range requests) — à traiter dans une story dédiée
  • Download progress tracking côté client
  • Quota de téléchargement par utilisateur
  • Notification de téléchargement au owner
  • Téléchargement en batch (ZIP)
  • Génération de lien à usage unique (single-use)
  • Restriction par IP
  • Modification des flux de partage existants

3. Définitions

Terme Définition
JWT ProbatioVault JSON Web Token, mécanisme d'authentification utilisé pour les appareils mobiles et le web
OAuth 2.0 / OIDC Open Authorization 2.0 / OpenID Connect, standard pour l'authentification des systèmes partenaires via API B2B
S3 Simple Storage Service, solution de stockage d'objets sécurisé (AWS/OVH compatible)
Pre-signed URL S3 URL sécurisée et temporaire générée par le backend permettant au client de télécharger directement depuis le stockage objet sans passer par l'application
Tenant Organisation ou entité déployant son propre système d'accès à ProbatioVault
TTL Time To Live, durée de validité d'une URL pre-signed (5 minutes)
WORM Write Once Read Many, mode de stockage immuable

4. Invariants (non négociables)

ID Règle Justification
INV-46-01 La validité d'un téléchargement dépend du TTL ET du maintien du droit d'accès au moment de la requête Le TTL limite la fenêtre temporelle, la vérification des droits garantit l'autorisation
INV-46-02 La révocation d'un droit empêche toute nouvelle requête de téléchargement Évite les accès après modification de droits
INV-46-03 Un téléchargement déjà initié n'est pas interrompu par une révocation ultérieure Évite la frustration utilisateur, fenêtre résiduelle technique acceptée
INV-46-04 Chaque succès ou refus de téléchargement est journalisé dans le registre probatoire append-only Garantit la traçabilité des actions
INV-46-05 L'événement audit est rattaché à un acteur (personne physique) et, le cas échéant, à une personne morale responsable Attribution claire de chaque action
INV-46-06 Aucun téléchargement ne doit permettre de contourner le modèle Zero-Knowledge. Le contenu déchiffré n'est jamais exposé côté serveur. Le backend ne lit pas le contenu du fichier ; il génère uniquement une URL de redirection vers S3. Respect du modèle de sécurité ProbatioVault. Observable : aucune opération GetObject S3 côté backend.

5. Flux nominaux

Flux 1 — Coffre Personnel (Owner)

1. Utilisateur authentifié (JWT) demande une URL de téléchargement via GET /documents/:id/download
2. Backend vérifie : utilisateur = owner du document
3. Backend génère une pre-signed URL S3 (TTL 5 min, GET uniquement)
4. Backend retourne l'URL au client
5. Client télécharge directement depuis S3 via l'URL
6. S3 déclenche un événement (via Lambda ou notification)
7. Backend journalise l'événement DOWNLOAD_SUCCESS dans le registre probatoire

Flux 2 — Coffre Personnel (Partage actif)

1. Utilisateur authentifié (JWT) demande une URL de téléchargement via GET /documents/:id/download
2. Backend vérifie : partage actif existe (lecture autorisée, non expiré, non révoqué)
3. Backend génère une pre-signed URL S3 (TTL 5 min, GET uniquement)
4. Backend retourne l'URL au client
5. Client télécharge directement depuis S3 via l'URL
6. Backend journalise l'événement DOWNLOAD_SUCCESS avec user_id du bénéficiaire

Flux 3 — Coffre Entreprise (API B2B)

1. Partenaire authentifié (OAuth/OIDC) demande une URL de téléchargement via GET /documents/:id/download
2. Backend vérifie : authentification OIDC valide + appartenance au tenant + rôle autorisé
3. Backend génère une pre-signed URL S3 (TTL 5 min, GET uniquement)
4. Backend retourne l'URL au partenaire
5. Partenaire télécharge directement depuis S3 via l'URL
6. Backend journalise l'événement DOWNLOAD_SUCCESS avec user_id + tenant_id

5bis. Diagrammes Mermaid

Diagramme de séquence — Flux nominal de téléchargement sécurisé

Ce diagramme couvre les trois flux (Owner, Partage, B2B). La vérification des droits (étape 2) varie selon le contexte d'accès mais le mécanisme pre-signed URL reste identique.

Invariants illustrés : INV-46-01 (TTL + droits vérifiés), INV-46-04 (journalisation audit), INV-46-06 (zero-knowledge — pas de GetObject backend).

sequenceDiagram
    participant C as Client (iOS/PWA/B2B)
    participant B as Backend
    participant S3 as OVH S3
    participant Audit as Registre Probatoire

    C->>B: GET /documents/:id/download (JWT ou OIDC)
    activate B

    Note over B: Authentification (JWT ProbatioVault / OAuth OIDC)
    Note over B: Vérification droits :<br/>Owner ? Partage actif ? Tenant + rôle ?

    alt Droits valides
        B->>S3: GeneratePresignedUrl(GET, TTL=5min)
        S3-->>B: Pre-signed URL
        B-->>C: HTTP 200 { url, expires_in: 300 }
        deactivate B

        Note over C,S3: INV-46-06 : téléchargement direct Client→S3<br/>Le backend ne lit jamais le contenu

        C->>S3: GET <pre-signed URL>
        activate S3
        S3-->>C: HTTP 200 + contenu chiffré (stream)
        deactivate S3

        S3-)B: S3 Event Notification (download completed)
        B->>Audit: DOWNLOAD_SUCCESS (user_id, document_id, timestamp)
        Note over Audit: INV-46-04 : append-only, INV-46-05 : acteur identifié

    else Droits invalides (révoqué, expiré, non autorisé)
        B-->>C: HTTP 403 FORBIDDEN
        B->>Audit: DOWNLOAD_DENIED (user_id, document_id, reason)
    end

Diagramme d'états — Cycle de vie d'une pre-signed URL

Invariants illustrés : INV-46-01 (TTL + droits conditionnent la validité), INV-46-02 (révocation bloque toute nouvelle requête), INV-46-03 (téléchargement initié non interrompu).

stateDiagram-v2
    [*] --> DroitsVerifies : GET /documents/:id/download

    DroitsVerifies --> URLGeneree : Droits valides → GeneratePresignedUrl
    DroitsVerifies --> Refuse : Droits invalides (HTTP 403)

    URLGeneree --> Telechargement : Client GET <URL> (dans TTL)
    URLGeneree --> Expiree : TTL 5min dépassé (HTTP 410)
    URLGeneree --> Invalide : Droits révoqués entre génération et requête S3

    Telechargement --> Termine : Stream complet → DOWNLOAD_SUCCESS
    Telechargement --> Termine : INV-46-03 — révocation pendant stream,<br/>téléchargement continue

    Refuse --> [*]
    Expiree --> [*]
    Invalide --> [*]
    Termine --> [*]

    note right of Telechargement
        INV-46-06 : flux direct Client↔S3
        Le backend ne touche pas le contenu
    end note

    note right of URLGeneree
        INV-46-01 : validité = TTL ∧ droits maintenus
    end note

6. Cas d'erreur

Code Condition Réponse
HTTP 401 Token JWT invalide ou expiré { "error": "UNAUTHORIZED", "message": "Token invalide ou expiré" }
HTTP 403 Utilisateur non owner ET pas de partage actif { "error": "FORBIDDEN", "message": "Accès au document refusé" }
HTTP 403 Partage révoqué ou expiré { "error": "SHARE_REVOKED", "message": "Le partage a été révoqué ou a expiré" }
HTTP 403 Tenant incorrect (B2B) { "error": "TENANT_MISMATCH", "message": "Le document n'appartient pas à votre organisation" }
HTTP 403 Rôle insuffisant (B2B) { "error": "INSUFFICIENT_ROLE", "message": "Rôle non autorisé pour le téléchargement" }
HTTP 404 Document inexistant { "error": "NOT_FOUND", "message": "Document introuvable" }
HTTP 410 URL pre-signed expirée (TTL dépassé) Réponse S3 native
HTTP 500 Erreur génération URL S3 { "error": "INTERNAL_ERROR", "message": "Erreur lors de la génération du lien" }

7. Critères d'acceptation (testables)

ID Critère Observable
CA-46-01 Un utilisateur owner peut télécharger son document via iOS, PWA ou API B2B Téléchargement réussi, HTTP 200
CA-46-02 Un utilisateur avec partage actif peut télécharger le document partagé Téléchargement réussi, HTTP 200
CA-46-03 Un utilisateur non autorisé reçoit HTTP 403 Réponse FORBIDDEN, téléchargement impossible
CA-46-04 Les droits sont vérifiés au moment du téléchargement effectif Accès bloqué si droits révoqués entre génération URL et téléchargement
CA-46-05 Chaque téléchargement effectif est journalisé avec acteur + document + timestamp Événement présent dans le registre probatoire
CA-46-06 Chaque refus post-révocation est journalisé Événement DOWNLOAD_DENIED présent dans le registre
CA-46-07 Le TTL de 5 minutes est respecté URL inutilisable après 5 minutes (HTTP 410)
CA-46-08 Les téléchargements en cours ne sont pas interrompus par une révocation Téléchargement initié continue jusqu'à complétion
CA-46-09 Le contenu n'est jamais exposé en clair côté serveur Pas de buffering/proxy du fichier par le backend

8. Scénarios de test (Given / When / Then)

Coffre Personnel — Owner

ID Given When Then
TC-CP-01 Un utilisateur est owner d'un document Il demande GET /documents/:id/download Il reçoit une URL pre-signed valide (HTTP 200)
TC-CP-02 Un utilisateur a une URL pre-signed valide Il télécharge depuis S3 dans les 5 minutes Téléchargement réussi
TC-CP-03 Un utilisateur a une URL pre-signed Il attend 6 minutes puis télécharge HTTP 410 (URL expirée)

Coffre Personnel — Partage

ID Given When Then
TC-PS-01 Un utilisateur a un partage actif (lecture, non expiré) Il demande GET /documents/:id/download Il reçoit une URL pre-signed valide (HTTP 200)
TC-PS-02 Un utilisateur a un partage actif Le partage est révoqué pendant qu'il a l'URL Nouvelle requête de téléchargement refusée (HTTP 403)
TC-PS-03 Un utilisateur a un partage expiré Il demande GET /documents/:id/download HTTP 403 (SHARE_REVOKED)

Coffre Entreprise — B2B

ID Given When Then
TC-CE-01 Un partenaire B2B authentifié OIDC avec rôle autorisé Il demande GET /documents/:id/download Il reçoit une URL pre-signed valide (HTTP 200)
TC-CE-02 Un partenaire B2B avec mauvais tenant Il demande GET /documents/:id/download HTTP 403 (TENANT_MISMATCH)
TC-CE-03 Un partenaire B2B sans rôle download Il demande GET /documents/:id/download HTTP 403 (INSUFFICIENT_ROLE)

Journalisation

ID Given When Then
TC-LOG-01 Un téléchargement réussi L'événement est capturé Entrée DOWNLOAD_SUCCESS dans le registre avec user_id, document_id, timestamp
TC-LOG-02 Un téléchargement refusé post-révocation L'événement est capturé Entrée DOWNLOAD_DENIED dans le registre avec raison
TC-LOG-03 Une génération d'URL Aucun événement Pas d'entrée dans le registre (non journalisé)

Révocation en cours de téléchargement

ID Given When Then
TC-REV-01 Un téléchargement est initié Les droits sont révoqués pendant le téléchargement Le téléchargement en cours se termine normalement
TC-REV-02 Un téléchargement est initié Les droits sont révoqués, nouvelle requête faite La nouvelle requête est refusée (HTTP 403)

9. Hypothèses explicites

ID Hypothèse Impact si faux
H-46-01 Le backend peut générer des pre-signed URLs S3 avec TTL configurable Nécessite implémentation SDK AWS/OVH
H-46-02 S3 peut notifier le backend des téléchargements effectifs (Lambda/SNS/SQS) La journalisation pourrait être retardée ou approximative
H-46-03 Le modèle de partage (share) existe et expose is_active, expires_at, revoked_at Nécessite vérification du schéma existant
H-46-04 Le registre probatoire append-only existe et est accessible via service dédié Nécessite vérification de l'API du registre
H-46-05 L'authentification OIDC B2B expose tenant_id et roles dans le token Nécessite vérification du flow Keycloak

10. Points à clarifier

ID Question Impact
Q-46-01 Comment le backend détecte-t-il qu'un téléchargement S3 a effectivement eu lieu ? (Lambda, S3 Events, CloudTrail ?) Architecture du mécanisme de journalisation
Q-46-02 Le TTL de 5 minutes est-il configurable par environnement (dev/staging/prod) ? Flexibilité des tests
Q-46-03 Faut-il supporter le téléchargement de documents archivés (Glacier) avec délai de restauration ? Réponse : Hors périmètre. Les documents archivés (Glacier) nécessitent une story dédiée pour la restauration asynchrone.
Q-46-04 Le bucket S3 cible est-il OVH Object Storage ou AWS S3 ? Réponse : OVH Object Storage (S3-compatible). Bucket principal : probatiovault-documents. Région : GRA (Gravelines). Credentials via HashiCorp Vault.

Références

  • Epic : STORAGE (PD-198)
  • JIRA : PD-46
  • Repos concernés : ProbatioVault-infra, ProbatioVault-backend
  • Documents associés : PD-43 (upload), PD-60 (audit)
  • Normes : NF Z42-013, ISO 14641, RGPD

Généré par : ChatGPT (OpenCode, agent balanced) Date : 2026-02-11 Statut : Corrigé post-Gate 3 (ECT-06, ECT-04) Corrections : - INV-46-06 : Observable renforcé (aucune opération GetObject S3 côté backend) - Q-46-03 : Glacier hors périmètre - Q-46-04 : OVH Object Storage (bucket probatiovault-documents)