Sharing and access questions
Recipes for sharing with relationships, answering who can do what, testing and rolling back policies, time-bound roles, access paths, and guardrails.
These recipes cover the day-to-day authorization work of a multi-tenant product: letting people share their own
things, answering access questions for reviewers, changing policies without surprises, and shaping when access
applies. credential is the caller's credential ({ token } or { headers }), and every call is authorized,
applied in one transaction, and audited like the rest of the API.
Share a resource with relationships
The problem: people want to share a folder with a colleague or a team, and editing a for every share does not scale.
The solution: use (relationship-based access control, ReBAC). Declare which relations a resource type supports, and write one that turns relations into permissions. From then on, sharing is creating one relationship: a record that says "this group is a viewer of this folder".
// In the betterIam() options: the resource types and the relations they support.
permissions: {
resourceTypes: {
folder: { managed: true, actions: ['folders:read', 'folders:share'], relations: ['viewer', 'editor', 'owner'] },
file: { managed: true, parent: 'folder', actions: ['files:read'], relations: ['viewer'] },
},
}
// One role turns relations into permissions. Bind it to everyone once.
const sharing = 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'] } } },
{ effect: 'allow', actions: ['iam:relationships:create'], resources: ['iam/folder/*'],
conditions: { ArrayContains: { 'resource.relations': ['owner'] } } },
],
},
});
await iam.api.bindings.create(credential, {
tenantId, roleId: sharing.id, subjectType: 'group', subjectId: everyone.id,
});
// Owners share their own folders; the server checks their `owner` relation on iam/folder/{id}.
await iam.api.relationships.create(ownerCredential, {
tenantId, type: 'folder', id: 'plans', relation: 'viewer', subjectType: 'group', subjectId: designTeam.id,
});How it works:
- A relationship binds an identity or a group to one resource under a declared relation. The relations the caller
holds reach policies as
resource.relations, and relations on the parent resource asresource.parentRelations. So the file statement lets anyone who can see a folder read its files. ArrayContainsmatches when the caller holds any of the listed relations.- The third statement makes sharing self-service but safe: only people holding
owneron a folder may create relationships on it.
See Relationships and
relationships.create.
Answer "who can?" and "what can they do?"
The problem: a reviewer or support engineer asks "who can read this folder?" or "what can Alice do here?", and reading every role and policy by hand is slow and error-prone.
The solution: ask the real evaluator. policies.whoCan lists the people who could perform one action on a
resource, and policies.effectiveActions lists everything one person can do on it.
const { identities, total } = await iam.api.policies.whoCan(credential, {
tenantId,
action: 'folders:read',
resource: { type: 'folder', id: 'plans' },
assumeMfa: true,
});
const { allowed } = await iam.api.policies.effectiveActions(credential, {
tenantId,
identityId: alice.id,
resource: { type: 'folder', id: 'plans' },
});
// allowed: the sorted list of actions Alice may perform on the folderwhoCanlists every active identity that could perform the action on the resource, with the decision reason, and atotal. Root administrators are not listed, because their override applies everywhere. It scales with the directory size, so use it on review screens, not per request.effectiveActionsevaluates every catalog action (or up to 200 you pass) for one identity on one resource. It returns the sortedallowedlist plus a reason per action.- Both need
iam:policies:simulateand evaluate a synthetic session.assumeMfa: truesimulates an MFA session; otherwise conditions onprincipal.mfaseefalse. The results are and never grant anything.
See Authorization queries and
policies.whoCan.
Test a policy before saving it, and roll back a bad one
The problem: a policy edit can lock people out or open access too widely, and you only find out after saving it.
The solution: test the unsaved document against a concrete request with policies.test. If a bad version gets
through anyway, make an earlier version current again with policies.restoreVersion.
// candidate: the edited policy document, not saved yet
const decision = await iam.api.policies.test(credential, {
tenantId,
document: candidate,
action: 'documents:write',
resource: 'document/alice-notes',
context: { 'principal.id': 'alice', 'principal.mfa': true },
});
if (!decision.allowed) console.warn('The candidate would deny this request:', decision.reason);
// Later, if version 4 turned out to be wrong:
await iam.api.policies.restoreVersion(credential, { tenantId, policyId, version: 3 });policies.test(iam:policies:simulate) validates the unsaved document against the catalog. It then evaluates it for the action, the resource string, and a context you supply (at most 200 keys). Nothing is stored.policies.restoreVersion(iam:policies:update) makes an earlier document current again as a new version; the old document is re-validated against today's catalog. The protected owner policy cannot be restored this way.policies.listVersionslists the versions you can restore.
Try documents interactively in the policy playground. See Policies.
Role hierarchy and business-hours access
The problem: roles often build on each other (every editor should also read), and some access should apply only during working hours.
The solution: let one role inherit another instead of copying its permissions, and put an on the that should only apply at certain times.
const viewer = await iam.api.roles.create(credential, {
tenantId,
name: 'Viewer',
permissions: ['documents:read'],
});
const editor = await iam.api.roles.create(credential, {
tenantId,
name: 'Editor',
permissions: ['documents:write'],
inherits: [viewer.id], // editors read too
});
// Support staff hold their role Monday to Friday, 08:00 to 18:00, Berlin time.
await iam.api.bindings.create(credential, {
tenantId,
roleId: support.id,
subjectType: 'group',
subjectId: supportTeam.id,
window: { from: '08:00', to: '18:00', timeZone: 'Europe/Berlin', days: [1, 2, 3, 4, 5] }, // 0 is Sunday
});inheritslists up to 20 roles whose grants this role includes; cycles and protected roles are refused.- A binding with a
windowapplies only inside those hours in the named time zone; outside them it grants nothing. Withoutdays, the window applies every day. A window whosetois not afterfromwraps past midnight.
See Roles and Temporary access.
Project teams and start dates
The problem: a lot of access has a natural beginning and end, such as a three-month project or a contract that starts next quarter. Granting it by hand and remembering to remove it later is how standing access piles up.
The solution: put the dates on the itself. A membership or a binding can start and end by itself.
// A three-month project membership that ends by itself.
await iam.api.groups.addMember(credential, {
tenantId,
groupId: projectTeam.id,
identityId: alice.id,
expiresAt: Date.now() + 90 * 86400_000,
});
// Access that begins on the contract's first day and ends on its last.
await iam.api.bindings.create(credential, {
tenantId,
roleId: contractor.id,
subjectType: 'identity',
subjectId: bob.id,
startsAt: Date.parse('2027-01-04T08:00:00Z'),
expiresAt: Date.parse('2027-06-30T18:00:00Z'),
});- A future
startsAtmakes the binding visible with its start date, but it grants nothing until then. - An
expiresAtstops the grant at that instant; the retention worker removes the record later. An expiring group membership ends every grant and activation the membership carried.
See Temporary access.
Tell people how to get access instead of just "denied"
The problem: a bare "access denied" sends people to an administrator even when they could fix it themselves.
The solution: when a check fails, call accessPaths.find to ask what the person could do about it:
to MFA, accept the terms of use, activate an
, or request an .
Every option is verified by simulating it in a transaction that is rolled back, so the list never promises access
that would still be refused.
const check = await iam.authorize({ token, tenantId, action: 'documents:delete', resource });
if (!check.allowed) {
const { paths } = await iam.api.accessPaths.find(
{ token },
{ tenantId, action: 'documents:delete', resource },
);
for (const path of paths) {
if (path.kind === 'mfa') showStepUp();
if (path.kind === 'accept-agreements') showTerms(path.agreements); // then agreements.accept
if (path.kind === 'activate')
offerActivation(path.bindingId, path.role, path.requireJustification); // bindings.activate
if (path.kind === 'request-package') offerRequest(path.package); // packages.request
}
if (!paths.length) showAskAnAdministrator();
}- The call needs only the person's own ordinary session.
- Activation and package options appear only when the person holds
iam:bindings:activateoriam:packages:requestfor them. Approval requirements are reported, so the UI can say "your request goes to an approver". - Like
authorize, a denial'sreasonis alwaysACCESS_DENIED, so the call does not reveal which rule refused. - An empty list means only an administrator can help. The
useAccessPathshook wraps the call for React and Vue.
See Access paths.
Guardrails, terms of use, and change previews
The problem: some rules must hold however roles are edited ("sales never approves payments"), some access should wait until people accept the rules, and every role edit risks a surprise.
The solution: three governance features. An states a rule and refuses changes that break it. An shows who gains or loses what before you edit. An records who accepted your terms, and a policy can require it.
// Sales may never approve payments, whatever roles say; refuse changes that would break this.
// `department` must be a declared identity attribute.
await iam.api.invariants.create(credential, {
tenantId,
name: 'Sales never approves payments',
subject: { attribute: { name: 'department', value: 'Sales' } },
action: 'payments:approve',
resource: { type: 'ledger', id: 'main' },
expect: 'deny',
mode: 'enforce',
});
// Before editing a role: who gains or loses what, and which invariants would break?
const preview = await iam.api.impact.preview(credential, {
tenantId,
change: { role: { roleId, permissions: ['payments:read', 'payments:approve'] } },
resources: [{ type: 'ledger', id: 'main' }],
});
// Terms of use that policies can require (deny while principal.pendingAgreements > 0).
await iam.api.agreements.create(credential, { tenantId, name: 'Acceptable use', content: '...' });- Invariants with
mode: 'enforce'are re-checked around every access-changing operation. A change that would newly break one is refused withINVARIANT_VIOLATION(409) and rolled back.monitormode only reports; schedulemonitor-invariantsto be alerted. - Impact previews apply the change with the real validation and permissions inside a transaction that is always
rolled back, and evaluate every holder before and after.
preview.identitieslists who gains and loses which actions on each resource, andpreview.invariants.brokenthe guardrails the change would break. - Agreements are enforced by an ordinary policy statement, for example a deny on
documents:*whileprincipal.pendingAgreementsis greater than 0. Members accept withagreements.accept.
See Change safety and Terms of use.
Next steps
Better IAM is created by Sean Filimon
Last updated
Recipes
Short, copy-ready solutions for common identity and access problems, from sharing and MFA rules to offboarding, plan limits, and audit archiving.
Sign-in and devices
Recipes for per-organization MFA and sign-in method rules, remembering trusted browsers after MFA, device lists, sign-out everywhere, and incident response.