Access paths
Tell a denied person what they could do themselves, such as step up to MFA, accept terms, activate a role, or request a package.
"Access denied" is a dead end. The person files a ticket, an administrator investigates, and often the answer was something the person could have done alone: sign in with their second factor, accept the new terms, or activate a role they are already eligible for.
Access paths answer the question "how can I get access?" at the moment of the denial, with the options the person can take on their own:
- to MFA;
- accept pending ;
- activate an ;
- request a requestable .
Every option is verified before it is offered, so the list never promises access that would still be refused.
Find the paths
Call accessPaths.find right after an authorization check fails, with the same action and resource, and turn
each path into a button or a hint:
const check = await iam.authorize({ token, tenantId, action: 'documents:delete', resource });
if (!check.allowed) {
const { paths } = await iam.api.accessPaths.find(
{ token },
{ tenantId, action: 'documents:delete', resource },
);
for (const path of paths) {
if (path.kind === 'mfa') showStepUp();
if (path.kind === 'accept-agreements') showTerms(path.agreements); // then agreements.accept
if (path.kind === 'activate')
offerActivation(path.bindingId, path.role, path.requireJustification); // bindings.activate
if (path.kind === 'request-package') offerRequest(path.package); // packages.request
}
if (!paths.length) showAskAnAdministrator();
}accessPaths.find({ tenantId, action, resource }) returns { allowed, reason, paths }. When the request is
already allowed, paths is empty. When it is denied, paths lists what the person could do alone, and an empty
list means only an administrator can help.
| Kind | Fields | What the person does next |
|---|---|---|
mfa | none | Step up to a second factor (MFA). |
accept-agreements | agreements: id, name, and version of each required agreement owed | Accept them with agreements.accept. |
activate | bindingId, role, requireApproval, requireJustification, requireMfa, maxActivationMs | Activate the eligible binding. |
request-package | package (id, name, description), requireJustification | Ask for the package. |
The approval requirements are reported so the UI can say "your request goes to an approver" before the person
asks. requireMfa on an activate path gates the activation itself, not the grants it brings, so a person
holding a password-only session may need to step up before activating.
How paths are verified
A suggestion that does not work is worse than none. So each candidate is applied inside a transaction that is always rolled back, and the ordinary decision runs again; only candidates that turn the denial into an allow are offered. Nothing is saved.
- MFA. The request is re-evaluated as if the session were MFA-verified.
- Terms. The required agreements the person owes are recorded as accepted, then removed again.
- Eligible roles. Each eligible binding the person holds, directly or through a group, is simulated as
activated. It is offered only when the person also holds
iam:bindings:activatefor its role. - Packages. Each requestable package is simulated as assigned. It is offered only when the person holds
iam:packages:requeston it.
At most 50 eligible bindings and 50 requestable packages are considered. Like authorize, a denial's reason is
always ACCESS_DENIED, so the call does not reveal which rule refused.
The call needs only the person's own ordinary session of the tenant. Assumed-role sessions and sessions of another
tenant are refused with ACCESS_DENIED, and impersonation sessions with IMPERSONATION_RESTRICTED. An action the
catalog does not know fails with INVALID_ACTION.
In React and Vue
In a React or Vue application, the useAccessPaths hook runs the same call for a component, so a button can
explain how to get access instead of disappearing. It returns allowed, reason, paths, status, error, and
refresh, and takes enabled: false to skip loading.
import { useAccessPaths } from 'better-iam/react';
export function DeleteButton({ tenantId, documentId }: { tenantId: string; documentId: string }) {
const { allowed, paths, status } = useAccessPaths({
tenantId,
action: 'documents:delete',
resource: { type: 'document', id: documentId },
});
if (status === 'loading') return null;
if (allowed) return <button>Delete</button>;
if (!paths.length) return <p>Ask an administrator for access.</p>;
return (
<ul>
{paths.map((path, index) => (
<li key={index}>
{path.kind === 'mfa' && 'Sign in with your second factor'}
{path.kind === 'accept-agreements' && 'Accept the terms of use'}
{path.kind === 'activate' && `Activate ${path.role.name}`}
{path.kind === 'request-package' && `Request ${path.package.name}`}
</li>
))}
</ul>
);
}The Vue composable of the same name, from better-iam/vue, accepts its input as a ref or getter and returns
computed refs. See React and Vue.
Better IAM is created by Sean Filimon
Last updated