Aller au contenu

PD-46 — Plan d'implémentation

Story : Implémenter download sécurisé avec pre-signed URLs Date : 2026-02-11 Auteur : Claude Opus 4.5


1. Découpage en composants

Composant Responsabilité Projet Agent
download-controller Endpoint GET /documents/:id/download backend agent-developer
download-service Orchestration vérification droits + génération URL backend agent-developer
document-access-guard Guard unifié pour vérification droits (owner/share/B2B) backend agent-developer
s3-presign-service Génération pre-signed URLs OVH S3 backend agent-sre
audit-download-service Journalisation DOWNLOAD_SUCCESS/DENIED backend agent-developer
share-repository Accès aux partages (is_active, expires_at, revoked_at) backend agent-developer
download.dto DTOs pour requête/réponse backend agent-developer
download.errors Codes d'erreur ERR-46-* backend agent-developer
download.tests Tests unitaires et d'intégration backend agent-qa-unit-integration

2. Flux techniques

Flux 1 — Coffre Personnel (Owner)

sequenceDiagram
    participant Client
    participant Controller
    participant Guard
    participant Service
    participant S3
    participant Audit

    Client->>Controller: GET /documents/:id/download (JWT)
    Controller->>Guard: canActivate(request)
    Guard->>Guard: extractUserId(JWT)
    Guard->>Guard: findDocument(id)
    Guard->>Guard: checkOwnership(userId, document.ownerId)
    Guard-->>Controller: true
    Controller->>Service: generateDownloadUrl(documentId, userId)
    Service->>S3: getSignedUrl(bucket, key, {expiresIn: 300})
    S3-->>Service: presignedUrl
    Service-->>Controller: {url, expiresAt}
    Controller-->>Client: 200 {downloadUrl, expiresAt}
    Client->>S3: GET presignedUrl
    S3-->>Client: document (stream)
    S3->>Audit: S3 Event (objectAccessed)
    Audit->>Audit: logDownloadSuccess(userId, documentId)

Flux 2 — Coffre Personnel (Partage actif)

sequenceDiagram
    participant Client
    participant Guard
    participant ShareRepo
    participant Service

    Client->>Guard: GET /documents/:id/download (JWT)
    Guard->>Guard: checkOwnership() -> false
    Guard->>ShareRepo: findActiveShare(userId, documentId)
    ShareRepo-->>Guard: Share {is_active, expires_at, revoked_at}
    Guard->>Guard: validateShare(share)
    alt Share invalide
        Guard-->>Client: 403 SHARE_REVOKED
    else Share valide
        Guard-->>Service: proceed
    end

Flux 3 — Coffre Entreprise (B2B OIDC)

sequenceDiagram
    participant Partner
    participant Guard
    participant Service

    Partner->>Guard: GET /documents/:id/download (OIDC)
    Guard->>Guard: extractOIDCClaims(token)
    Guard->>Guard: validateTenant(token.tenant_id, document.tenant_id)
    Guard->>Guard: validateRole(token.roles, 'document:download')
    alt Tenant/Role invalide
        Guard-->>Partner: 403 TENANT_MISMATCH / INSUFFICIENT_ROLE
    else Valide
        Guard-->>Service: proceed with tenant_id
    end

3. Mapping invariants → mécanismes

Invariant ID Exigence Mécanisme Composant Observable Risque
INV-46-01 TTL + droits au moment requête TTL=300s dans getSignedUrl + Guard avant génération s3-presign-service, document-access-guard X-Amz-Expires=300, Guard rejette si droits révoqués Faible
INV-46-02 Révocation bloque nouvelles requêtes Guard vérifie share.revoked_at IS NULL document-access-guard HTTP 403 SHARE_REVOKED Faible
INV-46-03 Téléchargement en cours non interrompu Pre-signed URL valide indépendamment du backend s3-presign-service URL reste valide si obtenue avant révocation Accepté
INV-46-04 Journalisation append-only S3 Events → Lambda → AuditService audit-download-service Event DOWNLOAD_SUCCESS/DENIED dans registre Moyen (dépend S3 Events)
INV-46-05 Attribution acteur + PM userId + tenant_id dans event audit-download-service Champs obligatoires dans event audit Faible
INV-46-06 Zero-Knowledge, aucun GetObject backend getSignedUrl() ne lit pas le fichier s3-presign-service Aucun GetObject dans CloudTrail backend Faible

4. Mapping critères d'acceptation → mécanismes

Critère ID Mécanisme(s) Composant Observable Risque
CA-46-01 Guard owner check + presign document-access-guard, s3-presign-service HTTP 200 + URL valide Faible
CA-46-02 Guard share check + presign document-access-guard, share-repository HTTP 200 + URL valide Faible
CA-46-03 Guard reject + error code document-access-guard HTTP 403 FORBIDDEN Faible
CA-46-04 Guard check before presign document-access-guard Rejet si droits révoqués entre génération et download Moyen
CA-46-05 S3 Events → AuditService audit-download-service Event avec user_id, document_id, timestamp Moyen
CA-46-06 S3 Events → AuditService (denied) audit-download-service Event DOWNLOAD_DENIED avec raison Moyen
CA-46-07 expiresIn: 300 dans getSignedUrl s3-presign-service X-Amz-Expires=300 Faible
CA-46-08 URL indépendante après génération Architecture pre-signed Download continue Accepté
CA-46-09 getSignedUrl() sans GetObject s3-presign-service Aucun proxy/buffering Faible

5. Mapping tests (TC-*) → mécanismes + observables

Test ID Référence spec Mécanisme(s) Point(s) d'observation Niveau test
TC-NOM-01 CA-46-01 Guard + presign HTTP 200, URL format Integration
TC-NOM-02 CA-46-02 Guard + ShareRepo HTTP 200, share check Integration
TC-NOM-03 CA-46-01 (B2B) Guard + OIDC claims HTTP 200, tenant_id log Integration
TC-ERR-01 HTTP 401 JwtAuthGuard HTTP 401 UNAUTHORIZED Unit
TC-ERR-02 HTTP 403 Guard ownership HTTP 403 FORBIDDEN Unit
TC-ERR-03 HTTP 403 Guard share.revoked_at HTTP 403 SHARE_REVOKED Unit
TC-ERR-04 HTTP 403 Guard tenant mismatch HTTP 403 TENANT_MISMATCH Unit
TC-ERR-05 HTTP 403 Guard role check HTTP 403 INSUFFICIENT_ROLE Unit
TC-ERR-06 HTTP 404 Document not found HTTP 404 NOT_FOUND Unit
TC-ERR-07 HTTP 410 S3 URL expiration S3 AccessDenied E2E
TC-INV-01 INV-46-01 Guard timing Rejet après révocation Integration
TC-INV-02 INV-46-02 Guard share check Rejet immédiat Unit
TC-INV-03 INV-46-03 URL independance Download continue E2E
TC-INV-04 INV-46-04 AuditService Event dans registre Integration
TC-INV-05 INV-46-05 AuditService fields user_id + tenant_id Unit
TC-ZK-01 INV-46-06 presign sans GetObject CloudTrail analysis E2E/Sec

6. Gestion des erreurs

Code Erreur Condition Observable
ERR-46-001 UNAUTHORIZED Token JWT invalide/expiré HTTP 401
ERR-46-002 FORBIDDEN Non owner + pas de partage HTTP 403
ERR-46-003 SHARE_REVOKED Partage révoqué ou expiré HTTP 403
ERR-46-004 TENANT_MISMATCH Tenant OIDC != document tenant HTTP 403
ERR-46-005 INSUFFICIENT_ROLE Rôle sans permission download HTTP 403
ERR-46-006 NOT_FOUND Document inexistant HTTP 404
ERR-46-007 S3_ERROR Erreur génération URL HTTP 500
ERR-46-008 AUDIT_ERROR Erreur journalisation (non bloquant) Log warning

7. Impacts sécurité

Risque Mitigation Observabilité
URL partagée publiquement TTL 5 minutes limite l'exposition CloudTrail S3 access logs
Révocation non immédiate Fenêtre résiduelle documentée et acceptée Audit log de révocation
Contournement Zero-Knowledge Backend utilise uniquement getSignedUrl, jamais GetObject CloudTrail analysis
Token OIDC falsifié Validation signature Keycloak Logs auth
Escalade de privilèges B2B Vérification tenant_id strict Audit tenant mismatch

Conformité : - RGPD : Minimisation (pas de log du contenu, seulement métadonnées) - NF Z42-013 : Journalisation append-only - Zero-Knowledge : Aucune lecture serveur

8. Hypothèses techniques

ID Hypothèse Impact si faux
H-IMPL-01 OVH Object Storage supporte getSignedUrl avec TTL Vérifier SDK AWS S3 compatible
H-IMPL-02 S3 Events disponibles sur OVH (ou polling CloudTrail) Journalisation retardée si événements non disponibles
H-IMPL-03 Table documents existe avec owner_id, s3_key, tenant_id Adapter le schéma
H-IMPL-04 Table shares existe avec is_active, expires_at, revoked_at Adapter le schéma
H-IMPL-05 Service AuditService existant avec méthode logEvent() Créer si inexistant
H-IMPL-06 Keycloak expose tenant_id et roles dans token OIDC Configurer mapper Keycloak

9. Points de vigilance (risques, dette, pièges)

Point Description Action
S3 Events OVH OVH Object Storage peut ne pas supporter tous les événements S3 natifs Vérifier documentation OVH, prévoir fallback CloudTrail polling
Timing révocation Fenêtre résiduelle entre génération URL et téléchargement Documenté et accepté par PO
Tests E2E Nécessitent bucket S3 réel Prévoir environnement de test avec bucket dédié
Performance getSignedUrl est synchrone et rapide (<10ms) Pas de cache nécessaire
Rate limiting Non inclus dans cette story Prévoir story dédiée si abus détectés

10. Hors périmètre

  • Resume partiel (Range requests)
  • Download progress tracking
  • Quota de téléchargement
  • Notification de téléchargement
  • Téléchargement batch (ZIP)
  • Single-use URLs
  • Restriction IP
  • Support Glacier (restauration asynchrone)

Architecture cible

src/modules/documents/
├── download/
│   ├── download.controller.ts      # Endpoint GET /documents/:id/download
│   ├── download.service.ts         # Orchestration
│   ├── download.dto.ts             # DownloadResponseDto
│   ├── download.errors.ts          # ERR-46-* codes
│   ├── guards/
│   │   └── document-access.guard.ts # Vérification owner/share/tenant/role
│   └── __tests__/
│       ├── download.controller.spec.ts
│       ├── download.service.spec.ts
│       └── document-access.guard.spec.ts
├── storage/
│   └── s3-presign.service.ts       # Génération pre-signed URLs
└── audit/
    └── audit-download.service.ts   # Journalisation événements

# Extensions modules existants
src/modules/shares/
└── share.repository.ts             # findActiveShare(userId, documentId)

Généré par : Claude Opus 4.5 Date : 2026-02-11 Statut : En attente de review (Gate 5)