Repository navigation
ADR 010 Attribute Level Access Control
Accepted.
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.
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
sealedAtwas 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.
- 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.
-
sysadminis superuser on levels 1-4, no route into level 0; onlycompliancedoes. - Ladder, level 0, Selected Credentials are Phase 3 design, not live; field plane is live.
- Open EAO questions live in the design doc.
- docs/rbac-architecture.md — full model, audit
-
Attribute-Level-Access — level table, catalog format,
/api/me - ADR-004: Read ACL Authorization Model — row visibility
- ADR-007: Service-to-Service Credentials
- Environment Reality & Gotchas
- Architecture
- Seeding — filling Cosmos from Track and Eagle
- Sources and Status — every source, one state
- Search & Retrieval — overview
- Search Index Reference
- Search Query Construction
- Search Measurements
- Extraction Pipeline
- Extraction Quality — coverage, word-joining, tiling
- Identity Model — which id means what
- Attribute-Level Access — field-visibility levels, catalog
- Sync Architecture — how records get in
- Eagle Mirror Reference — mirrored kinds, refusals, drift classes
- ENGAGE Rows Reference — ENGAGE-owned comment periods, reconcile
- Public-Read Backfill — filling and repairing the mirror containers
-
Short Links —
/s/{code}redirect and/api/links -
Recent Uploads —
/api/documents/recent-uploads, eagle-public homepage panel - Reports — comment export and BCGW CSV reads
-
User Data — saved map areas, preferences,
/meroutes -
Bulk Download —
/api/bulk-downloads, zip jobs - Bulk Download: Operations — enable, poison-queue runbook
- Document Intake — upload link, malware scan, release
- Connecting an Application to DEMI
- Azure Environments
- Document Backup — archive copy of originals, run and check
- Document Backup: Revert and Drill — put documents back from the archive
- PDF Titles — public name written into stored PDFs, run, restore
- Secret Sync — Key Vault, secret-sync Function app, required names
-
Operating DEMI in Azure — deploy, verify
- Operating the DEMI Data Plane — devbox access, probes, audit and cost reads
- Edge ban detector — hourly timer, modes, kill switch, soak and promote
-
CI Workflows — every GitHub Actions workflow, trigger, identity, roles
- CI Deploy Workflows — eagle-demi staging and production deploys
- CI Admin Frontend Workflows — eagle-demi-admin checks and deploys
- Release Process — cutting, tagging, shipping and rolling back a version
- Runbook: Search Outage — index/app schema drift, diagnose, fix, roll back
- BC Gov Landing Zone
- Business Rules
- EPIC.search Business Rules — where DEMI differs from EPIC.search
- 001: Typesense for DFL Search — superseded
- 002: Async Processing (MongoDB Queue) — superseded
- 003: Rule-Based Classification
- 004: Read ACL Authorization Model
- 005: Cosmos Full-Text Search Ruled Out
- 006: AI Summarization over BM25
- 007: Service-to-Service Credentials
- 008: Audit and Usage Analytics Store
- 009: Track Feed — proposed
- 010: Attribute-Level Access Control
- 011: Public Read Surface
- 012: Public Config Surface
- 013: Stored Project Summaries — proposed
- 014: Sync-out to Eagle
- 015: Malware scanning on intake