Skip to content

Module Access Control

Ed Mozley edited this page Jul 11, 2026 · 6 revisions

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.

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 default most mode a team defaulting to all would instead silently hand every member every module. Under least, 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".
  • Known follow-up: read-only endpoints are not yet individually gated (only pages + writes are). A denied analyst can't load a module's UI or change its data, but a hand-crafted GET to a module's own read endpoint would still return JSON. Closing that means mapping each module's cross-module read dependencies first β€” a deliberate, separate hardening pass rather than a risky blanket gate.

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_modules flag 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 to 1); analysts who were restricted (had analyst_modules rows) get can_access_all_modules = 0 and keep those rows. Teams start with no module grants (default 0) β€” which, under the default most mode, 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

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally