Access report
One document with what ends soon, what is elevated now, and which keys nobody uses, plus digest emails to owners and expiry reminders.
End dates and just-in-time roles only help if someone keeps an eye on them. A contractor whose account ends tomorrow may need an extension today; an administrator that has been live all week deserves a question; an API key nobody has used in months should probably be revoked.
The access report puts that picture in one document per organization. Two scheduled jobs deliver it for you: the access digest emails it to the organization's owners, and expiry reminders tell each person what of theirs ends soon, so they can ask for an extension before they lose access.
The report
reports.access builds the report for one organization. Use it for a Reports page, or run it nightly and post it
to a chat channel or ticketing system. The console shows it under Reports, and the CLI prints it with
better-iam report --tenant ID.
It covers:
- Identities ending within the window, including ones already past their deadline that the purge worker has not disabled yet.
- Grants ending within the window: temporary bindings, and temporary group memberships. Future-dated bindings starting within the window are listed too, so new access does not surprise anyone.
- Elevation: live just-in-time activations with their justification, and the number of activation requests waiting for approval.
- API keys unused for
unusedForMs(including keys never used) and keys ending within the window.
const report = await iam.api.reports.access(credential, {
tenantId,
withinMs: 30 * 86400_000, // look 30 days ahead (the default)
unusedForMs: 30 * 86400_000, // keys unused for 30 days (the default)
});
if (report.omitted.length) console.warn('Sections hidden from this caller:', report.omitted);Prop
Type
The report needs iam:identities:read. The binding and key sections additionally need iam:bindings:read and
iam:credentials:read. Sections the caller may not read are left out and named in omitted rather than failing
the whole call, so a directory administrator without iam:credentials:read still gets the identity section.
An activation or request counts only while it could still grant: its binding is live and eligible, its holder is active, and, for a group binding, the holder is still a member.
The CLI acts as the session or API key in BETTER_IAM_TOKEN, so each run is authorized and audited like any
other call.
The access digest
Most organizations will not open a Reports page every morning. iam.sendAccessDigest() (CLI digest) does the
routing: every active organization whose report has findings gets it emailed to its owners, at most once per 20
hours. Organizations with nothing to report get no email.
const result = await iam.sendAccessDigest({ withinMs: 30 * 86400_000, unusedForMs: 30 * 86400_000 });
// result.sent: [{ tenantId, recipients, expiringIdentities, expiringBindings, startingBindings,
// expiringMemberships, activations, pendingRequests, unusedKeys, expiringKeys }]
// result.skipped: { inactive, recent, quiet, noOwners }- It is a for schedulers: it needs no credential and includes every report section.
tenantIdlimits it to one organization, andminimumIntervalMs(20 hours by default) sets how often one organization may be emailed.- Each active owner with an email address receives an
access-digestemail. Its payload carries the finding counts and the full report as JSON (report), so your email template can summarize it or attach it. - Organizations are skipped when they are not active, were emailed within the interval (
recent), have nothing to report (quiet), or have no owner with an email (noOwners). - Each digest is recorded as
tenant:access-digest(actordeployment-operator) with the recipients and the finding counts. - It needs the configured
sendEmailcallback (DELIVERY_REQUIREDotherwise). The emails are queued in the , so runoutbox(oriam.auth.dispatchOutbox()), which delivers queued messages, afterwards.
Expiry reminders
Owners are not the only ones who need to know. iam.sendExpiryReminders() (CLI remind) tells the people
themselves: everyone whose account, direct role bindings, group memberships, or package assignments end within the
window (seven days by default) gets one expiry-reminder email listing them. They can then ask for an extension
before they are locked out.
const result = await iam.sendExpiryReminders({ withinMs: 7 * 86400_000 });
// result.sent: [{ tenantId, identityId, items }], result.skipped: { inactive, quiet }- Each item is reminded once per end date, so extending the access brings a fresh reminder when the new end comes into the window.
- The
expiry-reminderpayload carriescount,earliest, anditemsas JSON with thekind(account,role,group, orpackage),name, andexpiresAtof each. - A package's own bindings and memberships are reminded as the package. are not reminded, and group bindings are left to the owners' digest.
- Each reminder is recorded as
identity:expiry-reminderon the person (actordeployment-operator) with the item keys, which is how the job knows not to remind anyone twice. withinMsranges from one minute to a year. Like the digest, it needs no credential, needs asendEmailcallback, and relies on the outbox for delivery. Schedule it daily beside the digest.
Scheduling
| Job | Call | CLI | Suggested cadence | Why |
|---|---|---|---|---|
| Report to a channel | reports.access | report | nightly | A shared view for the team that runs access. |
| Owner digest | iam.sendAccessDigest() | digest | daily | Owners hear about findings without looking. |
| Personal reminders | iam.sendExpiryReminders() | remind | daily | People ask for extensions before they are locked out. |
See scheduling for the other governance jobs and background jobs for how to run them.
Better IAM is created by Sean Filimon
Last updated
Automatic assignment
Birthright access with package rules that grant a package to every matching identity and remove it from holders who stop matching.
Configuration as code
Export, plan, and apply a tenant's roles, policies, groups, teams, bindings, packages, agents, and guardrails as one reviewed document.