BetterIAM
Governance

Certifications

Access certification campaigns where reviewers or managers keep or revoke each binding, with reminders, usage-based recommendations, and auto-close.

Regulations and customers often require proof that someone regularly checks who has access to what, and removes what is no longer needed. Doing that in a spreadsheet is slow, and the decisions rarely make it back into the system.

An access turns the review into a recorded decision. It takes a snapshot of the role under review and asks reviewers to keep or revoke each one. When it closes, it applies the result: revoked bindings are removed and every item records what happened. Reviewers can be named people or each person's manager, recommendations put usage evidence beside every item, and campaigns can close themselves when they are due.

Create a campaign

Open one campaign per review cycle, such as a quarterly review of the administrator roles. certifications.create opens it and needs iam:certifications:manage. It returns the campaign with items, the number of bindings it covers.

Quarterly review of the admin roles
const campaign = await iam.api.certifications.create(credential, {
  tenantId,
  name: 'Q3 admin review',
  roleIds: [admin.id, billingAdmin.id],
  reviewerMode: 'manager',
  reviewerIds: [securityLead.id], // items without an active manager go here
  dueAt: Date.parse('2026-10-15T17:00:00Z'),
  autoClose: true,
  undecided: 'revoke',
});
// campaign.items: how many bindings the campaign covers

Route: POST /api/iam/certifications/create

Prop

Type

The campaign snapshots the live bindings of every non-protected role, or of the listed roles and subject type. It covers at most 5000 bindings (LIMIT_EXCEEDED otherwise; narrow it by role or subject type). Each item records the role, the subject, the binding's end, and whether it is eligible.

When email delivery is configured, each reviewer receives a certification-review email so they know they have work. It is queued in the campaign's transaction and carries campaignId, campaignName, items (their own item count), and dueAt when set.

Decide

Reviewers go through their items and record keep or revoke, optionally with a note that explains the decision. certifications.decide records decisions in batches of 1 to 200 items and returns how many it recorded:

await iam.api.certifications.decide(reviewerCredential, {
  tenantId,
  campaignId: campaign.id,
  decisions: [
    { itemId: 'item-1', decision: 'keep' },
    { itemId: 'item-2', decision: 'revoke', note: 'Moved to finance in July' },
  ],
});

Route: POST /api/iam/certifications/decide

  • decide needs iam:certifications:review. When the campaign names reviewerIds, only they may decide.
  • Nobody decides on their own access, directly or through a group (SELF_REVIEW), because a review of yourself is no review.
  • A decision can be changed until the campaign closes; a closed campaign refuses decisions (CONFLICT).

To follow progress, certifications.list lists campaigns (optionally by status) and certifications.get returns one campaign with its items. With mine: true, get leaves out items about the caller's own access, which they may not decide. Both report progress (total, decided, keep, revoke) and need iam:certifications:read.

Manager reviews

The best reviewer for a person's access is usually their manager. With reviewerMode: 'manager', each person's items go to their active manager (Identity.managerId, which SCIM can maintain). Items without one, and group items, go to the named reviewers, or to anyone holding iam:certifications:review when none are named.

Managers do not need an administrator role for this:

  • certifications.listMine({ tenantId }) returns the open campaigns with items assigned to the caller, which is all a manager's review screen needs.
  • certifications.review({ tenantId, campaignId, decisions }) records the manager's decisions. It needs no certification permission, only an ordinary session of the tenant (not ) and the assignment, and is audited as certification:review.
  • Through decide, a manager-assigned item may be decided only by that manager or by a holder of iam:certifications:manage.
  • Each reviewer is emailed once with their own item count.

Recommendations

Reviewers approve almost everything when they have nothing to go on. Recommendations give them evidence. roleMining.reviewRecommendations({ tenantId, campaignId, unusedDays? }) (iam:analysis:read) suggests a decision for every item, with a reason:

const { usageComplete, recommendations } = await iam.api.roleMining.reviewRecommendations(credential, {
  tenantId,
  campaignId: campaign.id,
});
// recommendations: [{ itemId, recommendation: 'keep' | 'revoke' | 'none', basis, reason, lastUsedAt }]

Route: POST /api/iam/roleMining/reviewRecommendations

  • revoke when the account is disabled, deleted, or expired, or when the person did not use the role within unusedDays (90 by default); keep when they did.
  • The evidence (basis) is recorded access usage once it covers the whole window (usageComplete), otherwise the person's last sign-in, or status for an inactive account.
  • Group items and service accounts without usage data get none.

Reviewers still decide. The console's campaign page shows the suggestion and its reason next to each open item.

Remind

Reviews stall when reviewers forget. certifications.remind({ tenantId, campaignId }) (iam:certifications:manage) emails every reviewer who still has undecided items a certification-reminder with their pending count, and returns { reminded, pending }. Send one a few days before dueAt. It needs an email delivery callback (DELIVERY_REQUIRED otherwise), refuses closed campaigns, and is audited as certification:remind.

renderDeliveryMessage from better-iam/auth/templates, the built-in email renderer, renders both the certification-review and certification-reminder emails. Give it a links.certification({ tenantId, campaignId }) function that returns your campaign page URL, and the emails get a button to it.

Close

Closing is what makes the review count. certifications.close({ tenantId, campaignId }) (iam:certifications:manage, recent authentication) applies the campaign:

  • Revoked items, and undecided ones when undecided: 'revoke', are removed under the closer's . Each revocation is audited as iam:bindings:delete.
  • Every item records its outcome: kept, revoked, already-removed (the binding was gone already), or revocation-failed (a binding a higher authority granted, left for that administrator). The result carries the counts per outcome.
  • Enforced (change safety) guard closing a campaign like any other access change.

Closed campaigns keep their record, with every decision and outcome, as evidence for auditors. certifications.delete removes a campaign and its items once you no longer need it; it only accepts closed campaigns.

Auto-close

A campaign with autoClose: true (and dueAt) closes itself when it is due. The deployment job iam.closeOverdueCertifications({ tenantId? }), or CLI close-certifications, finds every due campaign and applies it:

  • Each campaign closes in its own transaction, under the creator's grant authority. Revocations the creator could not make, or every revocation when the creator no longer exists, are reported as revocation-failed.
  • Each close is audited as certification:auto-close (with the outcome counts) by deployment-operator, plus one iam:bindings:delete per removed binding.
  • It returns { closed, skipped }, where skipped counts auto-closing campaigns that are not due yet. It needs no credential and no email transport.
Scheduler, daily
better-iam close-certifications --config better-iam.config.mjs

Enforced invariants do not guard scheduled jobs such as auto-closing campaigns; the invariant monitor reports what they break.

Options

Certifications have no deployment option: the certifications group is always available and its collections are part of the ordinary migration (npx better-iam migrate or iam.initialize()). Each campaign is configured by the certifications.create fields in Create a campaign. Reviewer emails need authentication.sendEmail and the outbox job; scheduled closing needs iam.closeOverdueCertifications() (CLI close-certifications).

Schema

CollectionOne record perKey fields
certificationCampaignsCampaignname, status (open, closed), roleIds, subjectType, reviewerIds, reviewerMode (named, manager), dueAt, autoClose, undecided (keep, revoke), createdBy, closedAt, closedBy
certificationItemsBinding under reviewcampaignId, bindingId, roleId, roleName, subjectType, subjectId, subjectName, bindingExpiresAt, eligible, reviewerId, decision, decidedBy, decidedAt, note, outcome (kept, revoked, already-removed, revocation-failed), outcomeDetail

Items are a snapshot taken when the campaign opens; later bindings are not added. certifications.delete removes a closed campaign with its items.

Routes

Every method is a POST under the handler's base path (default /api/iam) and needs a credential. The browser client calls the same routes as client.certifications.<method>.

MethodRouteMethodRoute
certifications.createPOST /api/iam/certifications/createcertifications.listPOST /api/iam/certifications/list
certifications.getPOST /api/iam/certifications/getcertifications.listMinePOST /api/iam/certifications/listMine
certifications.decidePOST /api/iam/certifications/decidecertifications.reviewPOST /api/iam/certifications/review
certifications.remindPOST /api/iam/certifications/remindcertifications.closePOST /api/iam/certifications/close
certifications.deletePOST /api/iam/certifications/delete

iam.closeOverdueCertifications() is the scheduled auto-close job, not a route.

Errors

CodeStatusWhen
INVALID_INPUT400An invalid subjectType, reviewerMode or undecided; a dueAt in the past; autoClose without dueAt; 0 or more than 200 decisions in one call
LIMIT_EXCEEDED409The campaign would cover more than 5000 bindings
SELF_REVIEW403A reviewer decided an item about their own access
ACCESS_DENIED403The caller is not a reviewer of the campaign, the item is assigned to someone else, or the review comes from a role session, session token, or another tenant
IMPERSONATION_RESTRICTED403Deciding from a "view as" session
CONFLICT409Deciding, reminding or closing a closed campaign, or deleting an open one
NOT_FOUND404An unknown campaign or item
DELIVERY_REQUIRED400remind without an email delivery callback
RECENT_AUTH_REQUIRED403close without a recent sign-in

Was this page helpful?

Better IAM is created by Sean Filimon

Last updated

On this page