Change safety
Preview who gains and loses what before editing a role or policy, and enforce access invariants that no change may break.
Access changes are risky because their effect is hard to see. Adding one action to a role changes access for everyone who holds it, directly, through a group, or through a role that inherits it. Removing one can break a team's work. And some things must never happen however roles evolve: a contractor approving payments, the on-call team losing the ability to restart production.
Two tools make changes safe:
- An answers "who gains or loses what if I make this change?" before you make it.
- An writes down a line that must hold whatever roles and policies say. It reports when the line is crossed and, when enforced, refuses any change that would cross it.
Impact preview
Use a preview before editing a or that many people hold, or before deleting a role, to see exactly whose access changes on the resources you care about.
const preview = await iam.api.impact.preview(credential, {
tenantId,
change: { role: { roleId, permissions: ['payments:read', 'payments:approve'] } },
resources: [{ type: 'ledger', id: 'main' }],
});
// preview.identities: who gains and loses which actions on each resource
// preview.invariants.broken: guardrails the change would breakimpact.preview({ tenantId, change, resources, actions?, assumeMfa? }) needs iam:policies:simulate. change is
exactly one of:
| Change | What it simulates | Permission it also needs |
|---|---|---|
{ role: { roleId, ...update } } | A roles.update: new permissions, document, policyIds, or inherits. | iam:roles:update |
{ policy: { policyId, document } } | Replacing a policy's document. | iam:policies:update |
{ deleteRole: roleId } | Deleting the role. | iam:roles:delete |
How it works
The server makes the change the way the real call would, inside a transaction it always rolls back. The same validation, the same permission on the item, and the same edit rights apply: a role others inherit cannot be deleted, and a lower authority's role cannot be edited. Nothing is saved.
- Who is evaluated. The affected roles are the changed role, or the roles attaching the policy, plus every
role inheriting them. Their holders are active identities with a binding to them, directly or through a live
group membership, up to 200 (
truncatedis true when there were more). - What is evaluated. Each holder is checked before and after the change against each of the 1 to 10
resources, for every known action or only theactionsyou pass (up to 200).assumeMfaevaluates holders as if they had completed MFA, to see the most they could reach. - Everything counts. The ordinary evaluator runs, so conditions, authority ceilings, , , and just-in-time eligibility all count, exactly as they would in production.
The result names the affected roles and how many holders were evaluated, and lists, per holder and resource,
the actions gained and lost, with gainedTotal and lostTotal. Its invariants lists the
access invariants the change would newly break (broken, with the new violations) or make
pass again (fixed).
The console's Change impact page previews role permissions, policy documents, and role deletion.
Access invariants
Roles change all the time, and so do the people who edit them. Some rules should hold whatever anyone does: "contractors can never approve payments", "the on-call group can always restart production". An access invariant states such a rule as a check Better IAM can run: these people, this action, this resource, must be denied (or must be allowed).
await iam.api.invariants.create(credential, {
tenantId,
name: 'Contractors never approve payments',
subject: { attribute: { name: 'contractor', value: true } },
action: 'payments:approve',
resource: { type: 'ledger', id: 'main' },
expect: 'deny',
mode: 'enforce',
});Prop
Type
The calls:
invariants.createandinvariants.updatesave a rule (iam:invariants:manage) and return it with its current result, so you see immediately whether it already holds.invariants.run({ tenantId, invariantId? })(iam:invariants:read) evaluates one or all rules against the current configuration with the ordinary evaluator. Each result haspassed, theviolations(the person and the decision reason), and anerrorwhen the rule can no longer be evaluated, for example because its group or resource is gone.invariants.listreturns the rules with their last monitored outcome, andinvariants.deleteremoves one.
A tenant holds at most 100 invariants. When running or monitoring, at most 500 people are evaluated per invariant.
Enforcement
A monitored rule tells you after the fact. An enforced rule (mode: 'enforce') stops the change instead. It is
evaluated before and after every operation that can change access:
- role and policy edits and deletions;
- binding creation and deletion, just-in-time activation and approval;
- group membership changes, identity creation and attribute changes;
- package assignment and approval, configuration apply, access-request review;
- relationship and resource changes and deletions, boundary and authority changes, root grants;
- closing certification campaigns, publishing agreements, and
roleMining.apply.
Enforcement evaluates every subject, not only the first 500. When the operation newly breaks a rule, or leaves it
impossible to evaluate (for example by deleting the group or resource it names), the operation is refused with
INVARIANT_VIOLATION (409) and its transaction rolls back.
Changes made outside an operation are not guarded: scheduled jobs (purge, package reconciliation, auto-closing campaigns), inbound SCIM provisioning, and members accepting agreements. The monitor below reports what they break.
The console's Access invariants page lists rules with their status, toggles enforcement, and creates new ones.
Monitor and alert
iam.checkInvariants({ tenantId? }) (CLI better-iam monitor-invariants) is the scheduled check that tells you
when a rule starts failing. It is a deployment operation that evaluates every organization's invariants, or one
tenant's:
- It stores the outcome on each rule (
lastCheck:at,passed, and the violating identity IDs). - It records
invariant:broken(with the new violators, outcomedeny) when a rule starts failing or gains violators, andinvariant:restoredwhen it passes again, both asdeployment-operator. - Each change is reported once, so a webhook subscribed to
invariant:*alerts without repeating itself.
Run it hourly, and route the events to your alerting:
await iam.api.webhooks.create(credential, {
tenantId,
url: 'https://alerts.example.com/iam',
events: ['invariant:*'],
});Gate CI
better-iam check-invariants --tenant ID --fail-on-broken runs every rule as the BETTER_IAM_TOKEN holder
(iam:invariants:read), prints the result, and exits with INVARIANTS_BROKEN when any rule is broken or cannot be
evaluated. Run it after config-apply, so a deploy that crosses a line fails the pipeline.
BETTER_IAM_TOKEN=... better-iam config-apply --config better-iam.config.mjs --tenant TENANT_ID --input tenant.json
BETTER_IAM_TOKEN=... better-iam check-invariants --config better-iam.config.mjs --tenant TENANT_ID --fail-on-brokenInvariants as code
Invariants are part of configuration as code, so they can live in
version control next to the roles they protect. In a document, the subject names groups and people portably
({ "group": "Contractors" }, { "identity": "alice@example.com" }). Enforced invariants are checked around the
whole apply against the rules as they were before it, so relaxing a rule and making the change it forbade take two
applies.
Better IAM is created by Sean Filimon
Last updated
Certifications
Access certification campaigns where reviewers or managers keep or revoke each binding, with reminders, usage-based recommendations, and auto-close.
Terms of use
Versioned agreements such as acceptable-use policies that members accept, with enforcement through ordinary policy statements.