Module Access Developer Guide - edmozley/freeitsm GitHub Wiki

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: 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 in tests/module-access-coverage.php with 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.


The two guards

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).

Page โ€” requireModuleAccess()

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> ...

Write API โ€” requireModuleAccessJson()

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 writes

Note the include depth: pages are one level deep (../includes/โ€ฆ), API endpoints two (../../includes/โ€ฆ).


Module keys (and the key โ‰  directory gotcha)

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>/

Adding a brand-new module

A module isn't "known" to access control until its key is in the registry. Wire these, in this order:

  1. Register the key โ€” add it to the $keys array in getModuleRegistry() (includes/functions.php). This is what makes it appear on System โ†’ Modules, in the effective-access tool, and gate correctly.
  2. Launcher entry โ€” add a '<key>' => ['name' => t('common.modules.<key>.name'), 'path' => '<dir>/', 'icon' => 'โ€ฆ'] row to $modules in includes/waffle-menu.php.
  3. 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; ?>
  4. Display name โ€” add '<key>' => ['name' => 'โ€ฆ', 'description' => 'โ€ฆ'] under modules in every lang/<locale>/common.php (at least en).
  5. Page gate โ€” put requireModuleAccess('<key>') on every user-facing page in the module's directory (index + any sub-pages / help pages).
  6. API gate โ€” put requireModuleAccessJson('<key>') on every endpoint in api/<dir>/, reads included; a read other modules also call gets requireAnyModuleAccessJson([...]) (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.


Adding a page to an existing module

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.

Adding an endpoint to an existing module

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 which API_BASE each hit uses - get.php, list.php and get_attachment.php exist in a dozen folders. For a shared assets/js/*.js file, 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 calls api/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).


Gotchas

  • functions.php must 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 try that 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.
  • system means administrator โ€” 'system' is not a module an analyst is given; passing it resolves to analystIsAdmin(). Use it inside requireAnyModuleAccessJson([...]) when the System screens share a read with a module, and for a System-only endpoint either it or includes/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).

Verifying your wiring

  1. Lint everything you touched: php -l <file> โ€” must be clean.
  2. Coverage test โ€” php tests/module-access-coverage.php fails 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>/
    
  3. 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.

Related pages

โš ๏ธ **GitHub.com Fallback** โš ๏ธ