Relationships
Relationship-based access control. Declare relations on resource types, record who holds them, and write policies that read resource.relations.
work well for "all editors may edit all documents". They work badly for sharing: "Alice may edit this one folder, and the design team may view it". Writing a per folder does not scale, and people share things all day. Relationship-based access control (ReBAC) handles this case.
A (or tuple) records that a person or a
stands in a named relation to one : alice is an
owner of folder/plans, the design group are viewers of folder/plans. A role then says "viewers may
read" once, and sharing a folder becomes a matter of adding a tuple rather than editing a policy. Policies see the
relations the (the caller) holds as resource.relations.
With the tuples above, a policy evaluated for a member of the design group on folder/plans sees
resource.relations as ["viewer"], and on file/roadmap sees resource.parentRelations as ["viewer"].
Declare relations
First decide which relations make sense for each type of thing, such as viewer, editor, and owner for
folders. Declaring them up front means a typo like veiwer is refused instead of silently granting nothing. A
resource type declares the relations it supports in permissions.resourceTypes, in a plugin's resourceTypes,
or, for tenant-defined types, on resourceTypes.register and resourceTypes.update:
permissions: {
resourceTypes: {
folder: { managed: true, actions: ['folders:read', 'folders:share'], relations: ['viewer', 'editor', 'owner'] },
file: { managed: true, parent: 'folder', actions: ['files:read'], relations: ['viewer'] },
},
},Relation names are lowercase identifiers (a letter, then letters, digits, _, or -), at most 32 per type.
Tuples naming an undeclared relation are rejected, and a relation someone still holds cannot be dropped from its
type (RESOURCE_IN_USE).
Record relationships
Your product records a tuple whenever someone shares something or creates something they should own. Call
iam.api.relationships.create from the code path that handles the share:
await iam.api.relationships.create(credential, {
tenantId,
type: 'folder',
id: 'plans',
relation: 'viewer',
subjectType: 'group', // or 'identity'
subjectId: design.id,
expiresAt: Date.now() + 30 * 86_400_000, // optional
});- It requires
iam:relationships:createoniam/{type}/{id}, so who may share what is itself a policy decision. - The type must be known (
INVALID_RESOURCE_TYPE) and the relation declared for it (INVALID_INPUT). Managed resources must be registered (NOT_FOUND); application-owned resources are accepted as named. - The subject is an identity of the tenant (not deleted) or a group of the tenant.
expiresAt(epoch milliseconds, in the future, within ten years) makes the tuple temporary. An expired tuple grants nothing.- Creating a tuple that already exists updates it in place, replacing its expiry (or clearing it when you omit
expiresAt).
To show a "shared with" panel, call relationships.list. It returns tuples filtered by type, id, relation,
subjectType, and subjectId, newest first, and omits expired tuples unless you pass includeExpired: true. It
requires iam:relationships:read on iam/{type}/{id} when you filter by both type and ID, on iam/{type}/* when
you filter by type, and on iam/* otherwise.
To stop sharing, call relationships.delete({ tenantId, relationshipId }), which removes one tuple and requires
iam:relationships:delete on the tuple's resource.
Tuples are removed together with their identity, group, or managed resource. Role sessions hold no relations, even when the source identity does.
Use relations in policies
A tuple on its own grants nothing; a policy has to say what each relation allows. During evaluation, the
principal's live relations on the evaluated resource, held directly or through group
membership, appear as resource.relations, a sorted list of relation names. Those held on the resource's
registered parent appear as resource.parentRelations. Test them with the ArrayContains
, which holds when the principal has any of the listed relations:
await iam.api.roles.create(credential, {
tenantId,
name: 'Sharing',
document: {
version: 1,
statements: [
{
effect: 'allow',
actions: ['folders:read'],
resources: ['folder/*'],
conditions: { ArrayContains: { 'resource.relations': ['viewer', 'editor', 'owner'] } },
},
{
effect: 'allow',
actions: ['files:read'],
resources: ['file/*'],
conditions: { ArrayContains: { 'resource.parentRelations': ['viewer', 'editor', 'owner'] } },
},
],
},
});Bind a role like this to a group everyone belongs to. The role grants nothing by itself; the tuples decide which folders each person reaches.
Try the sharing role in the playground
In the playground, the context stands in for the tuples: resource.parentRelations is what the server would
derive from a viewer tuple on the file's folder. Empty the list and the read is no longer allowed.
resource.parentRelationslooks one level up, to the registered parent only. Relations on a grandparent are not consulted, so record tuples at the level your policies read.- Both keys are lists.
StringEqualsnever matches them; useArrayContains(any of) orArrayContainsAll(all of). The linter reports the mistake asarray-key-string-operator. - A principal without relations sees an empty list, so
ArrayContainsis false and the statement does not apply.
Let owners share
Sharing is only self-service if the people who own things can share them without being administrators. Recording a
tuple is itself an administrative action (iam:relationships:create), so the trick is a policy that allows it
only to owners of the resource.
For administrative actions on iam/{type}/{id} that name a registered managed resource, resource.relations,
resource.parentRelations, resource.ownerId, resource.parentId, and the registered attributes describe that
resource. That is what lets an owner share a folder without holding a tenant-wide administrative role:
{
"effect": "allow",
"actions": ["iam:relationships:create", "iam:relationships:delete"],
"resources": ["iam/folder/*"],
"conditions": { "ArrayContains": { "resource.relations": ["owner"] } }
}// The server checks the caller's owner relation on iam/folder/plans.
await iam.api.relationships.create(ownerCredential, {
tenantId,
type: 'folder',
id: 'plans',
relation: 'viewer',
subjectType: 'group',
subjectId: designTeam.id,
});The same works with registered ownership instead of a relation, using a
policy variable:
{ "StringEquals": { "resource.ownerId": "${principal.id}" } } on iam/folder/*.
Lists and reviews
Shared resources should show up in lists, and reviewers should see who reaches a resource through sharing.
see relationships too:
listAccessible
returns the folders a person can read through their tuples, and
policies.whoCan lists everyone who reaches a
folder through one.
const { resources, total } = await iam.listAccessible({ token, tenantId, action: 'folders:read', type: 'folder' });Next steps
Better IAM is created by Sean Filimon
Last updated
Conditions
Every condition operator, how values combine, negation and missing-key rules, and the context keys the server provides to policies.
Batches and reverse queries
Check many actions at once with authorizeMany, list the resources a caller may act on with listAccessible, and render UI from advisory decisions.