-
Notifications
You must be signed in to change notification settings - Fork 15
Roles and Permissions
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.
This is deliberately not an action-level permission system for everyday work. There is no tick-box for "reply to a ticket" or "take a course". Layer 2 governs a module's settings surface and nothing else. The day-to-day job stays governed by module access.
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.
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
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
- MobileβFriendly
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)