Skip to main content
The access model is in beta.
Hand-written read and write rules work well for one or two tables. Once you have organizations, projects inside them, tasks inside projects, and people who hold different roles at each level, the same checks repeat in every rule. defineAccessModel from bijection/server lets you declare that structure once. It generates the bodies of your read, disclosure and write rules and gives your functions helpers to check permissions and manage grants. It is a library that runs inside your own rules, not a separate service: the rules are still queries you export, and Bijection enforces them as it does any other rule.

Concepts

The model has five parts.
  • Types. Everything you protect is an object of a type. A type is either backed by a table, where each row is an object, or key-only, such as an organization or a group, whose objects exist only as keys in grants. Types form a tree through a parent field on each row. A type without a parent hangs from the built-in root object, whose key is *.
  • Permissions. A permission is one capability on one type, written type:name, such as task:read or project:grant.viewer. Your code checks permissions, never roles.
  • Roles. A role is a named bundle of permissions on its own type and on descendant types. project:owner including task:* means a project’s owner can do anything to its tasks.
  • Grants. A grant says that a user, or the members of a group, hold a role on one object. Grants are rows of a grants table. A row field can also confer a role, such as a task’s assignee field conferring task:assignee on the user it names.
  • Restrictions. A restriction makes an object require its caller to pass another object, whatever roles they hold. The common case is a tenant: every project belongs to an organization, and only people who pass that organization can reach its projects.
Users are identified by their tokenIdentifier (see Auth in Functions). Grants and row fields that name users store that string.

Declaring a model

bijection/accessModel.ts
defineAccessModel checks the declaration and throws if it is inconsistent, for example if a role names an undeclared permission, if the parents don’t form a tree, or if a role grants a read permission without the permission that makes the object visible.

Types

Roles

A role is a list of permissions, written type:name, type:* or type:grant.*. It can also be an object:

Restrictions and categories

categories names each restriction category and how it combines. In an "all" category a caller must pass every restricting object of that category an object carries; in an "any" category, at least one. Passing an object means holding its pass permission. A tenant is a restriction in an "any" category, as tenant above. Markings, such as data:pii, are key-only types listed in a row’s restriction field; adding a marking needs apply on it and removing one needs declassify.

Wiring the rules

The model supplies rule bodies. Export each one as a query, one set per type and one for the grants table:
bijection/access.ts
Then attach them in your schema. access.grantsTable() defines the grants table with its indexes, and access.inputs() lists the policy inputs every rule of the model reads:
bijection/schema.ts
The grants table’s write rule uses basis: "original", so the authority to grant a role must exist before the transaction that grants it. Parent fields hold the parent’s key: an ID for a table-backed parent, any string for a key-only one. Each table needs the index its parent names, and each scalar row-grant field a by_<field> index for access.visible. The model doesn’t see your schema, so a missing index fails when a query needs it.
A generated read rule decides a range pinned on the parent field the same way for every row. Add rowsFollowRange: access.rowsFollowRange("task") to the table’s .access to let large ranges be decided once. See Large ranges.

What the generated rules decide

Reads. A read of the whole document needs every read permission of the type. If a type puts a field in a read permission of its own, for example "read.finance": ["budget"], a caller without it can read the other fields only by selecting them. Writes. Grants. Inserting a grant of role R on an object requires grant.R on it and respects the role’s maxDuration and justification. Grants are never edited: a change is a revocation plus a new grant. Revoking a grant needs the same grant.R permission, except that users may always remove their own grant and anyone may remove a grant that has expired. The last administrator of a top-level object can’t be removed.

Checking permissions in functions

The rules enforce access. In your functions, the model’s helpers answer questions about it, for example to show or hide a button:
bijection/projects.ts
The helpers run as ordinary reads by the caller, so they only see what the caller may read. If a helper needs a row the caller can’t read, the calling function is refused rather than given a guess. Their answers are advisory; the rules decide.

Granting and revoking

Grant a role with access.grant inside a mutation. The grants table’s write rule checks the caller’s authority when the mutation commits:
bijection/sharing.ts
A grant to the members of a group uses subject: { type: "group", key, permission: "member" }, with the group type listed in the model’s groups: { types: ["group"] }. A grant with expiresAt needs the expire option: access.grant schedules your expireGrant mutation at that time, so the grant’s deletion is an ordinary commit and live queries lose access when it happens. reason records why the grant was made. To delete an object, first revoke every grant on it with access.revokeAll(ctx, type, key); the grants write rule decides each revocation, so the caller needs the authority to revoke them.

The first administrator

In a fresh deployment nobody holds any role, so nobody could grant one. While the grants table and every table of the model are empty, a signed-in caller may insert exactly one grant: an administering role of a top-level type, such as org:admin, to themselves.

Model options