# Access paths (/docs/features/governance/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:

* step up to MFA;
* accept pending terms of use;
* activate an eligible role;
* request a requestable access package.

Every option is verified before it is offered, so the list never promises access that would still be refused.

<Mermaid
  chart="flowchart LR
  D[&#x22;authorize: denied&#x22;] --> F[&#x22;accessPaths.find&#x22;]
  F -->|mfa| M[&#x22;Step up to MFA&#x22;]
  F -->|accept-agreements| A[&#x22;agreements.accept&#x22;]
  F -->|activate| B[&#x22;bindings.activate&#x22;]
  F -->|request-package| P[&#x22;packages.request&#x22;]
  F -->|no paths| X[&#x22;Ask an administrator&#x22;]
  M --> R[&#x22;Retry the action&#x22;]
  A --> R
  B --> R
  P --> R"
/>

## Find the paths [#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:

```ts title="After a denial"
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();
}
```

**Route:** `POST /api/iam/accessPaths/find`

`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](/docs/authentication/mfa)).                                  |
| `accept-agreements` | `agreements`: `id`, `name`, and `version` of each required agreement owed                       | Accept them with [`agreements.accept`](/docs/features/governance/agreements#accept).           |
| `activate`          | `bindingId`, `role`, `requireApproval`, `requireJustification`, `requireMfa`, `maxActivationMs` | Activate the [eligible binding](/docs/features/privileged-access/elevation#activate).          |
| `request-package`   | `package` (`id`, `name`, `description`), `requireJustification`                                 | Ask for the [package](/docs/features/privileged-access/access-packages#self-service-requests). |

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 [#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:activate` for its role.
* **Packages.** Each requestable package is simulated as assigned. It is offered only when the person holds
  `iam:packages:request` on 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. A refusal by a
[security clearance](/docs/features/security-clearances) label returns no paths at all, since nothing the person can
do alone lifts it.

The call needs only the person's own ordinary session (or API key) of the tenant. Assumed-role sessions, session
tokens, delegated 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-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.

```tsx title="components/delete-button.tsx"
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](/docs/integrations/react) and [Vue](/docs/integrations/vue).

> **Advisory, like authorize.** 
  Access paths are advisory: advice for the UI. The server still calls `iam.require`
  (or `authorize`) immediately before performing the protected operation.

## Options [#options]

Access paths have no deployment option and store nothing: every candidate is simulated in a transaction that is rolled
back. The call takes:

| Input      | Type                           | Description                                                       |
| ---------- | ------------------------------ | ----------------------------------------------------------------- |
| `tenantId` | `string`                       | Required. The tenant of the caller's session.                     |
| `action`   | `string`                       | Required. The action that was denied, such as `documents:delete`. |
| `resource` | `{ type: string; id: string }` | Required. The resource the action was denied on.                  |

## Routes [#routes]

| Method             | Route                            |
| ------------------ | -------------------------------- |
| `accessPaths.find` | `POST /api/iam/accessPaths/find` |

The browser client calls it as `client.accessPaths.find`, and the React and Vue `useAccessPaths` hooks call it for
you.

## Errors [#errors]

| Code                                                                          | Status | When                                                                                        |
| ----------------------------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------- |
| [`ACCESS_DENIED`](/docs/reference/errors#access_denied)                       | 403    | An assumed-role session, session token or delegated session, or a session of another tenant |
| [`IMPERSONATION_RESTRICTED`](/docs/reference/errors#impersonation_restricted) | 403    | A "view as" session                                                                         |
| [`INVALID_ACTION`](/docs/reference/errors#invalid_action)                     | 400    | The catalog does not know the action                                                        |

A denial is not an error: `find` answers `{ allowed: false, reason: 'ACCESS_DENIED', paths }`.

  - [accessPaths.find](/docs/reference/api/access-paths#find): Signature and HTTP route.

  - [Just-in-time elevation](/docs/features/privileged-access/elevation): The eligible bindings behind activate paths.
