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 1)
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 and a save-time "this would leave someone with zero modules" warning matter.)

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-APIs carry the guard β€” a denied analyst gets redirected (pages) or a 403 (APIs), not the module.
  • 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. Each module gets the same "guard the mutations, not the shared reads" judgement.

This per-module sweep is the largest part of the work and is rolled out (and verified) module by module.

The management screen

Under System β†’ Modules (admins only):

  • 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 that opens a modal to view and edit the full set (add/remove teams and analysts, or flip a module to all-access).
  • The mode toggle (most / least) with the persistent strict-mode explainer described above, and a confirm-with-impact prompt when switching.

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. In progress
3 β€” Server-side enforcement A per-module guard (requireModuleAccess) on every module's pages and write-APIs β€” the point at which a denied analyst is actually stopped, not just hidden from. Rolled out and verified module by module. Planned

Until Phase 3 lands, module access is configurable and resolvable but still only drives launcher/landing visibility β€” the same as before this work began. Phase 3 is where it becomes real access control. Nothing in phases 1–2 changes anyone's effective access on upgrade (grandfather + most default + no team grants yet).


Related pages

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally