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)