Skip to content
Registry StackDocsv0.46.0

Control access and reads

View as Markdown

If you are deciding who may read and change what in a Base Registry Engine (BReg) registry, this page shows you how, using the facility template. You will see how a token reaches one access profile, how a profile limits fields and rows, how related Records are disclosed, and which stricter read checks and grants a project can add.

Create the facility project in an empty directory and check it:

Terminal window
bregctl init --template facility-registry facility
Terminal window
bregctl check facility
Authoring check passed; registry revision sha256:<digest>.

The starter check passes without warnings. It keeps operator access on direct, jurisdiction-bound routes. Disclose related Records shows the warning you get when you add its optional related read.

The runtime verifies every bearer token against the one issuer its runtime configuration names, and admits only the clients listed in allowedClients. A request without a verified token is refused before any profile is considered. The same configuration names the token claims that carry the caller’s principal and purpose; What a token must carry covers that side.

Each request then uses exactly one profile. The caller names it with the accessProfile query option; without one, the route’s default applies: the only profile granting that route, or the one marked default: true. Profiles are never merged, and naming a profile grants nothing. The profile admits the request only when the token carries its principalClaim, every scope in requiredScopes, and one of its requiredPurposes, and matches actorKind and requesterClients when the profile sets them. Here is the profile that maintains operators:

registry.yaml
accessProfiles:
- id: operator-editor
principalClaim: registry_principal
requiredScopes: ["registry:operator:write"]
requiredPurposes: [facility-administration]
permissions:
records:
- recordType: operator
operations: [create, get, list, patch]
readableFields: [operator-code, status, jurisdiction]
writableFields: [operator-code, status, jurisdiction]
rowBoundaries:
- {field: jurisdiction, claim: jurisdiction, operator: equals}

When the token fails any of these checks, or the profile does not grant the operation, BReg answers exactly as it does for a Record that does not exist, and records the refusal in the audit journal. A caller cannot tell a forbidden Record from a missing one.

BReg has no built-in notion of jurisdiction, region, or office. In the template, jurisdiction is an ordinary token claim, and the row boundary compares it with the Record’s jurisdiction field. The local test clients show two readers that differ only in that claim:

dev-clients.yaml
- id: reader
accessProfiles: [facility-reader]
scopes: [registry:facility:read]
claims: {registry_principal: synthetic-reader, registry_purpose: facility-administration, jurisdiction: zone-a}
- id: foreign-reader
accessProfiles: [facility-reader]
scopes: [registry:facility:read]
claims: {registry_principal: synthetic-foreign-reader, registry_purpose: facility-administration, jurisdiction: zone-b}

dev-clients.yaml only mints local tokens that ask for these profiles; it adds no authority. In production the claim comes from your issuer, so the issuer must be one you trust to assert it.

A record permission names a Record type and its operations. readableFields limits what a response discloses and writableFields what a create or patch may set; filterableFields, sortableFields, and allowCount each enable one query feature. Field lists use the field id, not its apiName. A write never reveals a field the profile cannot read.

A row boundary compares a Record field with a token claim. equals compares with one scalar claim value. in requires a claim whose value is a JSON array; in dev-clients.yaml, write the array as a YAML sequence:

dev-clients.yaml
claims:
registry_principal: synthetic-transfer-clerk
registry_purpose: permit-administration
allowed_jurisdictions: [north-district, south-district]

A missing in claim or a scalar in its place fails closed. When journeys need another claim variant for a refusal, bind that client to the exact step with testBindings.

Every boundary on a permission must hold. Reads leave other rows out, and writes check both the stored row and the row as it would be after the change. A permission that creates must keep its boundary field writable, otherwise every create is refused, and bregctl check says so. A patch under an in boundary can move a row between the caller’s allowed values, which bregctl check points out. Write rowBoundaries: unrestricted only for deliberately registry-wide authority.

An agent is held to a narrower rule. A token from an agent, or one acting for another principal, cannot create, patch, or delete Records directly, with or without a task grant; it reaches them through Actions and Proposals instead.

For a task-grant token, row-boundary claims other than sub resolve only from the grant’s signed identity object, never from other token claims. To pin a grant to its subject, author a row boundary on that subject key. Without one, the profile’s ceiling applies to every row, as for standing agents, and bregctl check reports breg.access-profile.task-grant-rows-unbound.

A Record type can declare a read path to related Records. The facility reaches its operators through the assignment that links them:

registry.yaml
readPaths:
- {id: operators, through: facility-assignment, to: operator, route: operators}

The starter leaves that path ungranted. Its facility-reader profile grants operators directly, and the facility grant has no readPaths entry:

registry.yaml
- recordType: facility
operations: [get, list]
readableFields: [facility-code, name, address-line, city, postal-code, status, jurisdiction]
rowBoundaries:
- {field: jurisdiction, claim: jurisdiction, operator: equals}

Direct operator reads apply the operator permission’s jurisdiction boundary. A related read uses the facility permission’s authority instead. Add the optional path grant to see that distinction:

facility/registry.yaml
operations: [get, list]
readableFields: [facility-code, name, address-line, city, postal-code, status, jurisdiction]
readPaths:
- {path: operators, readableFields: [operator-code, status, jurisdiction]}
rowBoundaries:
- {field: jurisdiction, claim: jurisdiction, operator: equals}

Check the edited project:

Terminal window
bregctl check facility
warning[breg.access.profile-related-disclosure] <project-dir>/registry.yaml:237:15 /accessProfiles/1/permissions/records/1/readPaths/0
profile `facility-reader` can read `operator` records linked to `facility` through `facility-assignment` on read path `operators`; it sees fields jurisdiction, operator-code, status under its own `facility` row boundaries, and access profiles declared for `operator` do not apply
next: Remove fields that these readers should not see from this read path's readableFields, or remove the read path; use a direct permission on the related record type when its own row boundaries should apply.
0 errors, 1 warning in 3 files

Listing a facility’s operators is authorized by facility-reader’s grant on the facility, the root of the path. The row boundaries of facility-reader’s own grant on operator do not apply to the operators that come back. A reader who may see a facility sees every operator assigned to it, with the path’s readable fields, whatever that operator’s jurisdiction.

The warning remains even though the direct operator grant names the same three fields, because its row boundary does not govern the related read. Ask the same two questions of your own paths: can a related Record sit outside the reader’s rows, and do the path’s fields disclose more than a direct grant would? If either answer exposes more than you intend, narrow the path’s readableFields, or remove the path grant and use the direct target grant. The shipped starter takes that latter, narrower route. When a target, or the Record type a path passes through, must never be disclosed without its own row check, give that Record type accessRequirements with row boundaries: the compiler then refuses any read path grant that reaches it.

A row boundary works when the Record carries the claim value it is compared with. Sometimes the right to read follows an affiliation that changes over time: a facility belongs to an organization, and whoever is currently a member of that organization may read it. Copying members into tokens would leave access in place until each token expired.

membershipBoundaries on a record permission puts that rule into the registry. Each entry names the Record’s reference field, a membershipRecordType, and that type’s membershipKeyField, principalField, and activeField. A row is readable only while a membership Record exists with the same key, the caller’s verified principal, and active: true. Memberships are ordinary Records that a separate steward profile creates and deactivates. A request that starts after a deactivation commits no longer sees the Records; a read already running may finish. Historical reads compare against current memberships too, so a past membership cannot restore access.

Membership boundaries apply to reads only: get, lookup, list, snapshot, and revisions. Give writes to a separate profile. Declare rowBoundaries beside them, even if only unrestricted; every membership and row boundary must hold.

A membership boundary asks whom the caller works for. A consent check asks what the Record’s subject decided: that this recipient may read their Record, for this purpose, under this profile.

requireConsent on a record permission lists checks, each naming a consent record type and on, the field that identifies the subject (id for the row itself). The project declares recipients, the organizations that read and the clients each one reads through. BReg derives the caller’s recipient from the verified client, never from a token claim, so a gated profile must list its requesterClients. A row is disclosed only while a current consent covers it; a withdrawal takes effect for every request that starts after it commits, with no token change or restart. Gated permissions are read-only, cannot export, and need their own clients, because any profile without requireConsent reads without the check.

The profile id is the scope the subject agreed to. Removing requireConsent or widening a gated profile in place would read under a scope the subject never saw, so publish a new profile and list the old id under retiredConsentScopes.

Membership and consent refusals look like every other access refusal. A get on a Record the caller may not see answers 404, exactly as for a Record that does not exist; a list or count leaves the Record out. The caller learns nothing about why, or whether the Record exists. The problem documents in the API reference list the codes, and the configuration reference carries every key named on this page.

A project that publishes counts declares statistical datasets, and a profile reaches them through permissions.datasets. Each entry names a dataset and its operations: read-live for current counts, publish to publish or withdraw a release, read-releases to read published releases. A standing agent profile, one with actorKind: agent and no taskGrant, cannot hold publish: a release takes effect at once, with no draft for a person to confirm, so the compiler refuses it. Publish a statistical series covers the datasets themselves.

Terminal window
bregctl explain access facility
profile facility-reader
actor kind not declared
requester clients any
principal claim registry_principal
task grant none
operations ["get","list"]
required scopes (all) ["registry:facility:read"]
allowed purposes (any) ["facility-administration"]

The human report shows the actor kind, requester clients and task-grant requirements beside each profile’s fields and row restrictions. Action and Proposal permissions include their target rows, inputs, attachments and ownership. Omitted ownership and writable-input defaults are explicit. Caller ownership limits which Proposal documents can be managed; it does not bound the target Records. The report also shows the built-in executor’s system application ceiling. Each read-path grant names its target Record type, through Record type, and route. rowReach also lists the target under a read_path surface with the root profile’s row boundaries, so a review that starts from the disclosed Record type does not miss incoming paths. Add --scenario with a file of synthetic claims to preview a decision without a token or a database. To prove the refusals against a running registry, write journeys that read as both readers.