Permissions & Role Mapping¶
Identity Atlas authorizes every API request against a fixed catalog of permissions. Customers map their Entra ID app roles to those permissions, so "who can do what" is configurable without code changes.
This page is the reference for the permission model. The catalog below is kept in sync with the code by a test (app/api/src/auth/docsCoverage.test.js) — if a permission is added to the catalog without being documented here, CI fails.
How authorization works¶
- Authentication — when
AUTH_ENABLED=true, every/api/*request must carry a valid Entra ID bearer token (or a read/crawler API key). WhenAUTH_ENABLED=false(the default for local/laptop use) the API runs in open mode: all gates are no-ops and a banner warns that anyone with the URL has full access. Open mode is a supported deployment for evaluation and single-user local runs — do not use it on a network without a trusted boundary. - Role → permission resolution — the token's
rolesclaim is resolved to a set of permissions via the configured mapping (Admin → Authentication → Roles & Permissions), falling back to the built-in seed mapping. - Per-route enforcement — each protected route declares the permission(s) it needs via
requirePermission(...). The gate is attached to the individual route, not the/apimount, so one router's permission never affects another's endpoints. - Wildcard — a role mapped to
*is granted every permission (including ones added in future releases). The seedAdminrole uses*.
Read & crawler API keys
- Read API keys (
fgr_…) are accepted only onGETrequests to non-admin endpoints and resolve todata.read. They're used by the generated Excel/Power Query workbook. - Crawler API keys (
fgc_…) authenticate the worker on/api/crawlers/*and/api/ingest/*only, with their own per-crawler scope (systemIds) and permissions (ingest,refreshViews,admin). They are separate from the user-facing permission catalog below.
Permission catalog¶
Permissions are grouped into Read, Export, Write, and Admin.
Read¶
| Permission | Label | Notes |
|---|---|---|
data.read |
Read all data | Effectively "can sign in at all." Enforced as authentication-required — any signed-in user can read; there is no per-route requirePermission('data.read') gate. fgr_ read tokens are granted it implicitly. |
Export¶
| Permission | Label | Gated endpoint (representative) |
|---|---|---|
data.export.ui |
Export to Excel/CSV | GET /api/admin/export/curated, POST /api/admin/data-export/workbook |
data.export.apikey |
Generate read-only API keys | POST /api/admin/read-tokens (mint your own fgr_ token) |
Write¶
| Permission | Label | Gated endpoint (representative) |
|---|---|---|
data.write.tags |
Manage tags | POST/PATCH/DELETE /api/tags… |
data.write.categories |
Manage categories | POST/PATCH/DELETE /api/categories… |
data.write.risk |
Risk score overrides | PUT/DELETE /api/risk-scores/:type/:id/override |
data.write.identity |
Identity link decisions | PUT/DELETE /api/identities/:id/members/:userId/override (confirm / reject / clear an account-linking decision) |
data.write.certifications |
Certification decisions | Reserved — no interactive endpoint yet. Certification decisions are currently ingested via the crawler (POST /api/ingest/governance/certifications). When an approve/revoke endpoint is added it will be gated by this permission. |
Admin¶
| Permission | Label | Gated endpoint (representative) |
|---|---|---|
admin.crawlers |
Crawler configuration | GET/POST/PATCH/DELETE /api/admin/crawlers…, crawler jobs, trigger risk-scoring runs, account-linking config + runs (PUT /api/account-linking/config, POST /api/account-linking/runs) |
admin.systems |
Systems configuration | PUT /api/systems/:id, owners, clean-database, history-retention |
admin.llm |
LLM configuration | /api/admin/llm/*, risk profiles/classifiers |
admin.context-plugins |
Context plugins | /api/context-plugins… (run/configure clustering, manager-hierarchy, etc.) |
admin.csv-import |
CSV import | /api/admin/crawler-configs/:id/csv-files, custom-connector ingest |
admin.read-tokens |
Manage read API keys | GET/DELETE /api/admin/read-tokens (list/revoke tokens minted by others) |
admin.feature-flags |
Feature flags | POST /api/admin/features/toggle |
admin.auth |
Authentication & roles | GET/PUT/DELETE /api/admin/roles — edits this very mapping. A self-lockout guard prevents a save that would strip your own admin.auth. |
Seed (default) role mapping¶
A fresh install ships with this mapping (customisable in the Admin UI):
| Role | Permissions |
|---|---|
Admin |
* (all permissions) |
RoleMiner |
data.read, data.export.ui, data.export.apikey |
Servicedesk |
data.read |
No-role users fail closed¶
A signed-in user whose roles resolve to no permissions is denied every permission gate — they can authenticate but cannot perform any admin or write action. There is deliberately no "no roles → full admin" fallback (that was a privilege-escalation flaw). Note that read endpoints are not permission-gated, so a roleless user can still read data; set AUTH_REQUIRED_ROLES to keep roleless users from signing in at all.
Getting the first admin in (bootstrap)¶
Because there's no fallback, you must grant access explicitly:
- In Entra ID, assign your user to an app role that the mapping maps to permissions — the seed maps the
Adminrole to*(full access). - Then enable auth (
AUTH_ENABLED=true) and sign in.
Locked yourself out? If you enabled auth before assigning yourself a role, no one can reach the admin UI. Recover from the host shell with the auth CLI — disable auth, assign the role in Entra, then re-enable:
# 1. Disable auth (recovery path), then restart:
docker compose exec web node src/cli/auth-config.js disable
docker compose restart web
# 2. Assign your user the Admin app role in Entra ID.
# 3. Re-enable auth and restart:
docker compose exec web node src/cli/auth-config.js enable --tenant <tenant-guid> --client <client-guid>
docker compose restart web
UI gating¶
The SPA calls GET /api/auth-me after sign-in to learn its own permissions and hides controls the user can't use (the Admin tab, its sub-tabs, and export buttons). The server remains the source of truth — hidden controls are also enforced server-side, so hiding is a UX nicety, not a security boundary.
Deployment notes¶
- Open mode (
AUTH_ENABLED=false) — supported for local/evaluation use; all permission gates are bypassed. - Azure App Service —
AUTH_ENABLED=trueis set from first boot; the Authentication admin sub-tab is hidden (managed platform). - Tenant/client IDs are configured via
AUTH_TENANT_ID/AUTH_CLIENT_ID; the role→permission mapping is edited live in Admin → Authentication. - Token audience — the API accepts only access tokens whose audience is
api://<client-id>(its exposed API scope). ID tokens are rejected. This is why the setup walkthrough has you "Expose an API" with the defaultapi://<client-id>Application ID URI.
Testing¶
Enforcement is verified by:
app/api/src/auth/permissionMatrix.test.js— for every gated permission, asserts the representative endpoint is reachable with the permission and returns403without it; plus a completeness guard that fails if any catalog permission is unclassified.app/api/src/middleware/requirePermission.test.js— unit tests for the gate decision logic.app/ui/e2e/permission-gating.spec.js— confirms the UI shows/hides the Admin tab and sub-tabs per permission.app/api/src/auth/docsCoverage.test.js— fails if a catalog permission is missing from this page.