Aller au contenu

Story CP-001 : TenantContext + Fondation Auth

Module : control-plane Slice : Slice 1 from architecture Brand context : [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority : 1 Depends on : Aucune Estimated complexity : L


Objectif

Mettre en place l'infrastructure multi-tenant fondamentale dont dépendent tous les autres slices. Ce slice implémente le mécanisme de propagation du TenantContext (via Java 25 ScopedValue), la chaîne de filtres Spring Security pour la validation JWT avec Keycloak, et la configuration dual DataSource qui route les requêtes vers la base de données plateforme ou la base de données tenant. Sans ce slice, aucun code tenant-aware ne peut exister.


Périmètre Backend

Entités & Records

Aucune entité métier dans ce slice. Il s'agit uniquement d'infrastructure.

Records/Classes à créer :

// TenantContext — immutable value object carried via ScopedValue
public record TenantContext(
    UUID tenantId,
    String countryCode,
    String dbProfile  // "SHARED" | "SCHEMA" | "DB_PER_TENANT" | "BYO_DB"
) {}

Couche Infrastructure

Propagation du TenantContext : - TenantContextHolder.java — Holder statique ScopedValue. Fournit l'accesseur current(). Utilise un adaptateur ThreadLocal en fallback pour les contextes où ScopedValue ne se propage pas (ex. @Async).

Filtres de sécurité (ordonnés) : 1. TenantResolutionFilter.java — Filtre Spring Security. Extrait tenant_id, country_code depuis les claims JWT. Résout le profil DB depuis la base plateforme. Définit ScopedValue pour le scope de la requête. 2. JwtTenantExtractor.java — Utilitaire pour extraire les claims tenant depuis le JWT Keycloak. Claims JWT attendus : user_id (UUID), tenant_id (UUID), country_code (ISO), roles[] (noms de rôles), iat, exp. 3. TenantMdcFilter.java — Définit tenant_id et user_id dans le MDC SLF4J pour les logs structurés.

Configuration DataSource : - DataSourceConfig.java — Configure deux DataSources nommés : - platformDataSource — connexion à la base de données plateforme (tenants, utilisateurs, facturation, configuration pays) - tenantDataSource — connexion à la base de données tenant (employés, unités organisationnelles, entrées d'audit, données métier) - MVP : les deux pointent sur la même instance PostgreSQL, schémas différents ou même schéma selon le profil - TenantDataSourceRouter.java — Pour le profil C (DB-per-tenant) : résout dynamiquement la connexion à la base de données du tenant depuis tenant.db_connection_url en base plateforme. Utilise un cache de pool de connexions (HikariCP) avec éviction.

Configuration de sécurité : - SecurityConfig.java — Configuration Spring Security 6.x : - Session stateless (pas de cookies) - Serveur de ressources JWT avec validation de clé publique Keycloak - Endpoints publics : /api/v1/onboarding/**, /api/v1/auth/login, /actuator/health - Tous les autres endpoints requièrent un JWT valide - CORS configuré pour les origines du frontend Papillon + console Admin

Configuration applicative : - application.yml — URI émetteur Keycloak, validation JWT, propriétés de connexion dual DataSource, surcharges spécifiques par profil

Couche Repository

  • TenantAwareRepository.java — Classe/interface abstraite de base. Tous les repositories tenant-scoped DOIVENT étendre celle-ci. Injecte automatiquement tenant_id depuis TenantContext dans chaque requête. Utilise @Query Spring Data JDBC avec le binding :tenantId.

Règles de validation

  • JWT obligatoire sur tous les endpoints non-publics (401 si absent)
  • Le claim tenant_id du JWT doit référencer un tenant existant en base plateforme (403 si introuvable)
  • Signature JWT validée contre la clé publique Keycloak (mise en cache localement, rafraîchie à la rotation de clé)
  • Expiration du token d'accès : 15 minutes. Refresh token : 7 jours.

Considérations multi-tenant

C'est la fondation multi-tenant. Décisions de conception clés : - ScopedValue est immutable une fois défini — empêche les fuites accidentelles de contexte tenant - Le pattern TenantAwareRepository rend impossible l'oubli du scope tenant - Le dual DataSource assure la séparation logique des données plateforme et tenant, même si physiquement sur la même DB (MVP) - Le routage profil C utilise un cache de pool avec max 100 connexions par tenant, timeout d'inactivité 10 min


Périmètre Frontend

Aucun frontend dans ce slice. Il s'agit d'une story d'infrastructure backend.

Pages & Routes

N/A

Composants

N/A

États UI (TOUS REQUIS)

N/A — Backend uniquement.

Comportement responsive

N/A

Interactions

N/A


Critères d'acceptation

AC-001: Tenant context propagation
Given a valid JWT with tenant_id="abc-123" and country_code="CI"
When any downstream service method accesses TenantContextHolder.current()
Then it receives TenantContext(tenantId="abc-123", countryCode="CI", dbProfile="SHARED")

AC-002: Missing JWT rejection
Given a request to /api/v1/employees without a JWT
When the request reaches the security filter chain
Then the response is HTTP 401 with body {"error": "UNAUTHORIZED", "message": "Authentication required"}

AC-003: Invalid tenant rejection
Given a valid JWT with tenant_id="nonexistent-uuid"
When the tenant resolution filter queries the platform DB
Then the response is HTTP 403 with body {"error": "TENANT_NOT_FOUND"}

AC-004: MDC logging includes tenant
Given a valid JWT request is processed
When any log statement is emitted during request processing
Then the log entry includes tenant_id and user_id in the MDC context

AC-005: Dual DataSource configuration
Given the application starts with platform and tenant DataSource configurations
When a repository marked as @PlatformScoped queries data
Then the query executes against the platform DataSource
And when a TenantAwareRepository queries data
Then the query executes against the tenant DataSource resolved for the current tenant

AC-006: Public endpoints accessible without JWT
Given no JWT is provided
When a request is made to /api/v1/onboarding/register
Then the request is not rejected by the security filter (authentication is not required)

AC-007: TenantAwareRepository enforces scoping
Given a repository extending TenantAwareRepository
When a query is executed
Then the query automatically includes WHERE tenant_id = :currentTenantId
And it is impossible to query data for a different tenant

Règles OHADA & Réglementaires

  • L'isolation des données tenant est existentielle : Une fuite de données inter-tenant (ex. une entreprise voyant les salaires d'une autre) constituerait une violation RGPD/Loi 2013-450 ET détruirait la confiance des clients. Le pattern TenantAwareRepository doit rendre cela impossible.
  • Loi n° 2013-450 (Art. 36) : Minimisation des données — le JWT doit uniquement porter les claims nécessaires à l'autorisation (user_id, tenant_id, country_code, roles), pas de données personnelles.

Standards & Conventions

  • docs/standards/java-spring-guidelines.md — §Spring Modulith boundaries, §Records & sealed types, §ScopedValue pattern
  • docs/standards/database-guidelines.md — §Multi-tenant repository pattern, §Dual DataSource, §Connection pooling
  • docs/standards/api-guidelines.md — §Authentication, §Error response format

Exigences de test

Tests unitaires

  • TenantContextHolder : cycle de vie set/get/clear
  • JwtTenantExtractor : parsing JWT valide, gestion des claims manquants, JWT malformé
  • TenantDataSourceRouter : résolution correcte du DataSource pour les profils A et C

Tests d'intégration

  • Flux complet de requête : JWT → TenantResolutionFilter → TenantContext défini → requête repository scopée
  • Isolation tenant : créer des données pour tenant A, requête avec JWT tenant B → zéro résultats
  • Accès aux endpoints publics sans JWT
  • Rejet de JWT expiré
  • Requêtes concurrentes avec JWT de différents tenants (vérifier l'absence de fuite de contexte entre virtual threads)

Ce que le QA validera

  • Le QA ne peut pas tester directement ce slice (pas d'UI). Le QA valide l'isolation tenant indirectement via tous les slices suivants.
  • Le DevOps valide : l'application démarre correctement, se connecte à Keycloak, le health check passe.

Hors périmètre

  • Entité User et rôles — c'est CP-003
  • Entité Tenant et CRUD — c'est CP-002
  • Configuration du realm Keycloak — tâche infrastructure/DevOps, pas du code applicatif. Supposer que Keycloak tourne et est configuré.
  • Rotation du refresh token — géré par Keycloak, pas le code applicatif
  • Rate limiting — V2

Définition de Done

  • [ ] Tous les endpoints backend implémentés et retournant les réponses correctes
  • [ ] Toutes les pages frontend s'affichent correctement à 360px, 768px, 1024px — N/A (backend uniquement)
  • [ ] Les 5 états UI implémentés (loading, empty, error, offline, success) — N/A
  • [ ] Les micro-textes français correspondent exactement à la spec design — N/A
  • [ ] Tous les critères d'acceptation passent manuellement
  • [ ] Tests unitaires écrits et passants
  • [ ] Tests d'intégration écrits et passants
  • [ ] Isolation multi-tenant vérifiée (aucune fuite de données inter-tenant)
  • [ ] Écriture hors ligne + sync fonctionnelle (Papillon) / N/A (ALTARYS Enterprise) — N/A (backend uniquement)
  • [ ] Pas de any TypeScript — tous les types explicites — N/A
  • [ ] Pas de types Java bruts — tous les génériques explicites
  • [ ] Les réponses API respectent le format d'enveloppe standard
  • [ ] Entrées d'audit créées pour chaque mutation de données — N/A (aucune mutation dans ce slice)