-
Notifications
You must be signed in to change notification settings - Fork 15
Module Access Developer Guide
How to wire a new module, page, or endpoint into the module-access enforcement built in Module Access Control Phase 3, so it respects who's allowed where for free. If you skip this, your new code is an open door β a restricted analyst can reach it by typing the URL.
This is the module-access twin of the "Adding a new System feature" checklist on Admin Access Control; the two guards behave the same way and follow the same discipline.
The one rule to remember: a page or a write/mutation endpoint in a module gets a guard. A shared read that other modules or the login page depend on does not. When unsure,
grepthe codebase for who calls it before gating.
Both live in includes/functions.php, take only the module key, open their own DB connection, and fail closed. 'system' is not a module key here β it defers to is_admin (see Admin Access Control); any unknown key is denied.
| Guard | Use on | Effect on a denied analyst |
|---|---|---|
requireModuleAccess('<key>') |
A module page |
302 redirect to the launcher (?denied=<key>) |
requireModuleAccessJson('<key>') |
A module write API |
403 { success:false, error:β¦ } and exit
|
Both need includes/functions.php loaded first, and must run before any output (pages) or any DB write (APIs).
session_start();
require_once '../config.php';
require_once '../includes/functions.php'; // <-- must be loaded
require_once '../includes/i18n.php';
// ... theme / timezone init ...
requireModuleAccess('assets'); // <-- before any HTML
$current_page = 'assets';
?>
<!DOCTYPE html> ...session_start(['read_and_close' => true]);
require_once '../../config.php';
require_once '../../includes/functions.php'; // <-- must be loaded
if (!isset($_SESSION['analyst_id'])) { /* existing 401 auth check */ }
requireModuleAccessJson('assets'); // <-- right after auth, before writesNote the include depth: pages are one level deep (../includes/β¦), API endpoints two (../../includes/β¦).
The canonical list of module keys is getModuleRegistry() in includes/functions.php. The guards, the summary screen, and the effective-access tool all iterate it. The key you pass to a guard must be one of these.
Several keys don't match their directory β pass the key, not the folder name:
| Key (pass this) | Page directory | API directory |
|---|---|---|
assets |
asset-management/ |
api/assets/ |
changes |
change-management/ |
api/change-management/ |
problems |
problem-management/ |
api/problem-management/ |
wiki |
system-wiki/ |
api/wiki/ |
| (all others) | same as key | api/<key>/ |
A module isn't "known" to access control until its key is in the registry. Wire these, in this order:
-
Register the key β add it to the
$keysarray ingetModuleRegistry()(includes/functions.php). This is what makes it appear on System β Modules, in the effective-access tool, and gate correctly. -
Launcher entry β add a
'<key>' => ['name' => t('common.modules.<key>.name'), 'path' => '<dir>/', 'icon' => 'β¦']row to$modulesinincludes/waffle-menu.php. -
Landing-page card β in the root
index.php, wrap your card in the standard visibility check so it auto-hides for denied analysts:<?php if ($allowed_modules === null || in_array('<key>', $allowed_modules)): ?> β¦ card β¦ <?php endif; ?>
-
Display name β add
'<key>' => ['name' => 'β¦', 'description' => 'β¦']undermodulesin everylang/<locale>/common.php(at leasten). -
Page gate β put
requireModuleAccess('<key>')on every user-facing page in the module's directory (index + any sub-pages / help pages). -
API gate β put
requireModuleAccessJson('<key>')on every write/mutation endpoint inapi/<dir>/; leave shared reads open (see below).
Steps 1β4 make the module visible and configurable; steps 5β6 make it enforced. Do all six β visibility without enforcement is the exact hole Phase 3 closed.
One line: requireModuleAccess('<moduleKey>'), after config.php + functions.php load and before any HTML. If the page didn't already require_once functions.php, add it. That's it β the rest is inherited.
Decide read vs write:
-
Writes (create/update/delete/save/assign/import/send/reorder/toggle/β¦ β anything that mutates or performs an action): add
requireModuleAccessJson('<moduleKey>')right after the endpoint's existinganalyst_idauth check. Add thefunctions.phprequire if missing. -
Reads (
get_/list_/search_/export_/feeds/attachments): leave open if anything outside the module calls them β owner pickers, KB suggestions, CMDB object reads, calendar feeds, etc. are consumed cross-module and gating them breaks normal flows. If the read is genuinely private to this module and never called from elsewhere, you may gate it, but the safe default is open.
The test:
grep -rn "your_endpoint.php" --include=*.js --include=*.php .β if only your module's own front-end calls it and it mutates, gate it. If another module (orlogin.php) calls it, or it's a pure read, leave it open.
Endpoints that are analyst-triggered UI actions but run through an engine/webhook/cron (e.g. the workflow "test fire") still get the guard as long as they carry their own analyst_id session check β they're reached from the UI, not by the headless worker (the worker calls the engine PHP directly, not the HTTP endpoint).
-
functions.phpmust be loaded, or the guard is an undefined-function fatal. Most endpoints already require it; add it if not. -
Order matters β the page guard must run before any byte is sent (it uses
header()), and the API guard before any write. -
Fail closed β both guards deny on any DB/exception. Don't wrap them in a
trythat swallows the exit. - Restricted analysts don't auto-inherit new modules. A restricted analyst's access is a fixed list; when you ship a new module, all-access analysts get it immediately (their access is "all"), but restricted ones won't until an admin grants it on System β Modules. That's correct β new surface area shouldn't silently open to someone who was deliberately restricted.
-
systemis separate β never pass'system'to these guards for a normal module; System is gated byis_adminviaincludes/admin_api_guard.php. See Admin Access Control. -
Key, not directory β pass
'assets', not'asset-management'(see the table above).
-
Lint everything you touched:
php -l <file>β must be clean. -
Coverage grep β confirm the module's pages and write endpoints carry a guard:
grep -rl "requireModuleAccess" <dir>/ api/<dir>/ -
Live test with the effective-access tool β on System β Modules, use the Access level panel to restrict a test analyst, then the effective-access checker to confirm your module shows β, and try to open its page/POST its write endpoint as that analyst β you should be bounced (
302) /403. Then grant it and confirm access returns. This tool doubles as the verification harness for the whole feature.
- Module Access Control β the model, resolver, most/least policy, and management screen this guide plugs into
-
Admin Access Control β the
is_adminSystem gate + the "guard the mutations, not the shared reads" discipline this mirrors - Architecture
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
- β³ π’ Ticket numbering
- β³ π 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)