ADR-037 -- Entra Authenticates, Bilko Authorises; Single-Role v1; Multi-Org Deferred

ADR-037 — Entra Authenticates, Bilko Authorises; Single-Role v1; Multi-Org Deferred

FieldValue
ADR numberADR-037
Date2026-06-08
StatusAccepted
AuthorJohn (AI Director, ALAI Holding AS)
CEO decisionAlem Basic — confirmed 2026-06-07 (CEO resolution addendum, MC #103075)
Related MCsMC #103075 (Entra migration plan), MC #103141–103146 (WP1–WP6 execution), MC #103089 (multi-org, parked)
SupersedesExisting inline requireRole() pattern (pre-WP1)

Context

Bilko had a custom email/password authentication system and a simple numeric role hierarchy (requireRole() inline in route handlers). No permission catalog, no RBAC tables, no admin UI for user management. The CEO decision (June 2026) was to:

  1. Replace email/password authentication with Microsoft Entra External ID (CIAM) — hard REPLACE, not phased coexist
  2. Build a real permission-catalog RBAC system with a DB-backed role-to-permission mapping

Multiple design forks were evaluated by a multi-agent panel (Parisa Tabriz, Martin Kleppmann, Petter Graff, Bruce Momjian, Devils Advocate — MC #103075 forged prompt). Key unresolved tensions: web direct-bearer vs exchange, email-match JIT vs pre-provision-by-oid, roles-in-Entra-claims vs roles-in-Bilko-DB.

Decision

D1 — Identity Provider Boundary

Entra External ID (CIAM) authenticates. Bilko authorises.

D2 — Single Role per User per Organisation (v1)

One role per user per org: owner | admin | accountant | viewer. The role is stored in users.role (single column). Multi-role per user and multi-org membership are explicitly deferred to a separate epic (MC #103089).

Rationale: zero live clients; single-org Entra tenant; keep scope tightly bounded; multi-org requires a organization_members join table and CIAM tenant model decisions that are not yet resolved.

D3 — Permission Catalog in DB; Flat Inheritance Seed

A permissions catalog table (52 keys, resource:verb format enforced by CHECK) and a role_permissions mapping table (V67) replace the inline requireRole() calls. Seed strategy: flat exhaustive rows per role (Strategy A) — no runtime hierarchy derivation. The seed exactly reproduces existing behaviour (no regression — verified by 204 RbacMatrixTest cases).

D4 — Live DB Permission Resolution; Fail-Closed

PermissionService.resolve(role) queries role_permissions at request time. Unknown role resolves to emptySet() (no permissions). BilkoPrincipal carries the resolved permission set. All route-level checks use requirePermission("resource:verb").

D5 — Multi-Org Deferred

Entra CIAM is provisioned as a single tenant. JIT provisioning assigns a new Entra user to one Bilko organisation. Multi-org (one user in multiple orgs) requires: a organization_members join table, per-org permission resolution, and CIAM tenant model decisions. All deferred to MC #103089.

Consequences

Positive

Negative / Trade-offs

Alternatives Considered

AlternativeRejected reason
Roles in Entra claims (Entra app roles)Couples authorisation to IdP; role changes require Entra admin action not Bilko admin action; prevents clean multi-IdP future. Rejected per petter-graff + parisa-tabriz panel consensus.
Phased coexist (email/password + Entra in parallel for 2+ weeks)CEO confirmed hard REPLACE. Panel devils-advocate raised phased coexist as safer; CEO re-confirmed hard REPLACE given zero live users. AuthProvider interface (D5 MC #103075) technically enables a revert if needed.
Denormalised entra_oid on users table (bruce-momjian alternative)Separate-table V64 model kept; enables multi-IdP future; join cost is negligible at current scale. Fork preserved but not resolved — separate-table remains.
ABAC / policy engine (v1)Premature for current scale and requirements; adds complexity; deferred as explicit out-of-scope with comment in plan.

Open Decisions Not Resolved by This ADR


Revision #1
Created 2026-06-08 07:43:19 UTC by John
Updated 2026-06-08 07:43:19 UTC by John