LBAC reference
Security label options and a live look at which records actually carry them
7 cells · updated Aug 28, 2026
Label-Based Access Control (LBAC)
Live on both serenity-local and wzrd-lab since 2026-08-28 — see docs/admin-guide/aidbox-console.md's "Label-Based Access Control (LBAC)" section and memory project_aidbox_lbac_backlog for the full story.
The one rule that matters: a resource with no meta.security label is inaccessible to everyone — admin, root, any role. Every query below runs through this notebook's own SQL engine, which (like $psql) bypasses LBAC's read gate — this is the normal way to inspect labeled data as an admin, not a workaround.
Confidentiality codes (http://terminology.hl7.org/CodeSystem/v3-Confidentiality)
Hierarchical — a request's confidentiality level expands downward to everything less restrictive. Confirmed live against this exact Aidbox version (aidbox-docs's own worked LBAC example): a request carrying R is granted R, N, M, L, and U. V sits above this chain — an R-level request does not see V-labeled resources.
| Code | Meaning | Granted by a request holding... |
|---|---|---|
V | Very restricted | V only — does not expand down automatically per the accessibility matrix example |
R | Restricted | R (expands to grant N, M, L, U too) |
N | Normal | N, M, L, U |
M | Moderate | M, L, U |
L | Low | L, U |
U | Unrestricted | U only |
What this stack actually uses: every Keycloak-issued token for serenity-chart-viewer/practitioner-app carries a baseline U (unrestricted) — see the role-to-smart-scope script mapper in scripts/register-keycloak-sso.sh. Nothing in this stack currently issues a request with any other confidentiality level — the label options above are all available, just unused beyond the baseline so far.
Sensitivity / compartment codes (http://terminology.hl7.org/CodeSystem/v3-ActCode)
A second, independent code system LBAC also checks for overlap — not hierarchical the way Confidentiality is; a request needs the specific code a resource is labeled with (no automatic expansion). Only used in this stack so far in the demo/tutorial data actually seeded here:
| Code | Meaning |
|---|---|
PSY | Psychiatry |
HIV | HIV/AIDS information |
ETH | Substance abuse related information |
CTCOMPT | Care team compartment |
FMCOMPT | Financial management compartment |
RESCOMPT | Research project compartment |
PROCESSINLINELABEL | Marker telling Aidbox to also apply element-level masking (meta.security, not a sensitivity label itself) |
See aidbox-docs/docs/access-control/authorization/label-based-access-control.md for the full masking mechanics (this is what makes Patient.identifier/Patient.name maskable per-field, independent of whether the whole resource is readable).
SELECT 'Patient' AS resource_type, sec->>'code' AS code, sec->>'system' AS system, count(*)
FROM patient, jsonb_array_elements(resource#>'{meta,security}') sec
GROUP BY 1,2,3
UNION ALL
SELECT 'Encounter', sec->>'code', sec->>'system', count(*)
FROM encounter, jsonb_array_elements(resource#>'{meta,security}') sec
GROUP BY 1,2,3
UNION ALL
SELECT 'Observation', sec->>'code', sec->>'system', count(*)
FROM observation, jsonb_array_elements(resource#>'{meta,security}') sec
GROUP BY 1,2,3
UNION ALL
SELECT 'Condition', sec->>'code', sec->>'system', count(*)
FROM condition, jsonb_array_elements(resource#>'{meta,security}') sec
GROUP BY 1,2,3
UNION ALL
SELECT 'Questionnaire', sec->>'code', sec->>'system', count(*)
FROM questionnaire, jsonb_array_elements(resource#>'{meta,security}') sec
GROUP BY 1,2,3
UNION ALL
SELECT 'QuestionnaireResponse', sec->>'code', sec->>'system', count(*)
FROM questionnaireresponse, jsonb_array_elements(resource#>'{meta,security}') sec
GROUP BY 1,2,3
UNION ALL
SELECT 'Organization', sec->>'code', sec->>'system', count(*)
FROM organization, jsonb_array_elements(resource#>'{meta,security}') sec
GROUP BY 1,2,3
UNION ALL
SELECT 'Practitioner', sec->>'code', sec->>'system', count(*)
FROM practitioner, jsonb_array_elements(resource#>'{meta,security}') sec
GROUP BY 1,2,3
UNION ALL
SELECT 'PractitionerRole', sec->>'code', sec->>'system', count(*)
FROM practitionerrole, jsonb_array_elements(resource#>'{meta,security}') sec
GROUP BY 1,2,3
UNION ALL
SELECT 'User', sec->>'code', sec->>'system', count(*)
FROM "user", jsonb_array_elements(resource#>'{meta,security}') sec
GROUP BY 1,2,3
ORDER BY 1, 4 DESC;SELECT 'Patient' AS resource_type, count(*) AS missing_label FROM patient WHERE resource#>'{meta,security}' IS NULL
UNION ALL SELECT 'Encounter', count(*) FROM encounter WHERE resource#>'{meta,security}' IS NULL
UNION ALL SELECT 'Observation', count(*) FROM observation WHERE resource#>'{meta,security}' IS NULL
UNION ALL SELECT 'Condition', count(*) FROM condition WHERE resource#>'{meta,security}' IS NULL
UNION ALL SELECT 'Questionnaire', count(*) FROM questionnaire WHERE resource#>'{meta,security}' IS NULL
UNION ALL SELECT 'QuestionnaireResponse', count(*) FROM questionnaireresponse WHERE resource#>'{meta,security}' IS NULL
UNION ALL SELECT 'Organization', count(*) FROM organization WHERE resource#>'{meta,security}' IS NULL
UNION ALL SELECT 'Practitioner', count(*) FROM practitioner WHERE resource#>'{meta,security}' IS NULL
UNION ALL SELECT 'PractitionerRole', count(*) FROM practitionerrole WHERE resource#>'{meta,security}' IS NULL
UNION ALL SELECT 'User', count(*) FROM "user" WHERE resource#>'{meta,security}' IS NULL
ORDER BY 2 DESC;Anything showing up in the query above needs scripts/lbac-backfill-default-label.sh re-run (it's idempotent — only touches resources actually missing a label). This happens whenever new data is bulk-loaded or written by something other than practitioner-app (which labels its own writes at creation time, see apps/practitioner-app/lib/aidbox/security-label.ts) or a Keycloak-fronted app's own creates.
SELECT id, resource#>'{meta,security}' AS security_labels
FROM patient
ORDER BY id
LIMIT 50;