Skip to main content

RBAC

Seeding, granting and verifying role-based access control. The blong-access README documents the tables, the handlers and the configuration defaults; this page shows how to use them. The RBAC concept describes the model.

1. Turn the gate on​

config: {
default: {
gateway: {authorize: 'access.authorization.list'}, // ← turns RBAC on
},
},

Without that hook the routes are served with no RBAC check. The README's usage block shows the full suite wiring (server, login, core and access realms as children).

2. Model the authority data​

ConceptResource typeLinks toMeaning
actionaccess.action—one guarded method (a semantic triple)
capabilityaccess.capabilityactionsa group of actions — "what someone does"
roleaccess.rolecapabilitiesa job; carries the unique roleBit
useraccess.userroles, unitsthe caller
unitparty.unitrolesan org unit that can hold roles for its members

The edges are hasAction, hasCapability, hasRole, belongsTo (and hasScope for the implicit ACL grant). After changing them the materialized paths must be refreshed: access.authorization.merge does it for you, hand-written SQL calls CALL access_pathRefresh().

Granting is an occasional out-of-band write, while deciding happens on every request — which is why the two halves are shaped so differently:

3. Seed roles, capabilities and users​

Roles are declared by name only — the bit is allocated (see below):

# meta/db/1-accessRoleMerge.yaml
resourceType: access.role
role:
- description: Admin role with full access
name: Admin

Capabilities, actions and the users that hold them go in one authorization merge:

# meta/db/accessAuthorizationMerge.yaml
capability:
loginCapability: accessLogin
userManagement: accessUserFind,accessUserAdd,accessUserEdit
role:
Admin: loginCapability,userManagement
user:
testAdmin: {password: testPassword, roles: Admin}

Realm-specific grants belong in the owning realm's own merge file (same handler, same format); both files are additive and idempotent.

4. Role bits​

SituationBehaviourError
No bit givenallocated as max(high-water mark, MAX(roleBit)) + 1; a bit is never re-issued—
Bit given and freeused—
Bit given and owned by another rolerefusedrole.bitTaken
Bit that is not a number in 0–1023refusedrole.bitInvalid
Edit that changes an existing role's bitrefused; the column is never writtenrole.bitImmutable

All five rows are one decision, taken in one place:

Every creation path — the model's add, registration, the authorization merge, the seed merge and the gateway's bundles — goes through access.role.ensure, so a role either exists with a real bit or the call fails with one of the errors above. The UI leaves the bit blank; the save response carries the allocated value.

5. Login eligibility​

A user may establish (and renew) a session only while the accessLogin action is in their effective actions:

capability:
loginCapability: accessLogin
role:
Admin: loginCapability

Removing that capability from a role refuses its logins at the session gate with login.loginNotAllowed (401); a deactivated user gets login.userInactive. See the sessions concept.

6. Verify inside a handler​

export default handler(({handler: {accessSessionVerify}}) => ({
async invoiceInvoiceApprove(params: {invoiceId: string}, $meta: IMeta) {
// Re-checks the live graph, not just the token, and requires the record
// when the entity is guarded by the record-level ACL.
const {userId} = await accessSessionVerify(
{
action: 'invoice.invoice.approve',
record: {entity: 'invoice.invoice', recordId: params.invoiceId},
},
$meta,
);
// … proceed
},
}));

access.session.verify throws the access.session.* (401) family and acl.notPermitted (403, with a reason), so a caller can branch without parsing messages.

7. Test it​

  • Permission map — after login, assert the expected action is present: result.permissions.includes('accessTestPrivate').
  • Refusals — call with $meta.expect: ['<error>'] and assert the thrown error's type, so the test documents the expected failure instead of crashing.
  • Group wiring — a test group runs only when the platform that owns it lists it in integration.watch.test.

8. Troubleshooting​

SymptomLikely cause
403 with a valid token, empty action listthe materialized paths are stale — run CALL access_pathRefresh()
A role exists but has no rownot possible any more: a taken bit is refused; older databases may hold such a ghost
A token still authorizes old permissionsthe authorization cache TTL; the token's bits resolve against the current mapping
A capability has no effectits action resource name differs — matching ignores dots, nothing else