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.
Rendering a page usually needs many decisions: which buttons to show on a document, which menu entries to offer, which projects to list. Asking one at a time is slow, and listing resources one ID at a time is impossible when you do not know the IDs yet. Two calls cover these cases:
iam.authorizeManyanswers up to fifty checks in one round trip.iam.listAccessibleanswers "which of this type may I act on?" for managed resource types. Asking which resources a person can reach, rather than whether they can reach one, is called a .
Both are : like iam.authorize, they tell you what the caller could do right
now, to decide what to render. They do not protect anything by themselves, because access can change between
rendering a button and clicking it. The server must still call iam.require (or authorize) immediately before
performing each protected operation.
Check many actions at once
A document toolbar might need to know whether the person may edit, share, and delete. Three separate
authorize calls would work, but each is a round trip and each could see a slightly different configuration.
iam.authorizeMany({ ...credential, tenantId, checks }) evaluates 1 to 50 checks for one tenant in a single
transaction, so every answer reflects the same view of the configuration. Each check names an
and a resource. It is intended for rendering menus and lists.
const resource = { type: 'document', id: documentId };
const { results } = await iam.authorizeMany({
headers: request.headers,
tenantId,
checks: [
{ action: 'documents:write', resource },
{ action: 'documents:share', resource },
{ action: 'documents:delete', resource },
],
});
// results: [{ action, resource, allowed, reason }, ...] in the order of the checks
const can = Object.fromEntries(results.map((result) => [result.action, result.allowed]));Each check is evaluated and recorded exactly like iam.authorize: denials are audited and a denied check reports
reason: 'ACCESS_DENIED'. Fewer than one or more than fifty checks fail with INVALID_INPUT.
Use authorizeMany for application-owned resource types too: they are not enumerable, so check the IDs your
product already loaded.
List accessible resources
A "My projects" page cannot check projects one by one, because it does not know which project IDs to check. It needs the opposite question: of all projects, which may this person open? Conditions, group bindings, and shared folders all affect the answer, so filtering in your own database would duplicate your policies.
iam.listAccessible({ ...credential, tenantId, action, type }) answers it. It returns the registered resources of
one managed type that the caller may perform the action on:
const { resources, total } = await iam.listAccessible({
headers: request.headers,
tenantId,
action: 'projects:read',
type: 'project',
limit: 25,
offset: 0,
});
// resources: [{ type: 'project', resourceId: 'apollo', attributes, ownerId, parentId, ... }]
// total: how many projects are accessible in alllimit(default 100, at most 1000) andoffsetpage over the accessible set, ordered by type and ID, andtotalcounts all of it.- Everything a normal decision uses applies: conditions on attributes and owners, relationships, boundaries, access windows, and activations.
- Only managed types can be listed. An application-owned type fails with
INVALID_RESOURCE_TYPE, and an action outside the catalog withINVALID_ACTION. - In an impersonation session, only resources both the member and the impersonating administrator can reach are returned.
The result is advisory. List pages no longer need to know resource IDs up front, but opening a project still goes
through iam.require.
How batch evaluation works
A single decision loads the caller's state from storage: root override, tenant status, boundaries (ceilings on
what the caller can reach), grants through every binding and group, the evaluation context, and the caller's
relationship tuples.
listAccessible prepares that state once and evaluates every registration of the type against it, so grants
and boundaries are loaded once no matter how many resources the type has. The review calls
policies.effectiveActions and policies.whoCan work the same way, preparing once per identity. authorizeMany
evaluates each check in turn inside its one transaction.
Register resources in bulk
Lists only show what is registered, so adopting listAccessible for an existing product usually starts with a
backfill. resources.registerMany registers up to 100 managed resources in one transaction, which also suits
imports:
await iam.api.resources.registerMany(credential, {
tenantId,
resources: [
{ type: 'project', id: 'apollo', ownerId: alice.id },
{ type: 'project', id: 'gemini', attributes: { archived: true } },
{ type: 'task', id: 'apollo-1', parentId: 'apollo' },
],
});Every item is authorized as iam:resources:create on iam/{type}/{id} before anything is written. A denied item
rejects the whole batch with ACCESS_DENIED and records only its denial. Items are registered in order, so a
parent can precede its children in the same batch.
Render UI from decisions
Hiding a button a person cannot use is kinder than letting them click and see an error. The browser client
exposes the same calls as client.authorize, client.authorizeMany, and client.listAccessible, with the
session cookie as the credential, and the framework packages wrap them in hooks that re-run when the signed-in
person changes:
import { Can, useAccessible, useAuthorize } from '@better-iam/react';
export function DocumentToolbar({ tenantId, documentId }: { tenantId: string; documentId: string }) {
const resource = { type: 'document', id: documentId };
const { status, allowed } = useAuthorize({
tenantId,
checks: [
{ action: 'documents:write', resource },
{ action: 'documents:delete', resource },
],
});
if (status !== 'ready') return null;
return (
<div>
{allowed('documents:write', resource) && <button type="button">Edit</button>}
{allowed('documents:delete', resource) && <button type="button">Delete</button>}
{/* Without a resource, the check is on the tenant itself (iam/{tenantId}). */}
<Can tenantId={tenantId} action="iam:identities:create" fallback={null}>
<button type="button">Invite</button>
</Can>
</div>
);
}
export function ProjectList({ tenantId }: { tenantId: string }) {
const { status, resources, total } = useAccessible({ tenantId, action: 'projects:read', type: 'project', limit: 20 });
if (status !== 'ready') return <p>Loading…</p>;
return (
<ul aria-label={`${total} projects`}>
{resources.map((project) => (
<li key={project.resourceId}>{project.resourceId}</li>
))}
</ul>
);
}The helpers in the example:
useAuthorizesends all its checks in oneauthorizeManycall and re-runs when the checks or the signed-in identity change. Itsallowed(action, resource)is false until results arrive, and every check reads as denied when nobody is signed in.Can(React) anduseCan(Vue) wrap a single check.Canrenders its children when the check is allowed, itsfallbackwhen it is not, and itsloadingcontent while it waits. Without aresource, the check is on the tenant itself.useAccessiblewrapslistAccessiblethe same way and returns the page ofresourcesand thetotal, for list pages.
SvelteKit, Next.js, and the other integrations offer the same helpers; see Frameworks.
Choose the right call
| You need to | Use |
|---|---|
| Enforce access before a mutation or a read | iam.require |
| Branch on one decision on the server | iam.authorize |
| Show or hide several buttons or menu entries | authorizeMany, useAuthorize, Can, useCan |
| List the managed resources a person may open | listAccessible, useAccessible |
| Filter application-owned rows you already loaded | authorizeMany with their IDs, up to fifty per call |
| Tell a refused person how they could get access | accessPaths.find; see Self-service access paths |
| Show an administrator who can reach something | policies.whoCan; see Access reviews |
Next steps
Better IAM is created by Sean Filimon
Last updated
Relationships
Relationship-based access control. Declare relations on resource types, record who holds them, and write policies that read resource.relations.
Access reviews
Explain and review access without granting it. Simulate a decision, list who can act on a resource, see a person's effective actions, and scan for risk.