Skip to content
Updated Jun 9, 2026

Tenant Onboarding - Test & Review Flow

Status: draft · Owner: Chakshu · Informs: Phase 3 authz · Concern: ABAC (access), not at-rest Decides: how a new tenant's access configuration is validated and signed off before it can serve a single production request. Tutorial-style: follow it top to bottom. Use the ∑ terse / ¶ plain toggle for each step.

A tenant config is data, not code - it plugs into the one shared Rego policy and the RLS floor. So onboarding a tenant is a configuration review, gated by the same invariants the POCs proved hold at scale.


What "a tenant configuration" is

The reviewable unit. Everything that decides who in this tenant sees which rows and fields:

PartExampleLives in
Identityorg_locator / scheme / tenant_idKeycloak (subject claims) + the row tenant_id
Roles → row scopemember: claim.member_id == actor.id, employer-admin: claim.tenant_id == actor.tenant_iddata.claim_access.role_rules (OPA data)
RLS predicatetenant_id = current_setting('app.tenant')Postgres policy (the floor)
Field policy Φdx: deny, dob: year, name: via_vaultOPA Rego / data
Field classeswhich columns are tokenised at restvault config (separate at-rest concern)

The config is authored as code in a PR and applied as OPA data + an RLS migration. Nothing here is typed into a console by hand in prod.


The flow

Two automated gates (B, D), one human gate (E), one operational gate (F). Failure anywhere returns to the author - config never advances on a red.


Step by step

1 · Author (PR) config-as-code

Tenant config committed as data: role_rules entries, the RLS predicate, Φ overrides, field classes. Reviewable diff; no prod console edits.

Plain-English explanation

The person onboarding the tenant writes the config into version control - the role-to-scope rules, the row-level-security predicate, any field-policy overrides, and which fields are tokenised. It arrives as a pull request, so the change is a reviewable diff with history, not an ad-hoc change made directly against the running system.

2 · Static validation (CI) - gate B

What CI checks, all mechanical:

CheckPass conditionBacked by
Schemaconfig parses; required keys presentJSON schema
Allowlistevery rule's column ∈ table allowlist, op{eq, in, all}, actor_field setroles.Validate (same code the harness shows as VALID/INVALID)
RLS predicatecompiles against the real schema; references only tenant_id + a GUCmigration dry-run
Φ targetsreferenced fields exist; no field both deny and via_vaultpolicy lint

Gate B: all green, else back to author. *Cost: ms

  • no data needed.*

3 · Automated isolation & no-drift (sandbox) - gate D

CI seeds a synthetic copy of the new tenant and at least one sibling tenant, then runs the same checks the POC harness runs

  • now over the tenant's own roles:

  • Isolation (I-cross): new tenant's every role returns 0 sibling-tenant rows; sibling returns 0 of the new tenant's. RLS enforced even with the app filter removed.

  • No-drift (I1):list(role) = { x : canView(role, x) } exactly, over ≥500 synthetic actors/role.

  • Φ: restricted fields never returned in clear; via_vault fields stay tokens unless Φ says otherwise; dob:year never leaks day/month.

  • Fail-closed: an unknown role / malformed rule ⇒ deny / 0 rows, never allow-all.

  • Sargable: generated SQL is index-based (no Seq Scan on the tenant's hot tables).

Plain-English explanation

The system stands up throwaway data for this tenant and a neighbour, then asks, for every role the tenant defines: does a list query return exactly the rows a one-by-one permission check would allow (no drift)? Does this tenant ever see a neighbour's rows, even if the application code's own filter is deleted (the database must still refuse)? Are fields that should be hidden actually hidden, and are the ones that should only appear via the vault still encrypted? If the config is broken or incomplete, does the system deny rather than fall open? And do the queries still use indexes so they stay fast at scale? Every one of these must be green.

CheckProvesPOC / invariant
Isolationno cross-tenant leak even on an app bug#2 RLS - safe pattern 0 leaks / 50k
No-driftlist filter ≡ point check (one policy)#1 OPA partial-eval - I1, 500 actors/role
Φ field-levelright fields hidden/tokenisedABAC design
Fail-closedmisconfig denies, never openstranslate + RLS NULLIF
Sargablestays fast at 1M+ rows#1 / #2 EXPLAIN plans

Gate D: all green, else back to author. The report is attached to the PR.

4 · Human review · 4-eyes - gate E approval recorded

A reviewer who is not the author reads the diff + the green report, then clicks through the tenant's roles in the harness (roles-poc.dev.hiolly.com) - pick an actor, list rows, point-check an in-set and an out-of-set row, confirm the no-drift badge. Sign-off is audited.

Plain-English explanation

Automated green is necessary but not sufficient - someone independent confirms the intent is right (e.g. that "employer-admin" really should see the whole tenant, that no role was scoped too broadly). The reviewer opens the interactive harness, picks a real actor for each role, lists what they can see, spot-checks a row that should be allowed and one that should not, and watches the no-drift verdict. Their approval is recorded with who/when, so the decision is attributable later.

Reviewer checklist:

Item
Each role's scope matches the intent in the onboarding ticket (no role broader than needed)
employer-admin (or tenant-wide roles) are deliberate, not accidental
Harness: in-set row → ALLOW, out-of-set → DENY, no-drift ✓ for every role
Sensitive fields (nhs, dx) show deny; dob shows year only
EXPLAIN shows index scan (no Seq Scan) for the tenant's tables
Reviewer ≠ author; sign-off captured in the audit log

Gate E: approved (audited), else changes requested.

5 · Staged rollout - gate F

Apply to a staging tenant first: push the OPA data + run the RLS migration, run a smoke suite, optionally shadow real-shaped traffic (canary). Watch the composed-path latency budget (#4: p99 within +50 ms).

Gate F: staging green, else back to author. Rollback path stays open.

6 · Promote to prod ✓ live

Versioned, reversible apply: OPA data push (atomic) + RLS migration, with an audit entry. The previous config version is retained so promotion can be reverted in one step (the dashed rollback edge).


Reviewer's-eye sequence


Hard gates (nothing promotes past a red)

  1. Any cross-tenant row in the isolation test ⇒ blocked. Non-negotiable.
  2. Any drift (list ≠ canView set) for any role ⇒ blocked.
  3. Fail-open on a malformed/empty config ⇒ blocked (must deny).
  4. No human approval, or author == reviewer ⇒ blocked.
  5. A Seq Scan on a hot table is a warning, reviewed case-by-case (not an auto-block) - see #1/#2 plans.

This is the access-config review flow. The orthogonal question - how the PII those rows contain is protected on disk - is data protection at rest, reviewed separately.

Olly Health Insurance Platform