Some checks failed
CI/CD Pipeline / pipeline (push) Failing after 3m35s
Trois piliers transparency opérationnelle UnionFlow livrés en série :
S16.A — Métriques Prometheus métier (transparency observabilité)
- AuditTrailService.log() incrémente :
- unionflow.audit.operations{action, entity}
- unionflow.audit.sod_violations{entity}
- application.properties : quarkus.micrometer.export.prometheus.enabled=true,
http-server + jvm binders activés
- Endpoint /q/metrics exposé pour scraping Prometheus K8s
S16.B — Export massif audit-trail (transparency réglementaire BCEAO/ARTCI/CENTIF)
- AuditTrailExportService :
- exportCsv : Apache Commons CSV avec BOM UTF-8 (compat Excel)
- exportXlsx : POI XSSFWorkbook avec header coloré et auto-size
- exportPdf : OpenPDF A4 paysage avec table 7 colonnes
- Endpoint GET /api/audit-trail/export?format=csv|xlsx|pdf&scope=...&limit=500
- @RolesAllowed COMPLIANCE_OFFICER, CONTROLEUR_INTERNE, SUPER_ADMIN
- Auto-log de l'export lui-même dans audit (méta-traçabilité)
S16.C — Notifications transparency (pattern CDI Event Observer)
- AuditOperationLoggedEvent record + helper estSensible() (DELETE / PAYMENT_* / BUDGET_APPROVED / AID_REQUEST_APPROVED / EXPORT / VALIDATE / SoD-violation)
- AuditTrailService.log() fire le CDI event après persist
- AuditNotificationService observe @Observes(AFTER_SUCCESS) — notifie via NotificationService existant (DRY, pas de nouveau client mail)
- Sujets/corps localisés français + priorité automatique (HAUTE pour DELETE/PAYMENT_FAILED/SoD, NORMALE pour confirmations, BASSE pour exports)
- Fail-soft : exception notification ne bloque jamais l'audit principal
Tests (10 nouveaux S16.C, 21/21 cumulé audit)
- onAuditOperation × 5 (sensible/non-sensible/SoD/null/userId-null)
- mapping helpers × 4 (sujet × 8 actions, sodPriority, priorité × 6 cas, corps × 2)
127 lines
5.1 KiB
Java
127 lines
5.1 KiB
Java
package dev.lions.unionflow.server.security;
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import dev.lions.unionflow.server.entity.AuditTrailOperation;
|
|
import dev.lions.unionflow.server.repository.AuditTrailOperationRepository;
|
|
import jakarta.enterprise.context.ApplicationScoped;
|
|
import jakarta.inject.Inject;
|
|
import jakarta.transaction.Transactional;
|
|
import java.time.LocalDateTime;
|
|
import java.util.UUID;
|
|
import org.jboss.logging.Logger;
|
|
|
|
/**
|
|
* Service d'audit trail enrichi (SYSCOHADA + AUDSCGIE OHADA + Instruction BCEAO 003-03-2025).
|
|
*
|
|
* <p>Enregistre toutes les opérations sensibles (financières, lifecycle membres, configurations)
|
|
* avec leur contexte multi-org complet (rôle actif, organisation active, vérifications SoD).
|
|
*
|
|
* <p>Usage typique dans un service métier :
|
|
*
|
|
* <pre>{@code
|
|
* @Inject AuditTrailService auditTrail;
|
|
*
|
|
* public Cotisation enregistrerPaiement(...) {
|
|
* Cotisation c = ...
|
|
* auditTrail.log("Cotisation", c.getId(), "PAYMENT_CONFIRMED",
|
|
* "Paiement confirmé via " + provider, c);
|
|
* return c;
|
|
* }
|
|
* }</pre>
|
|
*
|
|
* @since 2026-04-25
|
|
*/
|
|
@ApplicationScoped
|
|
public class AuditTrailService {
|
|
|
|
private static final Logger LOG = Logger.getLogger(AuditTrailService.class);
|
|
|
|
@Inject AuditTrailOperationRepository repository;
|
|
@Inject OrganisationContextHolder context;
|
|
@Inject io.micrometer.core.instrument.MeterRegistry registry;
|
|
@Inject jakarta.enterprise.event.Event<AuditOperationLoggedEvent> auditEvent;
|
|
|
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
|
/**
|
|
* Enregistre une entrée d'audit trail à partir du contexte courant.
|
|
*
|
|
* @param entityType nom de l'entité (ex: "Cotisation", "Membre", "EcritureComptable")
|
|
* @param entityId UUID de l'entité ciblée (peut être null pour actions globales)
|
|
* @param actionType type d'action (cf. CHECK SQL : CREATE, UPDATE, DELETE, APPROVE, ...)
|
|
* @param description description libre courte (≤ 500 caractères)
|
|
* @param payloadApres entité après modification (sérialisée JSON, peut être null)
|
|
*/
|
|
@Transactional
|
|
public void log(String entityType, UUID entityId, String actionType, String description,
|
|
Object payloadApres) {
|
|
log(entityType, entityId, actionType, description, null, payloadApres, null, null, null);
|
|
}
|
|
|
|
/**
|
|
* Enregistre une entrée d'audit trail avec snapshot avant/après et résultat SoD.
|
|
*/
|
|
@Transactional
|
|
public void log(String entityType, UUID entityId, String actionType, String description,
|
|
Object payloadAvant, Object payloadApres, Object metadata,
|
|
Boolean sodCheckPassed, String sodViolations) {
|
|
try {
|
|
AuditTrailOperation entry = AuditTrailOperation.builder()
|
|
.userId(context.getCurrentUserId())
|
|
.userEmail(context.getCurrentUserEmail())
|
|
.roleActif(context.getRoleActif())
|
|
.organisationActiveId(context.getOrganisationId())
|
|
.actionType(actionType)
|
|
.entityType(entityType)
|
|
.entityId(entityId)
|
|
.description(description)
|
|
.payloadAvant(toJson(payloadAvant))
|
|
.payloadApres(toJson(payloadApres))
|
|
.metadata(toJson(metadata))
|
|
.sodCheckPassed(sodCheckPassed)
|
|
.sodViolations(sodViolations)
|
|
.operationAt(LocalDateTime.now())
|
|
.build();
|
|
repository.persist(entry);
|
|
|
|
// Sprint 16.A — Métriques Prometheus métier (transparency)
|
|
registry.counter("unionflow.audit.operations",
|
|
"action", actionType == null ? "UNKNOWN" : actionType,
|
|
"entity", entityType == null ? "UNKNOWN" : entityType).increment();
|
|
if (Boolean.FALSE.equals(sodCheckPassed)) {
|
|
registry.counter("unionflow.audit.sod_violations",
|
|
"entity", entityType == null ? "UNKNOWN" : entityType).increment();
|
|
}
|
|
|
|
// Sprint 16.C — Fire CDI event pour observers (notifications, etc.)
|
|
auditEvent.fire(new AuditOperationLoggedEvent(
|
|
entry.getId(), entry.getUserId(), entry.getUserEmail(),
|
|
entry.getOrganisationActiveId(), actionType, entityType, entityId,
|
|
description, sodCheckPassed, entry.getOperationAt()));
|
|
} catch (Exception e) {
|
|
// Fail-soft : l'audit trail ne doit jamais bloquer une opération métier.
|
|
// Les violations sont loguées et peuvent être détectées via les logs applicatifs.
|
|
LOG.errorf(e,
|
|
"Audit trail log failed: entityType=%s entityId=%s actionType=%s description=%s",
|
|
entityType, entityId, actionType, description);
|
|
}
|
|
}
|
|
|
|
/** Variante sans payload — pour les actions simples (LOGIN, LOGOUT, EXPORT...). */
|
|
@Transactional
|
|
public void logSimple(String entityType, UUID entityId, String actionType, String description) {
|
|
log(entityType, entityId, actionType, description, null);
|
|
}
|
|
|
|
private String toJson(Object o) {
|
|
if (o == null) return null;
|
|
try {
|
|
return objectMapper.writeValueAsString(o);
|
|
} catch (Exception e) {
|
|
LOG.warnf("Audit trail JSON serialization failed for %s : %s",
|
|
o.getClass().getSimpleName(), e.getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
}
|