Module Access Developer Guide - edmozley/freeitsm GitHub Wiki
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: every page and every endpoint gets a guard - reads included. A read used by several modules gets
requireAnyModuleAccessJson([...])naming all of them; only an endpoint that genuinely has no module (sign-in, your own account, a token feed, the bell on every page) stays open, and it goes on the allow-list intests/module-access-coverage.phpwith the reason. That test fails on anything else.
โ ๏ธ Changed in 2.10.2. Until then the rule was "guard writes, leave reads open", and it left 188 reads - ticket threads, contracts, asset lists, software API keys, decrypted settings - readable by any signed-in analyst whose team was never given the module. See Restricted analysts could read other modules' data.
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 endpoint inapi/<dir>/, reads included; a read other modules also call getsrequireAnyModuleAccessJson([...])(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.
Every endpoint gets a guard, right after its existing analyst_id auth check (add the functions.php require if missing). The only question is which modules, and the callers answer it:
| Who calls it | Guard |
|---|---|
| Only this module's pages | requireModuleAccessJson('<moduleKey>') |
| This module and others (an owner picker, a lookup used in three modules, a read the System screens use too) |
requireAnyModuleAccessJson(['<key>', '<other>', 'system']) - every module whose pages call it |
| Only System (administrator) screens |
requireModuleAccessJson('system') or require_once admin_api_guard.php - the same check ('system' resolves to analystIsAdmin()) |
| Everybody, from every page - the notification bell, global search, your own preferences, the company switcher | No module guard. Add it to $EXEMPT in tests/module-access-coverage.php with the reason, and make sure it does its own filtering (global search checks each result's module) |
| Something with no session at all - a token feed, a webhook, an API-key agent | No session guard is possible. It authenticates another way; exempt it with that reason |
Finding the callers: most front-end code calls an endpoint by its bare name (
API_BASE + 'get_x.php'), so grep the file name, then check whichAPI_BASEeach hit uses -get.php,list.phpandget_attachment.phpexist in a dozen folders. For a sharedassets/js/*.jsfile, find which pages load it. In the 2.10.2 audit the module had to be worked out from the callers for every one of 188 reads; guessing from the folder name would have broken the Teams screen (it callsapi/tickets/).
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.
-
systemmeans administrator โ'system'is not a module an analyst is given; passing it resolves toanalystIsAdmin(). Use it insiderequireAnyModuleAccessJson([...])when the System screens share a read with a module, and for a System-only endpoint either it orincludes/admin_api_guard.php- they are the same check. 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 test โ
php tests/module-access-coverage.phpfails on any endpoint with neither a guard nor a listed reason. Then confirm the module's pages carry one too: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