Skip to content

ADR 010 Attribute Level Access Control

Daniel Truong edited this page Oct 7, 2026 · 8 revisions

ADR-010: Attribute-level access control

Status

Accepted.

Context

DEMI gates records two ways: row visibility (read[], readClause/canRead, OData filterFor) and project scope (project:<id> roles). Neither controls which fields of a visible record a caller sees; a staff member and an anonymous visitor who can both read a project get the same document.

The design comes from "DEMI Role-Based Access Control & Field-Level Redaction" (Mark Lise, Digitalspace), audited against the codebase, then revised against the EAO's sharing model.

Decision

Add a third plane, field visibility, alongside the two that exist. Row visibility and project scope are unchanged in mechanism; the row plane now carries a sharing ladder.

Row plane: a four-value ladder. read[] holds ladder tokens, role types only, never ids, not cumulative: a level-2 row's read[] carries no team token, so a team-only caller cannot read an All-EAO row even in its own project.

Level Name read[] Matched by
1 Team only ['team'] team, matched as an OR arm against the caller's project:<id> roles
2 All EAO ['staff'] staff realm role
3 All IDIR ['staff','idir'] staff or IDIR login
4 Public ['staff','idir','public'] anyone

Levels 2-4 all carry staff, so a staff caller reads every row at level 2+ with one token; level 1 rows are reached only through the team arm. A key's projectScope is a separate RESTRICTION set by the key's issuer: it ANDs against every read regardless of level, never merged with the team grant. Team membership (project:<id> roles) is minted from Track's staff_work_roles feed (planned, Phase 3).

A record's level is derived from read[], never stored separately: the widest token present, or 1 when none. Default on admission is level 1 — every write site creating a record starts it at team only. Widening is an explicit, audited act: PUT /api/{projects,documents}/:id/level with { level, confirm, reason } changes a record's level; nothing widens a record automatically. Level 4 requires confirm: true and a reason, and refuses without either. A document cannot out-rank its project. Pulling a record back from level 4 is sysadmin-only, audited, incident response — not this endpoint — and follows a takedown runbook.

Level 0: a sealed compartment, outside the ladder. Not a fifth rung. A level-0 record carries read: ['compliance']; only the compliance role matches that token. No encryption: the row plane is the seal, so every read predicate excludes compliance-only rows even for privileged callers and systemAccess(). The seal depends on the ADMIN_API_KEY break-glass never resolving compliance on exports/backups staying locked down, and (planned, Phase 5) on compliance keys being mintable only by a compliance holder. sysadmin has no route in. A record leaves level 0 only through an explicit release to level 1 (team), which requires a case number and a decision and notifies the C&E lead.

Amendment, 2026-09-26 (eagle-demi #454): a level-0 row without sealedAt was sealed by an Eagle push, not by DEMI, and heals on its own next Eagle push rather than through release. See Attribute-Level-Access#level-0-sealed-compartment and Eagle-Mirror-Reference#mirrored-kinds.

Selected Credentials (planned, Phase 3). A named party — a BCeID Business login (proponents, consultants) or a registry API key — gets a time-bound, revocable grant to specific records at levels 1-3, without changing the record's level or anyone else's access. end is required and the grant auto-revokes when the project closes. Level 4 needs no credential.

Field plane. Every caller resolves to one integer level (1-4); a role not on the ladder resolves to 4. Every field of every entity is classified in code (defaultVis, maxVis ceiling), never read from Cosmos. A sparse vis dial can override a field's baseline, clamped to [0, maxVis]. Field dials are independent of the record's level: the row plane decides who sees the record, the field plane refines by the caller's own level.

Redaction runs at the response boundary, not inside getById (Cosmos upsert replaces the whole item, so a redacting getById would erase hidden fields on the next write). List reads use a catalog select so hidden values never leave Cosmos. Unknown role, missing field classification, or an out-of-range dial fail closed toward restriction.

Consequences

  • Every entity response site must call the redactor; a coverage test enforces this per call site.
  • The field catalog must track what the merge/seed pipelines emit, not a source document's field list; a completeness test diffs the two.
  • Day-one field defaults reproduce today's public output, not the source design's defaults.
  • sysadmin is superuser on levels 1-4, no route into level 0; only compliance does.
  • Ladder, level 0, Selected Credentials are Phase 3 design, not live; field plane is live.
  • Open EAO questions live in the design doc.

See also

DEMI Wiki

Home

Current system

Azure

Domain

ADRs

History

Related

Clone this wiki locally