Skip to main content

Access control reference

The four layers

OrderLayerDenial appears as
1Tenant isolation404 — the row is invisible
2Permissions403 with the missing codes named
3ABAC policies403 for a directly requested record; dropped from a list
4Field maskingThe 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

ConditionMatches on
SubjectThe caller's roles, groups, or identity
ActionThe permission code being exercised
EnvironmentRequest context, such as network origin
when expressionA condition over the caller's own attributes
Entity typeThe type of the record being read
AttributeA value on the record
RelationshipWhether the caller's own entity is related to the record
ConsentWhether 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 expressConditionReads as
Offshore staff must not open patient recordsEntity typeDeny entity.read for role offshore-support where the type is patient
Records flagged for a sanctions review are compliance-onlyAttributeDeny entity.read where compliance_status is under_review, except for compliance-officer
A relationship manager sees only their own bookRelationshipDeny 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 standsConsentDeny entity.read for role marketing unless an active marketing_contact consent covers the record
Administration only from the corporate networkEnvironmentDeny 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

RuleBehaviour
A denial always winsRegardless of ordering
No matching ruleAllowed — the layer is a deny overlay on permissions
Inactive rulesNot evaluated
A denial with no conditionsRefused when saved
An evaluation fault on a nested recordFails 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

StrategyResult555-12-3456 becomesConfiguration
fullEvery character replaced. Length is preserved, nothing else.***********None
partialThe last few characters kept, the rest replaced.*******3456showLast
hashA stable one-way hash, so equal values stay equal without revealing either.a1b2c3… (64 hex chars)None
nullThe value is emptied and flagged as masked; the field itself remains.nullNone
redactA 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 entries

On 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.

Field masking rules, listed with the strategy and roles each one applies to.

Masking rule fields

FieldAcceptsDefaultChangeableWhat it does
entityTypeIdAn entity type, or emptyNoWhich 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.
targetKindentity_attribute, entity_label, audit_change_valueentity_attributeNoWhich response surface the rule governs.
attributePath1–255 charsNoWhich field to mask. For a label rule the convention is label.
maskingStrategyOne of the five aboveYesHow the value is obscured.
maskingConfigAn object{}YesSettings for the strategy. Only partial reads it.
appliesToRolesUp to 2,000 chars — role names separated by commasYesWho the rule masks for. See the warning below.
exemptRolesUp to 2,000 chars — role names separated by commasYesWho is spared. A caller holding any listed role sees the value in the clear.
metadataAn object{}YesYour 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.

An empty appliesToRoles masks for everyone

Leaving 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.

Actor metadata is never masked

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


Last verified against commit 74cecd91 (2026-08-03)