Permission catalog
The actions and resource types policies may name, from built-in iam:* actions to product, plugin, and tenant-defined types and their resources.
refer to and
by name. Without a list of valid names, a typo such as document:read instead
of documents:read would be stored happily and then silently match nothing, and nobody would notice until someone
was refused. The is that list: the set of action names a
policy may use, and the those actions apply to.
The catalog contains the built-in iam:* actions, your product's actions,
actions, and, when you enable it, actions that define for themselves. For each
resource type it also records which attributes policies can test, which relations people can hold on it, and
whether IAM stores its records.
Registering an action grants nothing. It only makes the name valid in policies; access still comes from .
Configure the catalog
You declare your product's part of the catalog once, in the permissions option, next to the rest of your
configuration. List every action your code checks, grouped by the resource type it applies to:
import { betterIam } from 'better-iam';
import { sqliteAdapter } from 'better-iam/adapter-sqlite';
export const iam = betterIam({
database: sqliteAdapter({ filename: './iam.db' }),
secret: process.env.BETTER_IAM_SECRET!,
baseURL: 'https://app.example.com',
permissions: {
mode: 'tenant-defined', // or 'catalog' (default) to allow only developer-defined names
actions: ['reports:export'], // actions without a resource type
resourceTypes: {
document: { actions: ['documents:read', 'documents:write'], attributes: { classification: 'string' } },
project: { managed: true, actions: ['projects:read', 'projects:manage'], attributes: { archived: 'boolean' } },
task: { managed: true, parent: 'project', actions: ['tasks:read', 'tasks:write'] },
},
identityAttributes: { department: 'string', contractor: 'boolean' },
},
});Prop
Type
Each resource type accepts:
Prop
Type
Plugins contribute actions and platform resource types the same way (actions and resourceTypes on the plugin object), validated exactly like your own. Configuration errors fail at startup with INVALID_CONFIG:
- Action names cannot start with
iam:ortenant/, and cannot contain*,?, or whitespace. - Resource type names match
^[a-z][a-z0-9-]{0,63}$, and a name declared twice (by you or a plugin) is rejected. - These names are reserved for the platform:
iam,role,oauth-client,scim,saml,ssf,tenant,identity, andsession.
How documents are validated
The catalog pays off when a document is saved: mistakes are refused on the spot instead of surfacing later as unexplained denials. Policy documents are validated against the catalog when they are stored:
- An exact action name that does not exist is rejected with
INVALID_ACTION. - Once
resourceTypesis configured, an exact resource type in a resource pattern must also exist (INVALID_RESOURCE_TYPE). The platform's own types, such asiam, are always accepted. - Wildcard patterns such as
documents:*or*/reportare stored as written, without being resolved.
The same validation applies to every document the server stores, not only to policies attached to roles. That
includes inline role documents, (ceiling documents that cap what anyone can
reach, set on a tenant or on one person), ceilings (the cap on what a
delegated administrator may hand out), trust ceilings, and session policies, including the policy compiled from
an API key's scopes.
At request time, iam.authorize denies an action that is not in the tenant's catalog with the decision reason
UNKNOWN_ACTION.
Built-in actions
Every operation of the platform is authorized with an iam:* action, so administration is delegated with the
same policies as your product. The built-in actions are:
| Area | Actions |
|---|---|
| Tenants | iam:tenants:create, iam:tenants:read, iam:tenants:update, iam:tenants:delete |
| Identities | iam:identities:create, iam:identities:read, iam:identities:update, iam:identities:delete, iam:identities:impersonate |
| Groups | iam:groups:create, iam:groups:read, iam:groups:update, iam:groups:delete |
| Roles | iam:roles:create, iam:roles:read, iam:roles:update, iam:roles:delete, iam:roles:assume, iam:roles:revoke-sessions |
| Policies | iam:policies:create, iam:policies:read, iam:policies:update, iam:policies:delete, iam:policies:simulate |
| Bindings | iam:bindings:create, iam:bindings:read, iam:bindings:delete, iam:bindings:activate, iam:bindings:approve |
| Access packages | iam:packages:create, iam:packages:read, iam:packages:update, iam:packages:delete, iam:packages:assign, iam:packages:request, iam:packages:approve |
| Access requests | iam:access-requests:create, iam:access-requests:read, iam:access-requests:review |
| Delegation | iam:authorities:create, iam:authorities:revoke, iam:boundaries:update, iam:root:grant |
| Catalog | iam:actions:create, iam:actions:read, iam:actions:delete, iam:resource-types:create, iam:resource-types:read, iam:resource-types:update, iam:resource-types:delete |
| Resources | iam:resources:create, iam:resources:read, iam:resources:update, iam:resources:delete |
| Relationships | iam:relationships:create, iam:relationships:read, iam:relationships:delete |
| Credentials and trust | iam:credentials:create, iam:credentials:read, iam:credentials:revoke, iam:trust:create, iam:trust:read, iam:trust:update, iam:trust:revoke, iam:assertions:create |
| Temporary credentials | iam:session-tokens:create, iam:oidc-providers:create, iam:oidc-providers:read, iam:oidc-providers:update, iam:oidc-providers:delete |
| Configuration | iam:config:read, iam:config:apply |
| Audit and webhooks | iam:audit:read, iam:webhooks:create, iam:webhooks:read, iam:webhooks:update, iam:webhooks:delete |
| OAuth provider | iam:oauth:clients:create, iam:oauth:clients:read, iam:oauth:clients:update, iam:oauth:clients:delete, iam:oauth:grants:read, iam:oauth:grants:revoke |
| SCIM | iam:scim:connections:create, iam:scim:connections:read, iam:scim:connections:delete, iam:scim:credentials:create, iam:scim:credentials:revoke, iam:scim:mappings:update, iam:scim:targets:create, iam:scim:targets:read, iam:scim:targets:update, iam:scim:targets:delete, iam:scim:targets:sync |
| SAML | iam:saml:connections:create, iam:saml:connections:read, iam:saml:connections:update, iam:saml:connections:delete |
| Shared Signals | iam:ssf:streams:create, iam:ssf:streams:read, iam:ssf:streams:update, iam:ssf:streams:delete |
| Domains | iam:domains:create, iam:domains:read, iam:domains:update, iam:domains:delete |
| Governance | iam:analysis:read, iam:analysis:update, iam:certifications:read, iam:certifications:review, iam:certifications:manage, iam:sod:read, iam:sod:manage, iam:invariants:read, iam:invariants:manage, iam:agreements:read, iam:agreements:manage |
| Network security | iam:security:read, iam:security:manage |
Most names say what they allow: create, read, update, and delete on the area's records. The less obvious
ones:
iam:identities:impersonatestarts a "view as" session for a member.iam:roles:assumestarts a role session through a , andiam:roles:revoke-sessionsends the live role sessions of a role.iam:bindings:activateactivates an eligible binding one holds, andiam:bindings:approvedecides activation requests (Just-in-time elevation).iam:packages:assign,iam:packages:request, andiam:packages:approvegrant, ask for, and decide on access packages.iam:boundaries:updatesets tenant and principal boundaries, andiam:root:grantmakes someone a root administrator; both are for root administrators only.iam:assertions:createissues signed for downstream services, andiam:session-tokens:createissues short-lived session tokens from one's own session or API key.iam:oidc-providers:*registers external OpenID Connect providers whose tokens can be exchanged for role sessions.iam:config:applyapplies configuration as code, andiam:analysis:updatesuppresses access-analysis findings.
To see the whole catalog of a tenant, call iam.api.actions.list. It returns every action a policy in that
tenant may name, each marked platform (declared in configuration or built in) or tenant (registered by the
tenant), with the resource type it belongs to. Use it to build a policy editor or to check which names exist.
Administrative resources
Giving someone iam:roles:update on * lets them edit every role in the tenant. Often you want less: a team
lead who manages only their team's roles, or a project admin who registers only that project's tasks. That is
possible because every administrative operation is evaluated against an iam/... resource naming exactly what it
touches, and resource patterns can narrow it:
| What the operation does | Checked against |
|---|---|
Creates something tenant-wide or reads across the tenant: creating roles and policies (roles.create, policies.create), listing policies (policies.list), registering resource types (resourceTypes.register), asking who can reach a resource (policies.whoCan) | iam/{tenantId} |
Reads or changes one role (roles.get, roles.update, roles.delete), binds it to someone or edits such a binding (bindings.create, bindings.update, both under iam:bindings:create), activates it or approves an activation (bindings.activate, bindings.approveActivation) | iam/{roleId} |
Reads, edits, rolls back, or deletes one stored policy (policies.get, policies.update, policies.restoreVersion, policies.delete) | iam/{policyId} |
Removes one binding (bindings.delete) | iam/{bindingId} |
Renames a group, changes its members, or deletes it (groups.update, groups.addMember, groups.removeMember, groups.delete) | iam/{groupId} |
Registers, reads, changes, or deletes one managed resource (resources.register, resources.get, resources.update, resources.delete), or shares it (relationships.create) | iam/{type}/{id} |
Lists resources or relationships (resources.list, relationships.list) | iam/{type}/* with a type filter, otherwise iam/* |
Starts a role session (roles.assume, evaluated in the caller's own tenant) | iam/{roleId} of the target role |
Acts on one identity: delegating authority to it, issuing it an API key, or simulating its access (authorities.create, credentials.create, policies.simulate, policies.effectiveActions) | iam/{identityId} |
Creates, lists, or reports on separation-of-duties rules (sod.create, sod.list, sod.violations); changes or deletes one (sod.update, sod.delete) | iam/sod/*; iam/sod/{ruleId} |
For example, this policy lets its holders register and update the tasks of one project, and nothing else:
{
"version": 1,
"statements": [
{
"sid": "RegisterApolloTasks",
"effect": "allow",
"actions": ["iam:resources:create", "iam:resources:update"],
"resources": ["iam/task/apollo-*"]
}
]
}Resource types and resources
A decision needs facts about the resource: which tenant it belongs to (so nobody reaches another tenant's data),
who owns it, and the attributes conditions test. Before evaluating a request, the server resolves the
resource to learn these facts. Resource patterns in policies then match type/id within that already-resolved
tenant; * and ? are anchored glob wildcards, not regular expressions, and they cannot change the target
tenant.
Where the facts come from depends on the type, which is either application-owned or managed:
| Application-owned | Managed | |
|---|---|---|
| Records live in | Your database | Better IAM's database |
| Resolved by | Your resolveResource callback | The IAM registry, no callback |
| Owner and parent | Whatever your resolver returns as attributes | Built in: ownerId and parentId, validated on registration |
| Relationship tuples | Accepted for the resource as named | Accepted once the resource is registered |
Listable with listAccessible | No | Yes |
| Choose it when | Your product already stores the records and their tenant | You want IAM to hold ownership and sharing, or to list what a person may open |
Application-owned types
Application-owned types are resolved by your resolveResource callback. It receives the requested tenant, type,
and ID, and must load ownership and attributes from trusted storage. Types that are not declared at all are still
passed to resolveResource, so existing integrations keep working; declaring them adds validation and documents
the attribute schema.
import { betterIam, IamError } from 'better-iam';
export const iam = betterIam({
// ...database, secret, baseURL, permissions
async resolveResource({ type, id }) {
if (type !== 'document') throw new IamError('NOT_FOUND', 'Unknown resource type', 404);
const document = await db.documents.findById(id);
if (!document) throw new IamError('NOT_FOUND', 'Document not found', 404);
return {
tenantId: document.organizationId, // from your storage, never from the request
type,
id,
attributes: { classification: document.classification, ownerId: document.ownerId },
};
},
});Never echo the requested tenant
The resolver is what proves a resource belongs to the tenant being authorized. Never copy the request's tenant ID
into a fetched record to satisfy it. When the returned tenant, type, or ID differs from the request, the check
fails with RESOURCE_MISMATCH (403). Without a resolver, application resources fail with
RESOURCE_RESOLVER_REQUIRED.
Managed types
Managed types (managed: true, and every tenant-defined type) are registered with IAM, so authorization resolves
the registration and no application callback is involved. Your code tells IAM when a resource is created, changed,
or deleted, through iam.api.resources. resources.register records a new
resource with its attributes, owner, and parent:
await iam.api.resources.register(credential, {
tenantId,
type: 'project',
id: 'apollo',
attributes: { archived: false },
ownerId: alice.id,
});
// A task needs its registered parent project.
await iam.api.resources.register(credential, { tenantId, type: 'task', id: 'apollo-42', parentId: 'apollo' });- Attribute values are validated against the declared schema (string values up to 2048 characters).
ownerIdmust be an identity of the same tenant.- Types with a
parentrequire a registered parent resource of the declared parent type; a type without one refusesparentId. - Registering a resource requires
iam:resources:createoniam/{type}/{id}, so policies can scope who may register which resources. Registering the same resource twice fails withCONFLICT. resources.updatekeeps the registration in step with your data: it replaces the attributes (validated again) and sets or clears (ownerId: null) the owner. Call it when a document is reclassified or changes hands.resources.getreturns one registration, andresources.listlists them for administration screens, filtered bytype,parentId, andownerId, ordered by type and ID, withlimit(default 100, at most 1000) andoffset. To list what a person may open, uselistAccessibleinstead.resources.deleteremoves a registration when the resource is deleted. It refuses while child resources exist (RESOURCE_IN_USE) and removes the resource's relationships with it.resources.registerManyregisters up to 100 resources in one transaction, for imports and backfills; see Batches and reverse queries.
A tenant's resources plan limit, when set, caps how many it may register (LIMIT_EXCEEDED). Managed resource
registrations are trusted authorization inputs: the iam:resources:* permissions decide who may register or
edit them.
Only managed types can be listed with listAccessible,
the behind list pages. For application-owned types, check the IDs
your product already has with authorizeMany.
What policies see
The resolved resource becomes context for , the tests inside policy statements. The below is the caller:
| Key | Source |
|---|---|
resource.{name} | Attributes from the resolver or the registration. |
resource.ownerId, resource.parentId, resource.parentType | Managed resources, when set. |
resource.tenantId | The tenant the resource belongs to. |
resource.relations, resource.parentRelations | the principal holds on the resource and its parent. See Relationships. |
For administrative actions on iam/{type}/{id} that name a registered managed resource, these keys describe that
resource, so administration such as sharing can be conditioned on ownership and relations. The full list of keys
is on the Conditions page.
Tenant-defined catalogs
Some products let each customer model their own objects: a workflow tool where one customer tracks invoices and
another tracks shipments. You cannot declare those types in advance. With mode: 'tenant-defined', tenant
administrators extend the catalog for their own organization, and the new types and actions work in their
policies exactly like yours. Tenant types are always managed, so IAM stores their records.
resourceTypes.register creates a type with its actions, attributes, and relations in one call, and
actions.register adds one more action to an existing tenant type:
await iam.api.resourceTypes.register(credential, {
tenantId,
name: 'invoice',
description: 'Supplier invoices',
actions: ['read', 'approve'], // registers invoice:read and invoice:approve
attributes: { amount: 'number', currency: 'string' },
relations: ['approver'],
});
await iam.api.actions.register(credential, { tenantId, name: 'invoice:export', description: 'Download as CSV' });- Tenant type names cannot collide with reserved names, platform resource types, or the namespace of any platform
action (
documentsis taken bydocuments:read), and fail withINVALID_RESOURCE_TYPE. A name the tenant already registered fails withCONFLICT. - A
parentmust be an existing managed type. - Tenant-defined actions are namespaced under a tenant-defined type as
{type}:{verb}.actions.registerand theactionslist onresourceTypes.register(verbs only) create them; the type must be registered first (INVALID_ACTIONotherwise). resourceTypes.updateevolves a type: it changes the description, attributes, and relations and adds verbs; existing verbs are kept. A new attribute schema must still accept every registered resource of the type, and a relation still held by someone cannot be dropped (RESOURCE_IN_USE).- Deleting a type or action is refused while resources or policies still use it.
resourceTypes.deleteremoves a type once its resources, relationships, and child types are gone.actions.unregisterremoves one action and refuses while a policy or inline role document names it (RESOURCE_IN_USE). - With
mode: 'catalog', every tenant registration fails withCATALOG_LOCKED(403).
The permissions are iam:resource-types:create, read, update, and delete, and iam:actions:create, read,
and delete. resourceTypes.list returns platform and tenant types together, each with its source, and
resourceTypes.get returns one of them with its actions, attributes, and relations.
Configuration as code carries tenant-defined resource types (with actions as verbs), so a tenant's catalog can
live in version control with the roles that use it. See
Configuration as code.
Identity attributes
Roles answer "what job does this person do?". Some rules depend on facts about the person instead: their department, whether they are a contractor, their clearance level. Identity attributes carry those facts, so one statement can cover everyone in finance without a role per department.
Declare the attributes in permissions.identityAttributes. An administrator then sets them on a person with
identities.update (or on a service account with serviceAccounts.update), and policies read them as
principal.{name}:
await iam.api.identities.update(credential, { tenantId, identityId, attributes: { department: 'finance' } });{ "StringEquals": { "principal.department": "finance" } }They cannot shadow the built-in principal keys such as principal.id, principal.mfa, or principal.roles, nor
the session keys such as principal.mfaTime or principal.sessionTags; declaring one fails at startup with
INVALID_CONFIG.
Next steps
Better IAM is created by Sean Filimon
Last updated
Authorization
How Better IAM decides whether a principal may perform an action on a resource, and where roles, policies, boundaries, and relationships fit in.
Roles and bindings
Custom roles built from permission lists or policy documents, role inheritance, bindings to people and groups, and delegated grant authorities.