Story TENANT-007: BYO-DB provisioning — resolver branch, Flyway-on-BYO & credential encryption¶
Module: tenant-admin Slice: S3b (Vertical Slice Decomposition §11) Side: [ADMIN] + [BACKEND] Version target: [V0.0.4] Priority: 7 Depends on: TENANT-004 Can develop concurrently with: TENANT-005, TENANT-008 Merge order: After TENANT-004 Estimated complexity: L PRD User Stories: TENANT-US-03 Wireframe: docs/wireframes/tenant-admin/TENANT-007/ (design ref ADM-002 étape 3 BYO-DB block + ProvisioningPipeline BYO failures)
Objective¶
Activate the BYO database profile for greenfield enterprise onboarding (NSIA production go-live target). A platform admin can mark a tenant as BYO-DB at creation and supply its PostgreSQL connection; provisioning then validates the connection, Flyway-migrates the (identical) tenant schema into that DB, runs a tenant-scoping smoke test, and stores the credentials envelope-encrypted. The datasource resolver (wired inert since TENANT-001) gains its real BYO branch. Greenfield only — migrating an existing SHARED tenant to BYO is out of V0 (deferred to V2). AUTH user tables stay on the platform DB regardless (AUTH-ADR-01).
Backend Scope¶
Entities¶
Uses the BYO columns already present on tenant (created Day 1 in TENANT-002): byo_db, byo_db_jdbc_url, byo_db_username, byo_db_password_ciphertext, byo_db_pool_size. No new tables.
Migrations¶
None new. This story proves the existing db/migration/tenant/* set applies identically to a BYO DB (no dialect drift; same schema as SHARED).
Service Layer¶
TenantDataSourceRouterBYO branch:getConnection(tenantId)returns the per-tenant BYO datasource whentenant.byo_db = true, else SHARED. BYO pools cached per-tenant in aConcurrentHashMapkeyed bytenantId. This is the ONLY place (with the onboarding service) that knows thebyo_dbflag — all downstream code still seesgetConnection(tenantId).ByoDbProvisioner(BYO_DB provisioning step, previously a no-op in TENANT-004):- (a)
SELECT 1connection test → failBYO_DB_UNREACHABLE; - (b) Flyway baseline + migrate the tenant schema → fail
BYO_DB_MIGRATION; - (c) tenant_id-scoping smoke test (insert + read + delete on a fixture) → fail
BYO_DB_SMOKE. On any failure the tenant stays PENDING with a Frenchlast_provisioning_error; the step is idempotent and re-runnable (AC-02.2). Encryptor.envelope(plaintext)for BYO credentials: KEK in env (PAPILLON_KEK_BASE64) / hosted KMS in prod; per-tenant DEK encrypted by KEK; ciphertext =version‖nonce‖tag‖ctpacked inBYTEA. Plaintext password NEVER logged, NEVER returned to the UI (API returns"***"), NEVER in any audit text.TenantConfigRepositoryis the single chokepoint that reads/decrypts BYO credentials and audits each access.
API Endpoints¶
Extends existing endpoints (no new paths):
- POST /api/control/tenants accepts the BYO block {byoDb:true, jdbcUrl, dbUsername, dbPassword, poolSize} when ticked; all four become required.
- POST /api/control/tenants/{id}/provision now runs the real BYO_DB step for BYO tenants.
- Detail GET returns byoDbPassword: "***" (never the real value).
Validation Rules¶
- When
byo_db = true:jdbc_url,db_username,db_password,pool_sizeall required (AC-03.1). jdbc_urlmust be a postgresql JDBC URL.
Multi-Tenant Considerations¶
A BYO tenant's tenant-scoped rows (agency, tenant_settings, future contracts) live in their DB; the tenant row stays on the platform DB (routing needs it before resolving). No code path diverges past TenantAwareDataSource (NFR-08). Greenfield only — no SHARED→BYO data migration (V2). SCHEMA/DATABASE profiles remain V3; the resolver stays agnostic to the number of datasources.
Audit & Logging¶
- BYO credential access audited via
TenantConfigRepository.TENANT_PROVISIONEDcovers successful BYO activation. - Logback
MaskingPatternConverterredacts JDBC URLs containingpassword=(Technical Risk R2); an integration test attempts to log and asserts the secret is absent.
Frontend Scope¶
Pages & Routes¶
Extends the minimal create form (/admin/tenants/nouveau) with an isolation block, and the provisioning detail (/admin/tenants/:id) ProvisioningPipeline with BYO step states.
Components¶
Reuse Input (incl. type="password" with show/hide toggle), radio group, InlineBanner, ProvisioningPipeline.
UI States (ALL REQUIRED)¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | BYO step shows « Test de connexion… {n} s »; no UI timeout before 60 s (NFR-03) | « ⏳ 2. Base de données (BYO) — En cours… » |
| Empty | No run yet | « Aucun provisionnement lancé. » |
| Error | Failed BYO step shows French error + retry | BYO_DB_UNREACHABLE: « Connexion à la base BYO impossible. Vérifiez l'URL et le mot de passe. » · BYO_DB_MIGRATION: « Migration Flyway échouée. Vérifiez les droits (ALTER, CREATE TABLE). » · BYO_DB_SMOKE: « Test d'isolation échoué. Vérifiez que le schéma est vide et les droits DML. » |
| Offline / low network | Pipeline keeps last state; retry idempotent | « Connexion lente — état non actualisé. » |
| Success | BYO step ✅ → continues to KEYCLOAK → ACTIVE | « Cabinet {legal_name} activé. » |
Responsive Behavior¶
Desktop-first; isolation block stacks single-column ≤ 768 px.
Connectivity Behavior (CUSTOMER stories only)¶
N/A — admin-side. Long BYO Flyway runs report progress via the retry mechanism; step state is server-persisted so a dropped POST is recoverable with the same Idempotency-Key.
Interactions¶
- « BYO-DB (clause contractuelle requise) » radio reveals the JDBC fields (progressive disclosure, design D2/D9).
- Password field uses
type="password"+ « 👁 Afficher » toggle during entry; after save it renders « •••••••• » with label « Mot de passe (chiffré — non récupérable) » and a « Modifier » button (design D9). - Warning copy: « Les identifiants sont chiffrés (KEK). Ils ne seront jamais affichés après enregistrement. »
Acceptance Criteria¶
AC-03.1: BYO-DB radio unlocks JDBC fields
Given the create form
When I select "BYO-DB (clause contractuelle requise)"
Then fields {jdbc_url, db_username, db_password, pool_size} appear and all become required
AC-03.2: Connection validated before activation
When the form is submitted with BYO-DB
Then provisioning runs (a) SELECT 1 on the supplied connection, (b) Flyway migrate to baseline, (c) a tenant_id-scoping smoke test (insert + read + delete on a fixture)
And if any step fails, the tenant stays PENDING with a French failure reason (BYO_DB_UNREACHABLE / BYO_DB_MIGRATION / BYO_DB_SMOKE)
AC-03.3: Credentials encrypted at rest
When BYO-DB credentials are stored
Then db_password is envelope-encrypted with the platform KEK
And the plaintext NEVER appears in any log line, audit entry, or error message returned to the UI (API returns "***")
Compliance Rules¶
- No
INCERTAIN/PRÉLIMINAIREobligation raised. Security control: BYO credentials envelope-encrypted (NFR-06 spirit); MinIO/secret handling per arch §7.2.
Standards & Conventions¶
docs/standards/multi-datasource-patterns.md— BYO branch, per-tenant pool cache, single chokepoint.docs/standards/multi-tenant-model.md— DB profile taxonomy; BYO greenfield only in V0.docs/standards/database-guidelines.md— Flyway baseline + migrate on supplied connection.docs/standards/java-spring-guidelines.md— Encryptor, masking converter.docs/standards/design-system.md— password field D9, ProvisioningPipeline.docs/standards/critical-rules.md— secrets never logged; tenant isolation.
Testing Requirements¶
Unit Tests¶
- Resolver returns BYO datasource when
byo_db=true, SHARED otherwise. - Envelope encrypt/decrypt round-trip; ciphertext format
version‖nonce‖tag‖ct.
Integration Tests¶
- BYO provisioning against an ephemeral second PostgreSQLContainer: SELECT 1 → Flyway migrate → smoke insert/read/delete; idempotent retry per arch §13.2.2 (force BYO_DB failure, recover, only that step re-runs).
- Secret-leak guard (Technical Risk R2): attempt to log a credential-bearing JDBC URL → masked; API detail returns
"***". - Tenant-scoping smoke proves isolation on the BYO DB.
What QA Will Validate¶
- Greenfield BYO tenant reaches ACTIVE; each BYO failure code surfaces with correct French copy + retry; password never echoed.
Out of Scope¶
- SHARED → BYO migration of an existing tenant (V2).
- SCHEMA / DATABASE profiles (V3).
- Moving AUTH user tables off the platform DB (they stay there — AUTH-ADR-01).
- Keycloak per-org IdP federation (AUTH-005 / V0.0.5).
Definition of Done¶
- [ ] Resolver BYO branch live; per-tenant pool cache; no code divergence past TenantAwareDataSource
- [ ] BYO_DB provisioning step: SELECT 1 → Flyway migrate → tenant_id smoke; idempotent + re-runnable
- [ ] BYO credentials envelope-encrypted (KEK/DEK); plaintext never logged/returned/audited; API returns "***"
- [ ] Create form BYO block (progressive disclosure) + password D9 behavior render at 360/768/1024
- [ ] All 5 ProvisioningPipeline states incl. the three BYO error codes
- [ ] French micro-copy matches design spec exactly
- [ ] AC-03.1, AC-03.2, AC-03.3 pass
- [ ] Secret-leak guard test passes; both DB profiles tested (matrix)
- [ ] Unit + integration tests passing