Aller au contenu

PD-17 — Plan d'implémentation


Navigation User Story | Document | | | ---------- | -- | | [Spécification](PD-17-specification.md) | | | **Plan d'implémentation** | *(ce document)* | | Critères d'acceptation | *(à venir)* | | Retour d'expérience | *(à venir)* | [Retour à backend-core](../PD-186-epic.md) · [Index User Story](index.md)

Objectif

Implémenter l'extension PD-17 du journal probatoire PD-37 pour tracer toutes les tentatives d'accès (ALLOW/DENY) avec leur contexte, sans créer de schéma concurrent ni altérer les garanties cryptographiques existantes.


1. Découpage en composants

1.1 Composants existants (PD-37) — Non modifiés

Composant Rôle Fichier
AuditLog Entité append-only entities/audit-log.entity.ts
AuditSignatureService Signature HSM services/audit-signature.service.ts
AuditLogService Facade de logging services/audit-log.service.ts
JsonCanonicalizeService RFC 8785 services/json-canonicalize.service.ts

1.2 Composants à créer (PD-17)

Composant Rôle Fichier
AccessAuditService Facade spécialisée accès services/access-audit.service.ts
AccessResult Enum ALLOW/DENY types/access-audit.types.ts
DenyCode Taxonomie codes refus types/access-audit.types.ts
AccessAuditEntry Structure PD-17 types/access-audit.types.ts
AccessAuditGuard Guard NestJS guards/access-audit.guard.ts
AccessAuditInterceptor Interceptor NestJS interceptors/access-audit.interceptor.ts

1.3 Structure fichiers cible

src/modules/audit/
├── services/
│   ├── audit-log.service.ts          # Existant (PD-37)
│   ├── audit-signature.service.ts    # Existant (PD-37)
│   └── access-audit.service.ts       # Nouveau (PD-17)
├── types/
│   ├── audit-action.types.ts         # Existant (PD-37)
│   └── access-audit.types.ts         # Nouveau (PD-17)
├── guards/
│   └── access-audit.guard.ts         # Nouveau (PD-17)
├── interceptors/
│   └── access-audit.interceptor.ts   # Nouveau (PD-17)
└── audit.module.ts                   # Mis à jour

2. Flux techniques

2.1 Flux ALLOW

[Requête HTTP]
[AuthGuard] ─── Authentification ───► OK
[AccessAuditGuard] ─── Vérification autorisation ───► ALLOW
      ├──► AccessAuditService.logAllow(context)
      │         │
      │         ▼
      │    AuditLogService.log({
      │      actionType: 'access.allow',
      │      metadata: { pd: 'PD-17', access_result: 'ALLOW', context: {...} }
      │    })
      │         │
      │         ▼
      │    AuditSignatureService.signAuditEntry()
      │         │
      │         ▼
      │    [HSM Signature] ───► [audit_log INSERT]
[Controller] ─── Exécution métier
[Response]

2.2 Flux DENY

[Requête HTTP]
[AuthGuard] ─── Authentification ───► OK
[AccessAuditGuard] ─── Vérification autorisation ───► DENY
      ├──► AccessAuditService.logDeny(context, denyCode)
      │         │
      │         ▼
      │    AuditLogService.log({
      │      actionType: 'access.deny',
      │      metadata: { pd: 'PD-17', access_result: 'DENY', deny_code: 'ACL_DENY', context: {...} }
      │    })
      │         │
      │         ▼
      │    AuditSignatureService.signAuditEntry()
      │         │
      │         ▼
      │    [HSM Signature] ───► [audit_log INSERT]
[ForbiddenException] ───► HTTP 403

2.3 Flux HSM indisponible

[AccessAuditService.logAllow/logDeny()]
[AuditLogService.log()] ───► [HSM Error]
[AuditSignatureDelayedError] ◄─── Queue BullMQ
[AccessAuditService] ───► throw ServiceUnavailableException
[Guard] ───► DENY access (mode strict)
[HTTP 503]

2bis. Diagrammes Mermaid

2bis.1 Graphe de dépendances des composants

graph TD
    subgraph "PD-17 — Nouveaux composants"
        AAG[AccessAuditGuard]
        AAS[AccessAuditService]
        AAI[AccessAuditInterceptor]
        AAT[AccessAuditEntry / AccessResult / DenyCode]
    end

    subgraph "PD-37 — Composants existants (non modifiés)"
        ALS[AuditLogService]
        ASS[AuditSignatureService]
        ALE[AuditLog Entity]
        JCS[JsonCanonicalizeService]
    end

    subgraph "Externes"
        HSM[HSM PKCS#11]
        DB[(PostgreSQL audit_log)]
        BQ[BullMQ DLQ]
    end

    AAG -->|appelle| AAS
    AAI -.->|alternative| AAS
    AAS -->|utilise types| AAT
    AAS -->|délègue logging| ALS
    ALS -->|sérialise| JCS
    ALS -->|signe| ASS
    ASS -->|signature ECDSA| HSM
    ALS -->|INSERT append-only| ALE
    ALE -->|persiste| DB
    ASS -.->|fallback si HSM down| BQ

2bis.2 Diagramme de séquence — Flux ALLOW

sequenceDiagram
    participant C as Client HTTP
    participant AG as AuthGuard
    participant RG as RolesGuard
    participant AAG as AccessAuditGuard
    participant AAS as AccessAuditService
    participant ALS as AuditLogService
    participant JCS as JsonCanonicalizeService
    participant ASS as AuditSignatureService
    participant HSM as HSM PKCS#11
    participant DB as PostgreSQL

    C->>AG: Requête HTTP
    AG->>AG: Vérification authentification
    AG->>RG: OK
    RG->>RG: Décision autorisation → ALLOW
    RG->>AAG: Contexte + décision ALLOW
    AAG->>AAS: logAllow(context)
    AAS->>AAS: buildPd17Payload('ALLOW', null, context)
    AAS->>ALS: log({ actionType: 'access.allow', metadata: Pd17Payload })
    ALS->>JCS: canonicalize(entry)
    JCS-->>ALS: entry_canonical (RFC 8785)
    ALS->>ASS: signAuditEntry(entry_canonical)
    ASS->>HSM: ECDSA P-256 sign(SHA3-256(entry_canonical))
    HSM-->>ASS: signature
    ASS-->>ALS: signedEntry
    ALS->>DB: INSERT audit_log (append-only)
    DB-->>ALS: OK
    ALS-->>AAS: void
    AAS-->>AAG: void
    AAG->>C: Passe au Controller → Response 200

2bis.3 Diagramme de séquence — Flux DENY

sequenceDiagram
    participant C as Client HTTP
    participant AG as AuthGuard
    participant RG as RolesGuard
    participant AAG as AccessAuditGuard
    participant AAS as AccessAuditService
    participant ALS as AuditLogService
    participant ASS as AuditSignatureService
    participant HSM as HSM PKCS#11
    participant DB as PostgreSQL

    C->>AG: Requête HTTP
    AG->>AG: Vérification authentification
    AG->>RG: OK
    RG->>RG: Décision autorisation → DENY (ACL_DENY)
    RG->>AAG: Contexte + décision DENY
    AAG->>AAS: logDeny(context, 'ACL_DENY')
    AAS->>AAS: buildPd17Payload('DENY', 'ACL_DENY', context)
    AAS->>ALS: log({ actionType: 'access.deny', metadata: Pd17Payload })
    ALS->>ASS: signAuditEntry(entry_canonical)
    ASS->>HSM: ECDSA P-256 sign
    HSM-->>ASS: signature
    ALS->>DB: INSERT audit_log (append-only)
    DB-->>ALS: OK
    ALS-->>AAS: void
    AAS-->>AAG: void
    AAG->>C: ForbiddenException → HTTP 403

2bis.4 Diagramme de séquence — HSM indisponible (mode strict)

sequenceDiagram
    participant C as Client HTTP
    participant AAG as AccessAuditGuard
    participant AAS as AccessAuditService
    participant ALS as AuditLogService
    participant ASS as AuditSignatureService
    participant HSM as HSM PKCS#11
    participant BQ as BullMQ DLQ

    C->>AAG: Requête (ALLOW ou DENY)
    AAG->>AAS: logAllow(context)
    AAS->>ALS: log(...)
    ALS->>ASS: signAuditEntry(entry_canonical)
    ASS->>HSM: ECDSA sign
    HSM--xASS: Connection timeout / Error
    ASS->>BQ: enqueue(unsignedEntry)
    ASS-->>ALS: AuditSignatureDelayedError
    ALS-->>AAS: Error propagée
    AAS->>AAS: Mode strict → throw ServiceUnavailableException
    AAS-->>AAG: ServiceUnavailableException
    AAG->>C: HTTP 503 — Accès refusé

3. Mapping invariants → mécanismes

Invariant (Spec §4) Mécanisme technique Vérification
Unicité du journal AccessAuditService appelle uniquement AuditLogService.log() Pas de nouvel accès direct à audit_log
Primauté PD-37 Aucune modification de AuditLog entity ni migration Review code : 0 changement sur PD-37
Encodage unique dans entry_canonical Données PD-17 dans metadata (sérialisé en entry_canonical) Pas de nouvelle colonne dans migration
Append-only strict Trigger audit_log_immutable existant (PD-37) Test UPDATE/DELETE échoue
Signature obligatoire AuditLogService.log() signe via HSM Aucun bypass possible dans AccessAuditService
Horodatage certifié timestamp géré par PD-37 (new Date() serveur) Horloge NTP synchronisée
Neutralité décisionnelle AccessAuditService reçoit décision, ne la prend pas Aucune règle ACL dans le module

4. Gestion des erreurs

4.1 Matrice des erreurs

Erreur Source Comportement Code HTTP
Métadonnée invalide Validation DTO Rejet avant signature 400
HSM indisponible AuditSignatureService Accès refusé (mode strict) 503
Journal indisponible (DB) AuditLogService Accès refusé 503
DLQ pleine AuditDlqService Log CRITICAL, accès refusé 503
Contexte incomplet AccessAuditService Rejet avant signature 400

4.2 Mode strict (par défaut)

// access-audit.service.ts
async logAllow(context: AccessContext): Promise<void> {
  try {
    await this.auditLogService.log({
      actionType: AuditActionType.ACCESS_ALLOW,
      actorId: context.actorId,
      entityId: context.entityId,
      entityType: context.entityType,
      metadata: this.buildPd17Payload('ALLOW', null, context),
      ipAddress: context.ipAddress,
      userAgent: context.userAgent,
    });
  } catch (error) {
    // Mode strict : accès refusé si log impossible
    this.logger.error(`Access audit failed, denying access: ${error.message}`);
    throw new ServiceUnavailableException('Audit service unavailable');
  }
}

4.3 Codes d'erreur PD-17

Code Description Action
PD17_INVALID_CONTEXT Contexte d'accès incomplet 400 Bad Request
PD17_INVALID_DENY_CODE Code refus hors taxonomie 400 Bad Request
PD17_AUDIT_UNAVAILABLE HSM/DB indisponible 503 Service Unavailable

5. Impacts sécurité

5.1 Risques identifiés

Risque Probabilité Impact Mitigation
Bypass du guard Faible Élevé Guard appliqué globalement + tests
Injection dans metadata Faible Moyen Validation stricte DTO + sanitization
Volumétrie DENY Moyenne Moyen Rate limiting + monitoring
Exposition IP/UA Faible Moyen RLS PostgreSQL + anonymisation si exposé

5.2 Mesures de sécurité

  1. Validation stricte : Toutes les entrées PD-17 validées via class-validator
  2. Sanitization : IP et User-Agent nettoyés avant stockage
  3. Rate limiting : Limite de DENY par IP/utilisateur (configurable)
  4. Audit du guard : Le guard lui-même est audité (méta-audit)
  5. Pas de décision : AccessAuditService ne prend aucune décision d'autorisation

5.3 Données sensibles

Donnée Traitement Justification
actor_id UUID opaque Pas de PII direct
ip_address Stocké dans metadata signé Requis pour traçabilité
user_agent Stocké dans metadata signé Contexte technique
deny_code Code taxonomie fermée Pas de message libre

6. Hypothèses techniques

ID Hypothèse Validation
H-01 PD-37 est déployé et fonctionnel Pré-requis vérifié avant démarrage
H-02 AuditLogService.log() est la seule API de logging Review architecture
H-03 Les guards NestJS s'exécutent avant les contrôleurs Comportement standard NestJS
H-04 Le moteur d'autorisation est externe (PD existant) PD-17 reçoit décision, ne décide pas
H-05 metadata est inclus dans entry_canonical Vérifié dans AuditSignatureService
H-06 La taxonomie DENY est fermée et complète Définie dans spec §10.1
H-07 Performance HSM < 20ms par signature Benchmark PD-37 validé

7. Points de vigilance

7.1 Conformité PD-37

  • NE PAS modifier audit-log.entity.ts
  • NE PAS créer de migration SQL
  • NE PAS ajouter de colonne à audit_log
  • TOUJOURS passer par AuditLogService.log()

7.2 Structure metadata PD-17

Le payload PD-17 DOIT respecter strictement le schéma §5.0 :

interface Pd17Payload {
  pd: 'PD-17';
  access_result: 'ALLOW' | 'DENY';
  deny_code: DenyCode | null;
  context: {
    actor_type: 'USER' | 'SERVICE' | 'SYSTEM';
    actor_id: string;
    entity_type: 'DOCUMENT' | 'ENVELOPE' | 'API';
    entity_id: string;
    ip_address: string;
    user_agent: string;
  };
}

7.3 Ordre d'exécution guards

1. AuthGuard (authentification)
2. RolesGuard (autorisation) ───► Décision ALLOW/DENY
3. AccessAuditGuard ───► Log de la décision

Le AccessAuditGuard doit s'exécuter après la décision mais avant le contrôleur.

7.4 Volumétrie

  • Chaque accès (réussi ou refusé) génère une signature HSM
  • Estimer 2x le volume actuel de signatures
  • Monitorer la latence HSM et la taille du journal

8. Fichiers à créer

Fichier Description
src/modules/audit/types/access-audit.types.ts Types PD-17
src/modules/audit/services/access-audit.service.ts Service facade
src/modules/audit/services/access-audit.service.spec.ts Tests unitaires
src/modules/audit/guards/access-audit.guard.ts Guard NestJS
src/modules/audit/guards/access-audit.guard.spec.ts Tests guard
src/modules/audit/interceptors/access-audit.interceptor.ts Interceptor (optionnel)

9. Fichiers à modifier

Fichier Modification
src/modules/audit/types/audit-action.types.ts Ajout ACCESS_ALLOW, ACCESS_DENY
src/modules/audit/audit.module.ts Export nouveaux providers

10. Ordre d'implémentation

  1. Types PD-17 (access-audit.types.ts)
  2. AccessResult enum
  3. DenyCode enum (taxonomie §10.1)
  4. AccessContext interface
  5. Pd17Payload interface

  6. Ajout AuditActionType (audit-action.types.ts)

  7. ACCESS_ALLOW = 'access.allow'
  8. ACCESS_DENY = 'access.deny'

  9. AccessAuditService (access-audit.service.ts)

  10. logAllow(context)
  11. logDeny(context, denyCode)
  12. buildPd17Payload()
  13. Validation contexte
  14. Mode strict (exception si HSM indisponible)

  15. Tests unitaires AccessAuditService

  16. Mock AuditLogService
  17. Test payload PD-17 conforme
  18. Test mode strict
  19. Test validation contexte

  20. AccessAuditGuard (access-audit.guard.ts)

  21. Récupération contexte depuis request
  22. Appel AccessAuditService
  23. Gestion erreurs

  24. Tests AccessAuditGuard

  25. Mock service
  26. Test flux ALLOW
  27. Test flux DENY
  28. Test HSM unavailable

  29. Update AuditModule

  30. Export AccessAuditService
  31. Export AccessAuditGuard

  32. Tests d'intégration

  33. Flux complet ALLOW → signature HSM → audit_log
  34. Flux complet DENY → signature HSM → audit_log
  35. Vérification payload PD-17 dans entry_canonical

11. Critères de validation

  • Aucune modification de audit-log.entity.ts
  • Aucune migration SQL créée
  • Payload PD-17 conforme au schéma §5.0
  • ALLOW et DENY génèrent des entrées signées HSM
  • Mode strict : accès refusé si HSM indisponible
  • Taxonomie DENY complète et validée
  • Performance < 25ms par log (incluant signature)
  • Tests couvrant tous les cas d'erreur §6

12. Hors périmètre (explicit)

  • Dashboard ou UI de consultation
  • Alerting temps réel
  • Analyse des patterns DENY
  • Mode dégradé (bypass audit)
  • Export des logs PD-17 (utiliser export PD-37)
  • Anonymisation automatique IP/UA

13. Dépendances

Dépendance Version Usage
PD-37 Déployé Journal probatoire signé
AuditLogService PD-37 API de logging
class-validator Existant Validation DTO
NestJS Guards ^10.x Interception requêtes

Annexe A — Taxonomie DENY (spec §10.1)

export enum DenyCode {
  AUTH_INVALID = 'AUTH_INVALID',       // Authentification invalide
  AUTH_EXPIRED = 'AUTH_EXPIRED',       // Session/token expiré
  ACL_DENY = 'ACL_DENY',               // ACL refuse l'accès
  OWNER_MISMATCH = 'OWNER_MISMATCH',   // Propriétaire différent
  QUOTA_EXCEEDED = 'QUOTA_EXCEEDED',   // Quota dépassé
  SYSTEM_LOCKED = 'SYSTEM_LOCKED',     // Système verrouillé
  RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND', // Ressource inexistante
  PERMISSION_DENIED = 'PERMISSION_DENIED',   // Permission refusée
}

Annexe B — Exemple payload entry_canonical

{
  "actorId": "550e8400-e29b-41d4-a716-446655440000",
  "actionType": "access.deny",
  "entityId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "entityType": "DOCUMENT",
  "metadata": {
    "pd": "PD-17",
    "access_result": "DENY",
    "deny_code": "ACL_DENY",
    "context": {
      "actor_type": "USER",
      "actor_id": "550e8400-e29b-41d4-a716-446655440000",
      "entity_type": "DOCUMENT",
      "entity_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
      "ip_address": "192.168.1.100",
      "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
    }
  },
  "timestamp": "2025-12-22T14:30:00.000Z"
}

Ce JSON sera canonicalisé RFC 8785, hashé SHA3-256, et signé ECDSA P-256 par le HSM, exactement comme tout événement PD-37.


14. Prohibitions de sécurité (PD-17 audit)

Cette section documente les contre-mesures de sécurité ajoutées suite à l'audit du plan d'implémentation.

Risque identifié: L'extension dblink permettrait au rôle applicatif d'exécuter des requêtes arbitraires via transactions autonomes, contournant la protection append-only.

Contre-mesures:

  • Migration BlockDblinkExtension : supprime dblink si présent, révoque CREATE sur schemas
  • Fonction vault_secure.check_no_dblink() : vérification au démarrage applicatif
  • Service AuditSecurityMonitorService.verifyNoDblinkExtension() : échec fatal si dblink détecté

Fichiers:

  • src/database/migrations/1733700300000-BlockDblinkExtension.ts
  • src/modules/audit/services/audit-security-monitor.service.ts

14.2 Traçage violations via pg_notify (MAJEUR)

Risque identifié: Sans transactions autonomes, les violations UPDATE/DELETE ne seraient pas tracées car le rollback annule tout.

Contre-mesures:

  • Trigger audit_log_immutable modifié : envoie pg_notify('audit_violation', ...) AVANT l'exception
  • pg_notify survit au rollback de transaction (canal asynchrone)
  • Service AuditViolationListenerService : écoute les notifications et crée des entrées signées HSM

Fichiers:

  • src/database/migrations/1733700500000-UpdateAuditLogTriggerPgNotify.ts
  • src/modules/audit/services/audit-violation-listener.service.ts

14.3 Validation acteur multi-couches (MAJEUR)

Risque identifié: actor_id validé uniquement par soft checks applicatifs, contournable si service bypassé.

Contre-mesures:

  • CHECK constraint chk_audit_log_actor_id_format : validation format UUID
  • Trigger trg_validate_actor : vérifie existence USER dans vault_secure.users à INSERT
  • Pas de FK (préserve WORM : suppression utilisateur ne cascade pas)

Fichiers:

  • src/database/migrations/1733700400000-AddActorValidation.ts

14.4 Protection DDL/TRUNCATE (MINEUR)

Risque identifié: Protection append-only ne couvre pas DDL/TRUNCATE par rôles privilégiés.

Contre-mesures:

  • Schema audit_ddl avec table ddl_log pour tracer DDL
  • Event trigger audit_ddl_commands sur DDL avec alerte pg_notify('security_ddl_alert', ...)
  • Fonction vault_secure.check_security_invariants() : vérifie session_replication_role, triggers, etc.
  • Service AuditSecurityMonitorService : cron minute pour vérifications périodiques

Fichiers:

  • src/database/migrations/1733700600000-CreateDdlAuditSchema.ts
  • src/modules/audit/services/audit-security-monitor.service.ts

14.5 Nouveaux types d'action

// src/modules/audit/types/audit-action.types.ts
export enum AuditActionType {
  // ... existing types ...

  // PD-17: Security monitoring events
  AUDIT_VIOLATION_DETECTED = 'audit.violation.detected',
  SECURITY_INVARIANT_VIOLATION = 'security.invariant.violation',
  DDL_OPERATION_DETECTED = 'security.ddl.detected',

  // PD-17: Access logging events
  ACCESS_ALLOW = 'access.allow',
  ACCESS_DENY = 'access.deny',
}

14.6 Critères de validation sécurité

  • SELECT * FROM pg_extension WHERE extname = 'dblink' retourne vide
  • Application refuse de démarrer si dblink détecté
  • Tentative UPDATE/DELETE sur audit_log génère entrée signée HSM (type audit.violation.detected)
  • actor_id invalide rejette INSERT avec exception foreign_key_violation
  • DDL sur vault_secure génère alerte pg_notify('security_ddl_alert', ...)
  • session_replication_role != 'origin' déclenche alerte CRITICAL