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.
Roles dev guide: the guard that fatals, and why your test says it passed
Add requireCapabilityJson() but forget to require rbac.php and the endpoint
doesn't refuse anyone — it dies with "Call to undefined function". That is worse
than no guard: it looks protected and is broken for everyone, admins included.
And PHP serves a fatal as HTTP 200, so a status-code check reports a cheerful
PASS on a completely broken endpoint. That is exactly how it slipped through on
Change Management (#840): one endpoint requires its includes with __DIR__ rather
than a relative path, so the automated insertion missed it, and the test agreed.
Documents both defences: D005 now catches it statically and reports it as BROKEN
above everything else, and the testing section now says in terms — check the
response BODY, never the status code alone.
Roles dev guide: say "two", not "one" — the page contradicted itself
It opened with "the manifest is the single declaration… there is no second list
to keep in step", and then two paragraphs later told you to write two things.
A developer reading that would reasonably ask which it is.
It is TWO, and that is now the first thing on the page:
1. the NAME — a Cap:: constant
2. the DETAILS — the module's manifest entry
…with a diagram of the five things generated from them, and — the part that was
missing — WHY it cannot be one. The name has to be a real constant, a constant
has to exist before the code runs, and generating one needs a build step this
project deliberately doesn't have. And the constant isn't ceremony: it IS the
safety property. Cap::ASSETS_VCENTER misspelled is an immediate fatal error;
'assets.vcentre' misspelled is a silent permanent 403 the administrator never
sees. So the second list is the one worth having, and two is the floor.
Also clarifies that the residual block in settings_keys.php is a TO-DO list of
unconverted modules, not a duplicate — it empties as the roll-out proceeds.
The moving-parts table is demoted below this and now marks each row as
hand-written or derived, so it reads as reference rather than as the lede.
RBAC: the manifest is now the single declaration, and document D005
Roles — Developer Guide: the recipe changed. The capability registry, the module
list and the setting-key map are all DERIVED from <module>/settings/manifest.php
now, so adding a capability is a constant plus a manifest entry — there is no
capRegistry() to hand-edit and no settings_keys.php row to promote. Calls out the
one seam derivation doesn't cross (a Cap:: constant no manifest claims, which is
a capability nobody can hold — so any guard using it 403s everyone but admins,
permanently and invisibly to the admin who bypasses the check).
Adds a section on D005, the endpoint audit, and the discipline behind it: type
safety catches a MISSPELLED permission, nothing catches one nobody wrote. Lists
the five holes that were each found by hand, by accident. And the rule that keeps
the tool useful — if you add an auth mechanism, teach it, because its first run
gave five criticals of which four were false alarms, and a scanner that cries
wolf is ignored within a week.
Security: adds the auditing section so the tool is discoverable from the security
page, not just the developer guide.
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.