Module Access Control - edmozley/freeitsm GitHub Wiki
Module Access Control
How FreeITSM decides which modules an analyst may use โ granted to individual analysts and to teams, resolved together under a configurable most/least-permissive policy, and enforced server-side (not merely hidden in the launcher). Companion to Admin Access Control, which governs the one special case (the System module, gated separately by is_admin).
Requested in issue #30. Rolled out in phases โ the data model and resolver first, then the management UI, then the per-module enforcement sweep (the security payoff).
The idea in one line
Module access works exactly like company access (multi-tenancy): individual grants + team grants + an "all" flag, combined at a single choke-point. If you understand how an analyst gets access to a company, you already understand how they get access to a module โ same shape, same union logic, one extra twist (the most/least toggle).
The model
Each analyst's access is computed from a few sources, each of which contributes a set of module keys:
| Source | Contributes |
|---|---|
| The analyst | can_access_all_modules = 1 โ all modules; else the rows in analyst_modules |
| Each team they belong to | can_access_all_modules = 1 โ all modules; else the rows in team_modules |
The tables mirror company access precisely:
| Company access | Module access |
|---|---|
analysts.can_access_all_tenants |
analysts.can_access_all_modules (default 1) |
analyst_tenant_access |
analyst_modules |
teams.can_access_all_tenants |
teams.can_access_all_modules (default 0) |
team_tenant_access |
team_modules |
The two can_access_all_modules flags replace the old fragile rule where "an analyst with no rows means full access". Access is now explicit โ an all flag, or a specific set โ which is what makes multi-source resolution and the strict mode below well-defined. Defaults of 1 mean "unrestricted until someone restricts you".
The System module is not part of this โ it's governed by the administrator flag (
is_admin), see Admin Access Control. The resolver excludes it entirely.
Read only (3.8.0)
Change Management and Forms offer a second level for a grant: Full or Read only, chosen beside each team and analyst in System โ Modules when you open the module.
- Read only lets someone open the module and read everything there - records, approval history, text history, exports, the evidence pack - and change nothing. Every write endpoint refuses them (
403, coderead_only), and so do the change and form services behind them, which also covers an API key acting as that person. - Approving is not writing. A read-only analyst named on a change's CAB can still vote, and one named as a change's or form's approver can still decide. So an approver is a Read only grant plus being named; a reviewer is a Read only grant and nothing else.
- Levels combine like grants. In
mostmode the highest level any source gives wins (an analyst's own Full beats a team's Read only); inleastmode the lowest. An all modules flag is Full everywhere. - The effective-access tool shows the level and why, e.g. "Granted via team "Auditors" - Read only".
- Saving an analyst's or a module's grants keeps each grant's level.
- Other modules show no level and are Full wherever granted. A module is only given Read only once every one of its write endpoints carries the write guard -
tests/module-access-coverage.phpfails otherwise.
Developers: analystModuleLevel(), analystCanWriteModule() and requireModuleWriteJson() in includes/functions.php; MODULES_WITH_READ_ONLY lists the modules that offer it. analystCanAccessModule() is unchanged - read is access.
Combining the sources โ the most/least policy
A single site-wide setting, module_permission_mode (default most), decides how the sources combine.
most โ permissive (union) ยท the default
An analyst can use a module if any source grants it โ their own access, or any team they're in. This equals the historical behaviour and the company-access model.
least โ strict (intersection)
An analyst can use a module only if their own access and every team they belong to grant it.
Worked examples โ analyst in teams A and B:
| Own grant | Team A | Team B | most result |
least result |
|---|---|---|---|---|
| Tickets | Tickets, Assets | Tickets | Tickets, Assets | Tickets |
| all | Tickets | Tickets, Assets | all | Tickets |
| Tickets, Assets | all | all | all | Tickets, Assets |
| Tickets | (none) | all | Tickets | nothing |
[!WARNING] The strict-mode foot-gun. Under
least, a team with no modules granted removes all access from its members โ even modules they were granted individually (the last row above). Granting a module to a person will not give them access if any of their teams lacks it. This is intentional and powerful, but surprising โ so the management screen shows a prominent, persistent explainer whenever strict mode is on, and the effective-access tool is the way to see what anyone can actually reach. (Teams default to granting no modules โ the same conservative default as team company access โ because under the defaultmostmode a team defaulting to all would instead silently hand every member every module. Underleast, that same "empty team" is the lock-out case, which is why the strict-mode explainer is shown and the effective-access checker exists.)
Resolution is one choke-point
All of the above lives in a single function, getAnalystAllowedModules($conn, $analystId), which returns the effective module list (or null for "all"). It caches per request. Every place that needs "can this analyst use module X?" goes through it โ the launcher, the landing page, and (the important part) the server-side guards below. One place to reason about, one place to get right.
Enforcement is server-side
Historically, module access only decided which cards appeared in the launcher and on the landing page โ which meant a "restricted" analyst could simply type the module's URL and walk in. That is not access control. So enforcement is real:
- A helper
analystCanAccessModule()and page/JSON guards (requireModuleAccess()/requireModuleAccessJson()) mirror the admin gate from Admin Access Control. - Every module's pages and write/mutation APIs carry the guard โ a denied analyst gets redirected (pages) or a
403(APIs), not the module. That's ~230 write endpoints across the 20 user-facing modules, plus every module entry page. - As with the admin sweep, shared read endpoints that other modules or the login page depend on are deliberately not gated โ guarding those would break normal flows (analyst/team/department pickers, KB suggestions, CMDB object reads, calendar feeds, login-event logging). The rule is "guard the mutations, not the shared reads".
- Read endpoints are gated too, since 2.10.2 (Module-Access-Reads-Were-Open);
tests/module-access-coverage.phpkeeps every endpoint either guarded or listed with its reason.
This per-module sweep is the largest part of the work, rolled out and verified module by module.
The management screen
Under System โ Modules (admins only), everything lives on one full-width page so you never have to leave it to answer "who can use what, and why":
- The mode toggle (
most/least) with the persistent strict-mode explainer described above. - Access level โ a panel listing every active team and analyst with an all-modules toggle, so you can grant or revoke all-module access in place (off = restrict them to specific modules, which you then grant below). This is the same
can_access_all_modulesflag the Analyst/Team forms edit โ it just saves you the round-trip to those screens โ and it's name-filterable for larger installs. Toggling saves immediately and live-updates the summary pills and the effective-access checker. - The summary table โ modules as rows (a small, fixed axis), with two columns โ Teams with access and Analysts with access โ shown as pills, with a "+N more" chip. Clicking a row opens a per-module modal to tick exactly which teams and analysts get that module. Anyone who is all-modules shows as always-on there (change that with the Access-level toggles, not the modal).
The screen uses the shared settings chrome throughout โ the canonical modal and the global toast โ so it feels like the rest of System.
Effective access โ the "why"
Pick an analyst and get a table of every module with a โ /โ and a reason chain:
- โ Granted directly
- โ Granted via team "Service Desk"
- โ Not granted โ no individual or team access
- โ Granted individually, but overridden โ you're in "Contractors", which lacks it, and mode is strict (least-permissive)
Once access can come from several places and be cancelled by a strict-mode conflict, "why can/can't X see module Y?" is genuinely hard to eyeball โ so this tool is essential, not a nicety. It's the same show-your-working philosophy as the email-routing test, and it doubles as the verification harness for the whole feature.
Safe upgrade
- On the upgrade that adds
can_access_all_modules, existing analysts are grandfathered: everyone keeps exactly what they had. Analysts who were unrestricted stay all-access (the column defaults to1); analysts who were restricted (hadanalyst_modulesrows) getcan_access_all_modules = 0and keep those rows. Teams start with no module grants (default0) โ which, under the defaultmostmode, changes nothing (a team grants access, it never removes it), so members keep exactly the access they had until an admin deliberately grants modules to a team. - The mode defaults to
most, so an upgrade never silently tightens anyone. - Because System is gated separately by
is_admin, an administrator can always reach System โ Modules to fix a misconfiguration โ the built-in safety net against locking people (or yourself) out.
Implementation phases
The feature is deliberately built and shipped in verified phases โ the foundation everything hangs off first, then the tools to configure it, then the enforcement that gives it teeth. Each phase is safe to ship on its own.
| Phase | What it delivers | State |
|---|---|---|
| 1 โ Data model + resolver | can_access_all_modules flags (analysts + teams), the team_modules table, the module_permission_mode setting, and getAnalystAllowedModules() rewritten to combine every source under most/least. One-time grandfather so nobody's access changes on upgrade. |
Done |
| 2 โ Configuration UI | The System โ Modules summary (modules ร team/analyst pills + per-module edit modal), the most/least toggle with the strict-mode explainer, the all-access flag on the Analyst/Team forms, and the effective-access tool. | Done |
| 3 โ Server-side enforcement | Page + write-API guards (requireModuleAccess / requireModuleAccessJson) on every module โ the point at which a denied analyst is actually stopped, not just hidden from. ~230 write endpoints + all module entry pages, verified module by module. Read-only endpoints are a documented follow-up (see above). |
Done |
All three phases have shipped. Nothing in the rollout changes anyone's effective access on upgrade (grandfather + most default + no team grants yet) โ access only tightens once an admin deliberately restricts an analyst or switches to least.
Related pages
- Module Access โ Developer Guide โ how to wire a new module / page / endpoint into this enforcement (the contributor checklist)
- Admin Access Control โ the
is_adminSystem gate + the guard pattern this reuses - Multi-Tenancy โ the company-access model this mirrors
- Security
- Issue #30