Access control reference
The four layers
| Order | Layer | Denial appears as |
|---|---|---|
| 1 | Tenant isolation | 404 — the row is invisible |
| 2 | Permissions | 403 with the missing codes named |
| 3 | ABAC policies | 403 for a directly requested record; dropped from a list |
| 4 | Field masking | The value is masked, not the record refused |
Layer 3 dropping rather than erroring inside a list is deliberate: erroring would reveal that a record exists and is withheld, which is itself a disclosure.
ABAC policy conditions
| Condition | Matches on |
|---|---|
| Subject | The caller's roles, groups, or identity |
| Action | The permission code being exercised |
| Environment | Request context, such as network origin |
when expression | A condition over the caller's own attributes |
| Entity type | The type of the record being read |
| Attribute | A value on the record |
| Relationship | Whether the caller's own entity is related to the record |
| Consent | Whether an active consent of a given type covers the record |
The last four are evaluated when the record is loaded, since they need the record itself. The first four are evaluated at the request boundary.
Worked examples
Each row is a single denial. Remember the base is allow, so a policy describes what must not be reachable.
| Restriction to express | Condition | Reads as |
|---|---|---|
| Offshore staff must not open patient records | Entity type | Deny entity.read for role offshore-support where the type is patient |
| Records flagged for a sanctions review are compliance-only | Attribute | Deny entity.read where compliance_status is under_review, except for compliance-officer |
| A relationship manager sees only their own book | Relationship | Deny entity.read for role relationship-manager unless the caller's own entity is related to the record by manages |
| Marketing may use a customer only while consent stands | Consent | Deny entity.read for role marketing unless an active marketing_contact consent covers the record |
| Administration only from the corporate network | Environment | Deny user.update where the request originates outside the corporate range |
The relationship condition needs the caller's identity to be linked to an entity of its own; without that link there is nothing to be related to, and the condition denies.
Resolution
| Rule | Behaviour |
|---|---|
| A denial always wins | Regardless of ordering |
| No matching rule | Allowed — the layer is a deny overlay on permissions |
| Inactive rules | Not evaluated |
| A denial with no conditions | Refused when saved |
| An evaluation fault on a nested record | Fails closed — the record is dropped |
Ordering is presentational. It is never a decision input, so two rules cannot be made to disagree by reordering them.
Masking strategies
| Strategy | Result | 555-12-3456 becomes | Configuration |
|---|---|---|---|
full | Every character replaced. Length is preserved, nothing else. | *********** | None |
partial | The last few characters kept, the rest replaced. | *******3456 | showLast |
hash | A stable one-way hash, so equal values stay equal without revealing either. | a1b2c3… (64 hex chars) | None |
null | The value is emptied and flagged as masked; the field itself remains. | null | None |
redact | A fixed placeholder. | [REDACTED] | None |
Only partial takes configuration. showLast sets how many trailing
characters survive, and defaults to 4 when you do not set it. If the value
is no longer than showLast, it is masked completely rather than exposed — so
a short value cannot leak through a rule written for a longer one.
Masking applies to three surfaces: entity attribute values, entity display labels, and audit change values.
null behaves differently on audit entriesOn an attribute the field stays present with an emptied value, so a reader can
see that something was withheld. In an audit change value the key is removed
outright. Use redact if you want a visible marker on both surfaces.

Masking rule fields
| Field | Accepts | Default | Changeable | What it does |
|---|---|---|---|---|
entityTypeId | An entity type, or empty | — | No | Which type the rule covers. Required for attribute rules. For label and audit rules, leaving it empty applies the rule to every type in the tenant. |
targetKind | entity_attribute, entity_label, audit_change_value | entity_attribute | No | Which response surface the rule governs. |
attributePath | 1–255 chars | — | No | Which field to mask. For a label rule the convention is label. |
maskingStrategy | One of the five above | — | Yes | How the value is obscured. |
maskingConfig | An object | {} | Yes | Settings for the strategy. Only partial reads it. |
appliesToRoles | Up to 2,000 chars — role names separated by commas | — | Yes | Who the rule masks for. See the warning below. |
exemptRoles | Up to 2,000 chars — role names separated by commas | — | Yes | Who is spared. A caller holding any listed role sees the value in the clear. |
metadata | An object | {} | Yes | Your own annotations, stored and returned uninterpreted. |
The first three are fixed once created. To mask a different field, or the same field on a different surface, create another rule — an existing one cannot be re-pointed.
appliesToRoles masks for everyoneLeaving the role list blank does not mean "no one". It means every role, administrators included.
That is the safe default — a rule you have written but not yet scoped withholds
data rather than exposing it — but it is the opposite of what most people
expect, and a blank field is easy to submit by accident. If you intend a rule
for a limited audience, name those roles explicitly; if you intend it for
everyone except a few, leave it blank and list the exceptions in
exemptRoles.
Rules are unique on the combination of type, path, strategy, and the two role lists. Creating a duplicate returns a conflict rather than silently adding a second rule that would mask the same field twice.
Who performed an action is never redacted. Masking a sensitive value in an audit entry is correct; hiding who made the change would defeat the audit trail. Withhold the audit permission instead.
Referential integrity
A ABAC policy naming an unknown permission code, role, group, relationship type, or consent type is rejected when saved, with a 400. A rule referencing something that does not exist would silently never fire, which looks like coverage and is not.
Next
- Compose roles and bind them to groups
- Restrict access to individual records with ABAC policies
- Mask sensitive fields
- Permission codes
Last verified against commit 74cecd91 (2026-08-03)