-
Notifications
You must be signed in to change notification settings - Fork 0
Access and Roles
Two independent things decide whether a person can do something in Forge: what this install has switched on, and who that person is. The first is a capability, the second is a role, and they are enforced by different machinery that composes with an AND. Understanding both — and their real limits — is the difference between "I assigned the role and it still doesn't work" and a working access model.
Canonical reference: docs/functional-reference/roles-permissions.md — stale in two places, see the last section. The authoritative sources are forge.api/Data/SeedData.cs for the role set and the [Authorize(Roles = …)] attributes in forge.api for what each role reaches. For the other axis, see Capability Gating.
A capability answers does this feature exist on this box. A role answers may this person use it. They are evaluated separately and independently, so a request has to satisfy both.
| Capability enabled | Capability disabled | |
|---|---|---|
| User holds a checked role | Works | Refused as capability-disabled — a 403 carrying an X-Capability-Disabled header, see API Access
|
| User holds no checked role | Refused as an authorization failure | Refused |
The consequence for an administrator is that turning a capability on does not grant anyone access to it, and assigning a role does not make a switched-off feature appear. Both are separate admin actions, on separate screens.
The same pair drives what people see. Every entry in the sidebar tree carries an optional capability code and an optional list of allowed roles; an entry is rendered only if the capability is enabled and the signed-in user holds one of the roles, and a group whose children all filter out disappears entirely. Two people on the same install, and the same person on two installs, get different navigation. Routes are guarded the same way — roleGuard() and capabilityGuard() are separate guards that both redirect to the dashboard — so hand-typing a URL does not get round either.
Roles are ASP.NET Identity roles, created at first boot from a literal array at the top of SeedData.SeedAsync. Read that array for the current set; it is longer than the six the canonical doc describes.
There is no create-role endpoint. The admin API exposes GET api/v1/admin/roles and nothing that writes to the role list; the role-bundle handlers explicitly refuse any name that is not already a seeded role. Adding a role is a code change, not configuration — which is deliberate, because a role only means something if [Authorize] attributes name it.
Everything else about role assignment is conventional:
- Roles are additive and multi-assignment. A user holds several roles directly and their access is the union. There is no hierarchy and no inheritance; Manager reaching more than Engineer is a consequence of the attributes, not of a ladder.
-
Roles are baked into the token. Role claims are written when a token is issued, so a role change takes effect on the user's next token — a fresh sign-in or a token refresh — rather than mid-session. Every change writes a
RoleAssignedaudit row recording the before and after set. - To stop access immediately, deactivate the account. The deactivate action revokes every one of that user's sessions and unassigns their open jobs, which is what you want when someone leaves; it is a distinct action from editing the account.
- Admins never see or set a password or PIN. Account creation issues a setup token and the employee completes their own credentials — see Architecture § Authentication. Kiosk identities are provisioned in one shot with a role, a badge identifier and a PIN; see Shop Floor Kiosk.
The roles do not carry equal weight. Some are named on hundreds of endpoints; several are named on none, and exist only as UI labels and grouping keys.
| Role | Where it is actually checked |
|---|---|
| Admin | Almost everything, and the entire administration surface |
| Manager | Nearly as broad — operations leadership, approvals, scheduling |
| OfficeManager | Back office: vendors, purchase orders, invoices, payments, shipments |
| Engineer | Technical execution: parts, inventory, lots, quality, the board |
| PM | Planning and sales: backlog, planning, leads, quotes, sales orders |
| Controller | The accounting surfaces — GL, aging, bank statements, settlements, conversion |
| ProductionWorker | The shop-floor worker view |
| Procurement | The purchase-order surface, and effectively nowhere else |
| IT Admin · Production Manager · Production Planner · ComplianceOfficer | No API authorization sites. They are navigation, landing and rollup labels |
The roles in the last row are not broken and not dangerous — the API denies by default, so a role that is named nowhere grants nothing extra. But it does mean assigning one of them is not how you grant someone access. They do real work elsewhere: they filter the sidebar, they select a landing screen, they are selectable in the MFA policy, and ComplianceOfficer additionally receives read visibility on the regulatory compliance calendar through a data-level grant rather than an attribute check.
The practical rule: before promising someone a permission, look at the [Authorize(Roles = …)] attribute on the controller or action that owns it, and give them a role that appears there. Where a role's only effect is on the UI, say so out loud when you hand it out.
Landing behaviour follows from the same list. A user whose access is dominated by a single role lands on the screen for that role — production worker on the board, engineer on Parts, controller on Accounting — while anyone holding several roles, including Admin and Manager, lands on the dashboard, and an explicit per-user landing preference beats both. See App Surfaces.
What is configurable is a role bundle (a role template in the API): a named rollup of existing roles, managed under api/v1/admin/role-templates and on an admin panel. A handful ship seeded — a back-office rollup, a production rollup and an owner rollup, for shops where one person wears several hats. Seeded defaults are protected from edit and delete; your own are soft-deactivated rather than removed, so audit history survives. A bundle may only name roles that already exist.
Be clear about what a bundle is for today, because its history is misleading:
- A bundle is not how you give a person several roles. The user-side coupling was retired; users now hold each role directly, and a one-time migration expands any historic bundle assignment into direct grants at startup. Someone wearing three hats gets three roles.
- Its live job is scoping a headless credential. A system API key can be pinned to a bundle, and the key then emits the intersection of the bound user's roles with the bundle's — it can only narrow, never expand, so a key cannot grant a permission its user does not already hold. See API Access for the two key schemes.
- Presets can seed bundles. Applying a capability preset may install additional bundles as part of its reshaping pass, add-only — see Capability Gating § Presets.
Forge's authentication story does not stop at password and SSO. The full second factor is present and is worth switching on before an install faces the internet — see Hardening a Production Install.
- TOTP. Standard authenticator-app enrolment, verified before the device counts, several devices per user with one default.
- Passkeys (WebAuthn). Registered from a signed-in session on the account security screen. Note that passkeys here are a second factor, not a passwordless login: the password check happens first, and the passkey ceremony then runs against a single-purpose pending token.
- Recovery codes. A fixed batch, stored only as hashes, each usable once; regenerating replaces the whole set and the remaining count is shown on the account screen. Issue them at enrolment and store them somewhere that is not the laptop.
-
Trusted devices. After a successful challenge a user can have this browser remembered, which skips the challenge on later sign-ins — the password is still required every time. The token is single-purpose, bound to that user, and signed with a key derived from the main JWT signing key, so it is inert against every other token pipeline. Rotating
JWT_KEYinvalidates these along with everything else.
For a client, the MFA branch is the thing to code for: a login against an MFA-enabled account returns an empty token with an MFA-required flag and a pending token, and the session is only issued after a challenge, a passkey assertion or a recovery code. API Access has the request shapes.
An admin panel sets the policy by picking roles. Every user holding one of those roles is flagged as enforced, and enforcement means two concrete things: they cannot disable MFA, and they cannot remove their last verified device. The same panel lists every user with whether they have actually enrolled.
Be realistic about what that buys you. The policy is a floor plus a report, not a login block — a user marked enforced who has not yet enrolled can still sign in, so treat the compliance table as a follow-up list rather than as evidence that the policy is satisfied.
The MFA policy screen is itself capability-gated, while per-user enrolment and the challenge endpoints are bootstrap-exempt. That is the two axes again, and it has a practical edge: switching the MFA capability off removes the admin policy surface, it does not disarm an already-enrolled user's second factor.
Not every principal is a person. A system API key authenticates as a real user, so audit rows and role checks attribute to that identity and deactivating the user kills the key; a BI key is unbound and scoped to a read-only reporting surface. One seeded role is headless by design — a single-purpose sync role bound to a service account and scoped to one narrow intake path — and should never be assigned to a human. Both key schemes and the SSO token exchange are covered on API Access.
One thing SSO does not do is create accounts: a first federated sign-in links to an existing active local user by email, and roles come from the local account, never from the identity provider.
docs/functional-reference/roles-permissions.md is still the most detailed per-controller matrix anywhere, and it is worth reading — but two of its claims no longer hold:
- It opens by saying Forge defines six roles. The seed creates more than that; count the array in
SeedData.cs. - It describes MFA as TOTP only. Passkeys, recovery codes and trusted devices all post-date it.
Where the doc and the code disagree, the code wins — the standing rule on Documentation Map, and this is a page where readers hit it.
Forge · Apache 2.0 · built by Armory Works — this wiki maps the docs; the authoritative detail lives in docs/.
Evaluating
Running it
- Installation
- First Week
- Configuration and Integrations
- Hardening a Production Install
- Backup and Restore
- Upgrades and Rollback
- Accounting Modes
Using it
- App Surfaces
- Shop Floor Kiosk
- Mobile and Offline
- Access and Roles
- Customizing an Install
- Feature Reference
- UI Flows
Building on it
- API Access
- Architecture
- Capability Gating
- Workflow, Gates and Approvals
- Data Ownership and Export
- Glossary
Contributing
Repo wikis