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
parentfield on each row. A type without a parent hangs from the built-inrootobject, whose key is*. - Permissions. A permission is one capability on one type, written
type:name, such astask:readorproject: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:ownerincludingtask:*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
assigneefield conferringtask:assigneeon 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.
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, writtentype: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
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
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.
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
Granting and revoking
Grant a role withaccess.grant inside a mutation. The grants table’s write
rule checks the caller’s authority when the mutation commits:
bijection/sharing.ts
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 asorg:admin, to themselves.