Usage and role mining
Record which actions people actually use, right-size roles, and mine the directory for redundant grants, bundles, and peer outliers.
Least privilege means everyone holds only the access they need. It is easy to agree with and hard to practice, because nobody knows which grants are still needed, and access granted one person at a time turns into a tangle nobody can review.
Two features help. Access usage records which actions each person actually uses, so you can find nobody needs and actions no holder touches. Role mining reads who holds what today and proposes simpler ways to grant the same access.
Reading either needs iam:analysis:read; applying a role-mining suggestion needs iam:analysis:update. The
console's Role mining page shows both, with Apply, Remove, and Create package buttons.
Access usage
To know whether a grant is needed, you need to see it being used. Access usage counts every allowed check per person and action. It is off by default because it writes to storage; turn it on once and let it run for a review period, such as the 90 days the reports look back by default, before drawing conclusions.
const iam = betterIam({
// ...
accessUsage: true, // or { flushIntervalMs, maxBuffered }
});
// On shutdown, write what is still buffered.
await iam.flushAccessUsage();Prop
Type
What is counted
Every allowed authorize or authorizeMany check and every allowed provisioning operation counts. Root
overrides and calls made during do not, because they are not the
person's own use of their grants. Uses are counted in memory per person and action and written in batches, so the
request path never waits on storage.
The accessUsage collection holds one record per identity and action with firstUsedAt, lastUsedAt, and
count. accessUsageTracking remembers when each tenant's recording began, so reports can tell whether the
evidence covers a whole window.
iam.flushAccessUsage() writes whatever is still buffered and returns { written }. Call it in your shutdown
hook, so a deploy does not lose the last minute of usage.
Read usage
roleMining.usage({ tenantId, identityId?, limit?, offset? }) lists the raw records, most recently used first,
100 per page by default and at most 1000. It also returns tracking (whether the option is on), trackingSince
(when recording began), and the total number of records. Use it to answer "what does this person actually do?"
on a member's detail page.
const { records } = await iam.api.roleMining.usage(credential, { tenantId, identityId: alice.id });
// records: [{ identityId, action, firstUsedAt, lastUsedAt, count }]Right-size roles
Raw usage is too detailed to act on. roleMining.rightSize({ tenantId, unusedDays? }) turns it into
recommendations. It compares every live with its holders' use in the last
unusedDays (90 by default). It then reports who holds a role they do not use, and which of a role's actions
nobody uses at all. Run it after a full window:
const result = await iam.api.roleMining.rightSize(credential, { tenantId, unusedDays: 90 });
if (!result.complete) console.warn('Usage does not cover the whole window yet');
for (const entry of result.entries)
console.log(entry.identity.name, entry.role.name, entry.status, entry.unusedActions);
for (const role of result.roles) console.log(role.role.name, 'never used:', role.neverUsed);- A role's actions are the known actions its allow statements (own, attached, and inherited) match.
- Each
entriesitem is a person whose use of a role isunused(none of its actions) orpartial(some never used), with the used and unused actions and whether the role reaches them directly or through a group (via). roleslists, per role, the actions no holder used: the candidates for a narrower role.completeturns true once usage has been recorded for the whole window. Before that, "unused" only means "not since tracking began".
Both read methods write buffered usage first, so results include the latest checks. The console's Role mining page shows a Least privilege card with a Remove button for unused direct bindings.
Usage also powers review recommendations for , the periodic reviews where reviewers keep or revoke each binding.
Role mining
Over time the same access gets granted in several ways: a role bound to a person directly and again through their group, the same role bound to every member of a team one by one, two roles with identical permissions. Each is harmless alone, but together they make access hard to review and easy to leave behind when people move.
Role mining finds these patterns. roleMining.suggest({ tenantId, minIdentities?, minRoles?, kinds?, limit? })
reads who holds what and returns summary counts per kind and a list of suggestions, most actionable first.
Each suggestion has:
- a deterministic
id(the same condition always gets the same ID); - the roles and people involved;
savings, the number of grants the change would save;applicable, whetherroleMining.applycan carry it out for you.
minIdentities (default 3) is how many people must share a pattern, and minRoles (default 2) the smallest role
combination reported as a bundle. kinds limits the result to some kinds and limit caps its length.
| Kind | What it finds | Carried out by |
|---|---|---|
redundant-binding | Direct bindings of a role the person also receives through a permanent group membership, from a standing binding under the same authority that is at least as broad (no window, no later start, no earlier end). Removing them changes nothing today. | roleMining.apply |
group-binding | A role every permanent member of a group (at least minIdentities, default 3) holds through a plain direct binding (standing, permanent, not from a package). Bind it to the group once so joiners get it and leavers lose it. | roleMining.apply |
duplicate-roles | Roles whose inline, attached, and inherited statements are identical. | Role edits |
bundle | Role combinations of at least minRoles (default 2) roles held together by at least minIdentities people, skipped when a package or an inheriting role already has exactly those roles. | packages.create |
Bundles are found by looking for the largest sets of standing roles that the same group of people hold together (closed itemsets, in data-mining terms). Grant them as one (access packages), optionally assigned automatically by attribute, or as an inheriting role.
Apply a suggestion
Two kinds of suggestion can be carried out for you, so a cleanup is one click (or one call) instead of dozens of binding edits:
const { suggestions } = await iam.api.roleMining.suggest(credential, { tenantId });
const simplest = suggestions.find((item) => item.kind === 'group-binding' && item.applicable);
if (simplest)
await iam.api.roleMining.apply(credential, { tenantId, suggestionId: simplest.id });
// { applied, createdBindingId, removedBindingIds }roleMining.apply({ tenantId, suggestionId }) (iam:analysis:update) carries out a group-binding or
redundant-binding suggestion in one transaction. It recomputes the suggestion first, so pass the same
minIdentities and minRoles you gave suggest. It fails with NOT_FOUND once the suggestion no longer holds,
and with INVALID_INPUT for a suggestion that is not applicable. It runs under the authority the original
bindings used and with the caller's own binding rights:
- The group binding is created under the authority the direct bindings share, so the caller needs that authority
(or root),
iam:bindings:createon the role, andiam:bindings:deleteon every removed binding. - Suggestions whose direct bindings come from different authorities are listed with
applicable: false. - Bundles and duplicate roles are left to
packages.createand role edits. - Enforced (change safety)
guard
roleMining.applylike any other access change.
Peer outliers
When someone moves from sales to finance, they often keep their sales access; when someone joins a team, they often lack something everyone else has. Comparing a person with their colleagues catches both.
roleMining.outliers({ tenantId, peerBy?, threshold?, commonShare?, minPeers? }) compares each active person
with their peers: people with the same manager (peerBy: 'manager', the default) or the same value of a declared
identity attribute ('attribute:department').
const { outliers } = await iam.api.roleMining.outliers(credential, {
tenantId,
peerBy: 'attribute:department',
});
// outliers: [{ identity, peerValue, peers, unusualRoles, missingRoles }]- A role is unusual when fewer than
threshold(0.25) of the peers hold it: access that outlived a move. - A role is missing when at least
commonShare(0.8) of the peers hold it and the person does not: what a joiner still needs. - Peer groups with fewer than
minPeers(3) others are skipped, and just-in-time bindings count as held.
From the command line
better-iam mine-roles --tenant ID [--peer-by KEY] prints both reports, the suggestions and the peer outliers,
as JSON. It acts as the session or API key in BETTER_IAM_TOKEN, which needs iam:analysis:read. Run it weekly
and keep the output as a snapshot, so you can see whether the access model is getting simpler over time.
BETTER_IAM_TOKEN=... better-iam mine-roles --config better-iam.config.mjs --tenant TENANT_ID --peer-by attribute:departmentBetter IAM is created by Sean Filimon
Last updated
Governance
The loop that keeps access correct over time, from measuring usage and mining roles to reviews, guardrails, terms of use, and self-service.
Certifications
Access certification campaigns where reviewers or managers keep or revoke each binding, with reminders, usage-based recommendations, and auto-close.