Roles and Permissions - edmozley/freeitsm GitHub Wiki
How FreeITSM lets a non-administrator manage part of a module's settings โ without handing them the whole System module, and without handing them all of that module's settings either. This is a second, finer layer of access control sitting on top of module access, so it's called "Layer 2".
Companion pages: Module Access Control (Layer 1), Admin Access Control (the is_admin System gate whose guard pattern this reuses), and Security.
Managed under System โ Roles (administrators only). To wire a module or add a permission, see the Roles โ Developer Guide.
FreeITSM answers three questions about a person and a module, not one:
| Question | Answered by | Page |
|---|---|---|
| Can they use it? | Module access โ individual + team grants | Module Access Control |
| Can they administer this part of it? | A Role granting that settings tab's capability | this page |
| Can they run the System module? | is_admin |
Admin Access Control |
They stack. Module access gets someone into Asset Management to do their job; a role is what additionally lets them, say, maintain the locations list. is_admin is a superset of everything.
Layer 2 mostly governs a module's settings surface. There is no tick-box for "reply to a ticket" or "take a course" - the day-to-day job stays governed by module access. Since 3.8.0 there is one deliberate exception, everyday work (below), for the few everyday actions an organisation needs to separate from the rest of a module.
Some everyday actions decide who approves what, so an approver or a reviewer has to be able to be left out of them. These are permissions too, listed in System โ Roles under each module's Everyday work heading:
| Permission | Module | Applies when |
|---|---|---|
| Choose approvers - set a change's approver, whether CAB review is required, and its roster | Change Management | Change Management โ Settings โ Approvals โ Who can choose a change's approvers = Only people with "Choose approvers" |
| Design forms - create, edit, version, publish and delete forms, including who approves them and what happens next | Forms | Forms โ Settings โ Access โ Only people with "Design forms" |
| Approve standard-change templates - approve a template version, which then pre-authorises the changes raised from it | Change Management | Always - nobody may approve a template without it |
| Run the evidence report - open a repeating task's evidence report and print or export it (#181) | Tasks | Tasks โ Settings โ Evidence โ Who can run the evidence report = Only roles with "Run the evidence report" |
| View background job health - Reporting โ System logs โ Background jobs, and the Watchtower card (#182) | Reporting | Always. Also needs access to every company |
| Change what background jobs are expected to do - intervals, and "not used here" (#182) | Reporting | Always. Sensitive: it can silence a job's alarm |
An Auditor role (#181): Read only access to Tasks (Module access) plus Run the evidence report - every task, its history and the evidence, and nothing that changes them. See Repeating task evidence.
Nothing is taken away on upgrade. The ones with a setting apply only once their module's setting is switched on: until then module access is enough, exactly as before. Build the role, give it to the people who should keep the ability, and then switch the setting - Forms โ Settings โ Access lists who would keep designing before you do. Administrators hold every everyday permission, as they hold every capability.
๐ Holding a permission is not the same as being independent. Approving your own change is a separate rule (Change Management โ Settings โ Approvals), and administrators are not exempt from it - otherwise "make me an administrator" would be the way round it.
This is the design decision everything else follows from.
A single "manage this module's settings" permission sounds tidy, and it is useless. Look at what sits side by side on one tab bar:
| Asset Management tab | What it actually is |
|---|---|
| Asset types | A lookup list |
| Asset statuses | A lookup list |
| vCenter | Hostname, username and password for your hypervisor |
| Intune | Azure tenant id, client id and client secret |
One capability covering both means you can never say "Priya maintains the asset types and locations, but doesn't get the hypervisor credentials." Tickets is worse: its Mailboxes tab holds OAuth credentials and inbound mail routing โ whoever holds it can redirect or read the company's email โ and it sits two tabs from Ticket types, which is a list of words.
So each settings tab gets its own capability. Around 70 across the product when the roll-out completes.
Every module also declares a <module>.manage umbrella, which satisfies every capability in that module. So the ordinary "Asset Administrator" role is one tick, not seven.
The umbrella also makes roles stable over time: add a new tab next year and umbrella-holders pick it up automatically, while a role that ticked individual tabs does not. That's the safe direction โ a new administrative surface is never silently granted to someone who was given a narrow permission.
The Left panel tab (on seven modules' settings pages) is a per-analyst display preference โ where your sidebar sits. It isn't administration, it declares no capability, and everyone with the module sees it. There's nothing there to grant.
Any capability that reaches credentials, email, or money is marked sensitive and badged in the Roles picker. assets.vcenter and assets.intune are sensitive; assets.types is not. The badge is a prompt to think, not a second gate.
A capability is an atomic permission, keyed <module>.<tab> (e.g. assets.vcenter). A role is a named bundle of capabilities, assigned to analysts and/or whole teams. An analyst's effective capabilities are the union of the roles assigned to them directly and the roles assigned to any team they belong to โ the same individual-plus-team, combined-at-one-choke-point shape as module and company access.
| Table | Purpose |
|---|---|
rbac_roles |
A named role (name, description, is_active) |
rbac_role_capabilities |
The capability keys a role grants |
rbac_analyst_roles |
Roles assigned to an analyst |
rbac_team_roles |
Roles assigned to a team (every member inherits) |
Valid capabilities live in code, not the database. Each module declares its settings tabs โ and therefore its capabilities โ in a single file, <module>/settings/manifest.php. Everything else is derived from that one declaration: the tab bar you see, the tick-boxes on System โ Roles, and which stored settings each tab may write.
So the database can never hold a capability the app doesn't define, the Roles picker cannot drift from the code (it is generated from it), and retiring a capability retires it everywhere with no data migration.
Earlier this was four separate lists that had to agree, with an automatic check to catch them drifting apart. Needing that check was the problem, so they were collapsed into one. Detail in the Developer Guide.
With no role, a non-administrator has no access to any module's settings. This is the opposite of Layer 1's "absence = everything" default โ and it's the right default here, because a settings screen is administration, not someone's job.
An analyst with is_admin = 1 implicitly holds every capability. This is what makes deny-by-default safe to switch on:
- Every administrator โ including you โ keeps everything on upgrade. The tightening can never lock you out.
- The new control only ever adds reach for the non-admins you choose.
Note
Because admins bypass Layer 2, you never assign roles to your administrators. Promoting someone to admin on System โ Analysts gives them everything regardless of roles.
The whole point of this layer is that it is not a UI convenience. Three mechanisms, all server-side, all failing closed.
1. The page: not rendered, not hidden
A settings tab you lack the capability for is never emitted into the HTML. It is not display:none, and it is not disabled โ the markup does not exist. There is nothing to un-hide in devtools, and nothing to re-enable.
The tab bar is rendered from the module's manifest, filtered by what you hold. Someone granted only assets.locations receives a page containing exactly two tabs. Typing the URL of the vCenter tab does nothing, because there is no vCenter tab on their page.
Hiding a control is worthless if the endpoint behind it still answers. Every settings write endpoint carries its own guard:
| Guard | Use on | A lacking non-admin gets |
|---|---|---|
requireCapability(Cap::X) |
a settings page |
302 redirect to the launcher |
requireCapabilityJson(Cap::X) |
a settings write API |
403 { success:false, โฆ } and exit
|
Both are authoritative โ they re-check the database on every request, so an analyst whose role was revoked a second ago is stopped even on a stale session โ and both fail closed: if anything throws, access is denied.
Layer 1's original mistake was hiding launcher cards without enforcing, so a "restricted" analyst could type a URL and walk in. Layer 2 does not repeat it.
Some settings tabs have no endpoint of their own. Asset Management's Warranty, vCenter and Intune tabs, Tickets' General tab, and three System areas all save through one generic key/value endpoint (api/settings/save_system_settings.php).
A guard at the top of that file would be meaningless โ it's the same file for every caller, with five different audiences. So authorisation attaches to the setting key, not the file: includes/settings_keys.php records who owns each key, every key in a request is authorised before any of them are written (so a partly-permitted save is refused whole rather than half-applied), and a key that no module claims is refused outright.
Warning
This was a real hole, fixed in #829. That endpoint previously checked only that you were a logged-in analyst, then wrote whatever keys you posted. Any analyst โ including one whose module access was a single unrelated module โ could overwrite the vCenter and Intune credentials, every AI provider API key, the SSO configuration, or the security policy: setting max_failed_logins to 9999 switches off brute-force lockout entirely. Reads were never exposed (secrets are masked on the way out), so it was tampering rather than disclosure. If you are on an older release, upgrade.
Reads. get_asset_types.php is called by the settings page and by the everyday asset list, which needs type names to render its filters. Gate it and the module breaks for everyone who isn't a manager. Capabilities guard writes; operational reads stay on plain module access.
Warning
With one exception: a read that returns credentials is not an operational read.
get_mailboxes.php masked the OAuth client secret into a new field for display โ and then shipped the original plaintext alongside it, to every logged-in analyst (#849). It was a "read". The rule is: ask what the response contains, not just what the endpoint does.
Type-safe permissions catch a misspelled check. Nothing catches a check somebody simply forgot to write โ no language feature saves you from a line that isn't there.
So there is an audit: System โ Debug tools โ D005 (Endpoint permission coverage). It reads every endpoint in the product and reports what actually guards each one, ranked by how much damage the gap allows. Run it before a release.
It is not theoretical. Every one of these was a real hole, and every one was found by hand, by accident, while looking for something else โ which is precisely why the tool now exists:
| What was open | Who could do it |
|---|---|
| The shared settings endpoint (#829) |
Any analyst could rewrite the vCenter and Intune credentials, every AI API key, the SSO config โ and set max_failed_logins to 9999, switching off brute-force lockout |
| Six Intune endpoints (#830) | Any analyst could trigger a full sync of the Intune tenant |
| All 49 RFP Builder endpoints (#833) | Any analyst could read, edit or delete any RFP, or overwrite its AI key โ while the pages were correctly gated |
| An audit-log endpoint (#834) | Anyone not even logged in could forge audit entries attributed to any analyst |
| The LMS settings page (#836) | A learner could open the course-authoring settings by typing the URL |
Administrators only.
- Add a role โ a name and description.
- Edit a role โ tick its capabilities (grouped by module, umbrella first, sensitive ones badged), and choose the analysts and teams that hold it. All saved as one unit.
- A role with no capabilities grants nothing. An inactive role grants nothing either โ so you can switch one off without deleting it or un-assigning everyone.
The picker is generated from the code's capability registry, so a capability the code defines always appears, and one it doesn't can never be saved.
- Nothing changes for administrators โ they bypass the layer.
- Non-admins lose settings access they may incidentally have had. Before a module is wired in, its settings are reachable by anyone with the module; once wired, a non-admin needs a role. That is the intended tightening, it's called out in the changelog per module, and operational access is untouched โ a non-admin can still do their actual job the moment the upgrade lands.
All 16 modules. 76 capabilities. 143 capability-guarded endpoints.
| Module | Capabilities |
|---|---|
| Tickets | 14 โ departments, ticket types, ticket origins, statuses, priorities, SLA, rota, rota locations, email templates, general, CSAT, mailboxes ๐, messaging ๐, reply cleanup ๐ |
| Asset Management | 7 โ types, statuses, locations, suppliers, warranty, vCenter ๐, Intune ๐ |
| Contracts | 7 โ supplier types/statuses, contract statuses, payment schedules, contract terms, RFP departments, RFP AI ๐ |
| Change Management | 5 โ fields, statuses, priorities, types, impacts |
| Tasks | 5 โ statuses, priorities, calendar, card, tags |
| Knowledge | 4 โ email ๐, AI ๐, embeddings ๐, recycle bin |
| CMDB | 3 โ classes, relationship types, AI ๐ |
| Problem Management | 3 โ statuses, priorities, AI ๐ |
| Service Status | 3 โ services, statuses, impacts |
| Morning Checks | 2 โ checks, statuses |
| Forms | 2 โ layout, AI ๐ |
| Workflow | 2 โ AI ๐, formats |
| Calendar | 1 โ categories |
| Process Mapper | 1 โ step types |
| Software | 1 โ API keys ๐ |
| LMS | 1 โ lms.manage (module access lets you take assigned training; the capability lets you manage it) |
Every module also has a <module>.manage umbrella. ๐ = sensitive: reaches credentials, email, money or the audit trail.
It has ~20 areas, not tabs, and every one is administration by definition โ companies, analysts, teams, SSO, the security policy, branding, database verification. There is no operational half to separate out, which is the whole premise of this layer. Splitting it would create permissions only administrators would ever hold โ which is what is_admin already means.
- Roles โ Developer Guide โ add a capability, convert a module, and the traps that bite
- Module Access Control โ Layer 1, which modules an analyst can enter
-
Admin Access Control โ the
is_adminSystem gate - Security โ the whole picture
- Raising the PHP floor to 8.1 โ why capabilities are class constants rather than enums