-
Notifications
You must be signed in to change notification settings - Fork 15
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).
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).
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.
A single site-wide setting, module_permission_mode (default most), decides how the sources combine.
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.
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.)
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.
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
GETto 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.
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.
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.
- 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.
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.
-
Admin Access Control β the
is_adminSystem gate + the guard pattern this reuses - Multi-Tenancy β the company-access model this mirrors
- Security
- Issue #30
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)