Pulp Engine Document Rendering
Get started
Release v0.81.0

Release v0.81.0 — DB-backed editor user registry (HA fix for F4)

The post-hardening audit’s finding F4 noted that the named-user editor registry was per-instance (in-memory + flat file): in HA it diverged across replicas, and with OIDC_AUTO_PROVISION on, a shared EDITOR_USERS_FILE was clobbered last-writer-wins. v0.80.0 documented and warned about this and deferred the real fix. This release implements it.

In STORAGE_MODE=postgres/sqlserver, editor users now live in a shared editor_users table, so named users — including OIDC auto-provisioned ones — are consistent across all replicas. File mode is unchanged (single-instance flat-file registry).

Architecture

UserRegistry is now an in-memory cache over an IEditorUserStore persistence port:

  • Reads stay off the DB hot path. Auth lookups (getById/getByKey/getByOidcSub) serve from the cache; on a miss they read through to the store (so a user created on another replica is visible immediately), and the cache does a periodic full reload (EDITOR_USERS_CACHE_TTL_MS, default 10 s).
  • Local writes are immediate; cross-replica propagation of role changes / tokenIssuedAfter revocations / deletes is bounded by the cache TTL (new users are immediate via read-through).
  • Three adapters: File (single-instance flat file, 0o600), Postgres (Prisma), SQL Server (mssql). The registry is built over a bootstrap in-memory store in authPlugin and atomically swapped to the real store after storagePlugin boots.

Behavior changes

  • Seeding: in DB mode, EDITOR_USERS_JSON/EDITOR_USERS_FILE now seed an empty editor_users table on first boot only (under a seed-only-when-empty guard, so a user deleted from the DB is never resurrected from config). After that the DB is authoritative. Concurrent multi-pod boot converges via the table’s unique constraints.
  • POST /admin/users/reload now reloads the in-memory cache from the authoritative store (file in file mode, DB in DB mode) rather than re-reading EDITOR_USERS_FILE + replacing.
  • EDITOR_USERS_DB=true (new) enables DB-backed named-user mode with no JSON/FILE seed (bootstrap the first user with an admin API key via POST /admin/users). Valid only with STORAGE_MODE=postgres|sqlserver.
  • The v0.80.0 file-registry HA divergence startup warning is removed in DB mode (the divergence is fixed); it remains relevant only to file mode (single-instance). identityMode now also resolves to named-users once the DB registry is non-empty, even without a JSON/FILE seed.

Security

  • Editor keys remain stored recoverably (they double as the editor-token HMAC secret) — plaintext, parity with the 0o600 file. DB dumps contain editor login/token-verification secrets — access-control the database. Keys are never logged; only keyHint (last 4 chars) is exposed.
  • SQL Server id, [key], and oidc_sub use a case-sensitive binary collation (Latin1_General_BIN2) so a secret lookup cannot match case-insensitively (Postgres is case-sensitive by default).
  • DB-loaded rows are validated against active API keys: a collision fails boot (attach) or is skipped + logged on a runtime reload.

Schema & migrations

  • Prisma EditorUser model + migration 20260602000000_add_editor_users.
  • Hand-written SQL Server migration 012_editor_users.sql (bounded NVARCHAR, binary collation, filtered-unique oidc_sub), kept in lockstep with the Prisma schema by the sqlserver-schema-parity test (EditorUser added to its curated set).

Tests

  • editor-user-registry.test.ts — store contract (CRUD, duplicate-constraint naming, NOT_FOUND) and cache behaviour (read-through, TTL reload, write-through, attach seed-when-empty + no resurrection, API-key collision fail/skip).
  • postgres-editor-user.store.test.ts — DB-gated adapter tests (CRUD, constraint naming, case-sensitive lookups, null-oidc_sub multiplicity, default-tenant omission).
  • oidc-registry.test.ts / user-registry-persist.test.ts updated to the new async cache + the file adapter; config-validation.test.ts covers EDITOR_USERS_DB (file-mode rejection, invalid value) and EDITOR_USERS_CACHE_TTL_MS.

Deferred

  • A management UI for editor users (CRUD exists via /admin/users).
  • The DB-backed registry does not change file mode, which remains single-instance.