A sweep of the URL map found 165 GET routes that answered anybody, with no token at all: the whole asset inventory, every machine, PC, printer and network device, the USB checkout history and who is holding which drive, the staff directory with its photographs, every core report. Guest access had been argued about one feature-flag at a time while the front door stood open. Each of those now carries the read permission it belongs to. That also makes the guest role mean something: a site can open exactly the reads it chooses, and nothing follows by accident. Thirty-three endpoints stay public, and each is named with its reason rather than left to be inferred: the app shell and its bootstrap, the settings allowlist (the handler already filters an anonymous caller down to branding and a few keys), images fetched by <img src> which carries no Authorization header, what a display kiosk renders before anybody signs in, the parts and USB kiosks, the client scripts that run as SYSTEM before a user session exists, and a secure-send link whose unguessable token IS the authorisation. tests/test_core/test_public_read_surface.py is the standing gate, the companion to the mutating-route sweep that has guarded writes for months. A new public read fails CI until somebody adds it to that list in front of a reviewer. Four further reads need a login but no permission, because the handler decides from who you are - your own user record, your own API tokens, the role catalogue the grid renders - and the same test proves each still refuses an anonymous caller. Verified not vacuous by ungating one route and watching the sweep name it. The employees plugin gained employees.view; the directory is names, SSOs, photographs and reporting lines, and it had no permission of any kind. Around ninety tests asserted payload shapes against endpoints that answered anybody. They are about what the answer says, not about who may ask, so they are signed in now rather than the gate being loosened. The ones that assert an anonymous caller is refused were left exactly as they were.
USB plugin - CMMC USB check-in/out database contract
The USB plugin tracks removable-media check-in/out for CMMC compliance. Device
and log state live in a separate, read-write MySQL database (cmmc_usb),
reached with parameterized pymysql via cmmc_usb_connection(). Names of people
are resolved from the HR employee directory (see the employees plugin).
The plugin's own reference tables (usbdevicetypes, usbdevices,
usbcheckouts) live in the main app database; only the live check-in/out data
is in cmmc_usb.
The schema is standardized across sites - the
cmmc_usbcheck-in/out solution is the same deployment everywhere, so the tables below match as-is and no schema adaptation is needed. The database name may differ per site, though - setcmmc_usb_db_name(defaultcmmc_usb) to match the local name. The view recipe at the end is only a fallback for a site that somehow differs. (Contrast the employee directory, which genuinely varies per site.)
Connection
Credentials resolve settings-first, then environment, except the password which is env-only (never stored in the app database).
| Field | Setting key (editable in the setup wizard) | Env var (fallback) |
|---|---|---|
| Host | cmmc_usb_db_host |
CMMC_USB_DB_HOST |
| Database | cmmc_usb_db_name |
CMMC_USB_DB_NAME |
| User | cmmc_usb_db_user |
CMMC_USB_DB_USER |
| Password | (not stored) | CMMC_USB_DB_PASSWORD |
Set the password in .env; the setup wizard's Features step shows the exact
line to paste. This database is read-write - the app account needs
SELECT, INSERT, UPDATE. Engine is MySQL/MariaDB (pymysql).
Required schema
The plugin runs raw SQL against these tables (or views - see below).
devices
| Column | Type | Notes |
|---|---|---|
device_id |
VARCHAR | Primary key; the device serial/tag |
device_desc |
VARCHAR | Description |
device_owner |
VARCHAR | Owner |
status |
VARCHAR | Check-in/out state |
locker_location |
VARCHAR | Where the device is stored |
Operations: SELECT (list + by id), INSERT (register device), UPDATE
(edit fields, change status).
checkinoutlog
| Column | Type | Notes |
|---|---|---|
log_id |
INT (PK) | Auto id |
badge_number |
VARCHAR | Person's badge |
device_id |
VARCHAR | FK to devices.device_id |
action |
VARCHAR | check-in / check-out |
timestamp |
DATETIME | When it happened |
scanned_viruses |
(text/int) | Scan result |
locker_location |
VARCHAR | Locker at time of event |
sanitized |
(bool/int) | Sanitization flag |
Operations: SELECT (history per device), INSERT (log an event).
users
| Column | Type | Notes |
|---|---|---|
badge_number |
VARCHAR | Primary key; the scanned badge |
first_name |
VARCHAR | Given name |
last_name |
VARCHAR | Surname |
Operations: SELECT by badge, INSERT (auto-add a badge on first scan).
Employee directory dependency
To turn a scanned badge into a name, the plugin also reads the HR employees
directory (via the employees plugin's connection). A badge shaped 0<digits>BZ
carries a PayNo (the digits); lookups try employees.SSO and employees.PayNo.
See plugins/employees/README.md for that schema and connection.
Adapting a different site schema (recommended: views)
Sites whose USB-tracking database uses different table/column names should
create read-only/updatable views named devices, checkinoutlog, and
users that map local columns to the names above. Example:
CREATE VIEW devices AS
SELECT
asset_tag AS device_id,
description AS device_desc,
owner AS device_owner,
state AS status,
storage_bay AS locker_location
FROM usb_assets;
Because the plugin writes to devices/checkinoutlog/users, either make the
views updatable (single-table views usually are) or expose real tables with
these column names. Grant the app account SELECT, INSERT, UPDATE.
Notes:
device_idandbadge_numberare the natural keys the plugin matches on.- If
cmmc_usbis unreachable, USB endpoints return an error and the rest of the app keeps working (the feature degrades, it does not crash the app).