RBAC wiki: verified against the code, and two stale claims corrected
Checked every claim the Roles pages make against what the code actually does —
not by reading the wiki back to myself, but by generating the truth from
capabilities.php / the manifests and comparing:
16 modules ✓ 76 capabilities ✓ 143 guarded endpoints ✓
every module has a <module>.manage umbrella ✓
the umbrella satisfies every capability in its module ✓
9 personal-preference tabs declare no capability ✓
an undefined key grants nothing ✓
module 'system' resolves to is_admin ✓
the registry is derived from the manifests ✓
every per-module capability count and every 'sensitive' badge ✓
Two things were stale, both now fixed:
1. The setting-key list in settings_keys.php was described as "a residual list of
modules not yet converted, which shrinks to nothing". It doesn't: every module
IS converted, and what remains is the System module's keys — permanently, by
design, because System is deliberately administrator-only. Corrected in the
wiki and in the code comment.
2. The "guard writes, never reads" rule appeared WITHOUT its credentials exception
on both Roles & Permissions and Security. Only the dev guide carried it. Since
that exception exists precisely because the rule was applied too comfortably —
get_mailboxes was a "read", and it shipped the plaintext OAuth secret to every
analyst — stating the rule without it is the dangerous half.
Also documented two manifest features that had gone in late and never been written
up: a tab may carry a plain 'label' instead of 'label_key' where a module isn't
translated, and renderSettingsTabBar() takes the tab-switcher's function name
(Problem Management's is pmsTab(), and its panels are id="tab-x" not "x-tab").
A converter that assumes the conventions renders a tab bar whose tabs open nothing.
Plus the full field reference for a manifest tab, and the table of shared endpoints
that need per-key authorisation — a recurring shape, not a one-off.
RBAC roll-out complete: all 16 modules, and the rules it taught us
Roles & Permissions: the roll-out table is now the finished picture — 16 modules,
76 capabilities, 143 guarded endpoints, with the sensitive ones marked. Records
why the System module is deliberately NOT converted: it is ~20 areas, every one of
them administration by definition, with no operational half to separate out —
splitting it would create permissions only administrators would ever hold, which
is what is_admin already means.
Developer Guide — the six rules the roll-out taught, promoted above the mechanics
because they matter more:
1. A read that returns CREDENTIALS is not an operational read. get_mailboxes was
a "read", and it shipped the plaintext OAuth client secret to every analyst.
2. A config change offered as a shortcut from an operational screen still needs
the config permission (Kanban columns; CMDB property options).
3. Creating is not always administering (create a tag inline: everyday; delete
one: administration).
4. An endpoint serving several tabs cannot have one guard — authorise per
setting key. A recurring shape, not a one-off.
5. A feature reachable from several modules needs requireAnyModuleAccessJson().
6. Converting a module is an AUDIT of it. Expect to find, not merely to move.
Two new traps, both real:
- CRLF. A guard-insertion script searching for "}\n" found nothing in the CRLF
files, strpos returned false, false+2 = 2, and the guard was spliced into the
middle of "<?php" in ten files — which STILL PASSED php -l, because short open
tags made "<?" valid. The HTTP test caught it, the linter did not.
- The audit can hide a hole as well as find one. D005 reported db_verify — the
endpoint that creates every table in the database — as "signature
authenticated", because it scanned for the word relay_secret and db_verify
declares a relay_secret COLUMN. Match the call, not the word. And check your
write-detection covers what you think: the same tool called CREATE TABLE a read.
Wiki: split the PHP floor page, refresh Roles & Permissions, restructure the sidebar
The PHP floor page had grown to 861 lines and was three documents in a trench
coat: a decision, a language tour, and a deep argument about permission keys.
Split into three, each standing alone:
Raising-the-PHP-Floor (307) the DECISION — where we are, who breaks, the
verdict, and the constants-to-enums bridge
PHP-8-Language-Features (261) the 101 tour of 8.0/8.1 with before/after from
real code, including the features that WOULDN'T
help us, said plainly
PHP-Enums-and-RBAC (353) the one feature the argument turns on — and the
reason a capability is a Cap:: constant and never
a string. Worth reading even if the floor never
moves, because it explains the permission system.
Section numbers and cross-references rewritten by meaning rather than by
find-and-replace, since the sections changed pages.
Roles & Permissions was stale: written before the manifest became the single
declaration and before the endpoint audit existed. Now says that capabilities are
derived from one file per module (and why the four-list version was the bug), and
carries a table of what D005 has actually found — the shared settings endpoint,
six Intune endpoints, all 49 RFP Builder endpoints, unauthenticated audit-log
forgery, and an LMS learner reaching the authoring settings. Every one found by
hand, by accident, which is the point.
Sidebar: the security pages are now grouped by the three layers they belong to
(module access → capabilities → administrator), rather than a flat list, and all
nine RBAC/PHP pages are linked. The two new PHP pages nest under the floor page.
RBAC: bring the wiki up to date with per-tab capabilities
Both Roles pages predated the per-tab decision and described a system that no
longer exists: rbacCapabilities() (now capRegistry), bare-string guards (now
Cap:: constants), "one manage per module, split later" (we went per-tab), and a
rename that orphans grants and needs a data migration (there's an alias map now).
Neither mentioned the settings manifest, the shared-endpoint key ownership, the
sensitive badge, the umbrella, or the security fix.
Roles & Permissions — rewritten around the per-tab model: why a tab is the unit
(vCenter credentials sit next to a lookup list), the umbrella, sensitive badging,
and a full "how it's enforced" section covering all three mechanisms — panels not
rendered rather than hidden, hard guards on every write endpoint, and per-KEY
authorisation on the shared settings endpoint (including the #829 hole it closed).
Plus what is deliberately NOT guarded, and why gating a read breaks the module.
Roles — Developer Guide — rewritten as the extension guide: why capabilities are
constants (a mistyped string 403s silently and is invisible to admins, who bypass
the check — so the typo becomes a privilege escalation), adding/renaming/removing
a capability, and the full six-step recipe for converting a module, worked against
Asset Management. Now carries the traps that actually bit: a get_* endpoint that
isn't a read (get_vcenter performs the sync and Servers calls it), endpoints with
no guard at all, and the reminder that converting a module is an audit of it.
Plus the three-actor HTTP test matrix and a checklist.
Security — the Authorisation section was wrong, not just stale: it claimed the
System module "cannot be disabled" and is always accessible to active analysts,
which stopped being true when is_admin shipped. Rewritten as the three layers,
each with its gate and its default, and the note that hiding a button is never
the control.
Raising the PHP floor — made the constants-to-enums bridge concrete rather than
asserted. Shows the actual migration: call sites are a symbol rename an IDE can
do and the compiler verifies (a missed one FATALS; you cannot half-finish it),
against hunting 200 string literals where a miss keeps working and a typo 403s
in silence. The real work is confined to one file. Also states the honest
caveat: constants make the typo loud, an enum makes the wrong thing impossible.
LMS authoring + AI + Roles: new deep-dive pages, refreshed LMS hub
- LMS.md: rewritten for the two course kinds (authored + SCORM), the
learner/manager split, correct table names, links to the new sub-pages.
- LMS-Authoring.md: native content model, editor, native player, server-side
grading (answer key never on the wire), the CMI-reuse trick, learner/manager
enforcement, SCORM upload safety.
- LMS-AI-Authoring.md: mechanics of the three AI helpers (outline / article→lesson /
lesson→quiz) — grounding, JSON contracts, the draft-never-writes safety model.
- Roles-and-Permissions.md: RBAC Layer 2 (settings permissions) — two layers,
deny-by-default, is_admin bypass, server-side enforcement, the LMS pilot.
- Roles-Developer-Guide.md: add/edit/remove a capability and wire a module's
settings in — registry, guards, the operational-vs-settings split, splitting
a capability later, gotchas.
- _Sidebar.md + Home.md wired up.