🛡️ feat: Gate audit-log routes on a dedicated READ_AUDIT_LOG capability

The audit-log routes were gated on `ACCESS_ADMIN`, which conflates "can log
into the admin panel" with "can see who granted what to whom." Anyone with
`ACCESS_ADMIN + READ_CONFIGS` (a config reviewer with no people-management
authority) could read the grant history of every user, group, and role —
information they have no need to know.

`READ_AUDIT_LOG` ('read:audit_log') is now an explicit, separately grantable
read capability with no MANAGE counterpart, matching the append-only nature
of the collection. `seedSystemGrants` iterates `Object.values(SystemCapabilities)`
so existing ADMIN-role seeds pick it up automatically on next startup.

This also makes an "auditor" persona possible: hold `ACCESS_ADMIN + READ_AUDIT_LOG`
without any MANAGE_* grants and you can review history without modifying anything.
This commit is contained in:
Dustin Healy 2026-05-14 11:46:43 -07:00 • committed by Danny Avila
parent 681fd73468
commit 2682d70dff
2 changed files with 13 additions and 3 deletions

View file

@ -7,7 +7,7 @@ const db = require('~/models');
const router = express.Router();
const requireAdminAccess = requireCapability(SystemCapabilities.ACCESS_ADMIN);
const requireAuditLogRead = requireCapability(SystemCapabilities.READ_AUDIT_LOG);
const handlers = createAdminAuditLogHandlers({
listAuditLogPage: db.listAuditLogPage,
@ -15,7 +15,7 @@ const handlers = createAdminAuditLogHandlers({
streamAuditLogEntries: db.streamAuditLogEntries,
});
router.use(requireJwtAuth, requireAdminAccess);
router.use(requireJwtAuth, requireAuditLogRead);
router.get('/', handlers.listAuditLog);
router.get('/export.csv', handlers.exportAuditLogCsv);

View file

@ -42,6 +42,12 @@ export const SystemCapabilities = {
/** Reserved — not yet enforced by any middleware. */
READ_ASSISTANTS: 'read:assistants',
MANAGE_ASSISTANTS: 'manage:assistants',
/**
* Required to list, view, and CSV-export the SystemGrant audit log. Append-only
* by design, so there is no MANAGE counterpart — modifying historical entries
* would defeat the forensic guarantee.
*/
READ_AUDIT_LOG: 'read:audit_log',
} as const;
/**
@ -224,6 +230,10 @@ export const CAPABILITY_CATEGORIES: CapabilityCategory[] = [
{
key: 'system',
labelKey: 'com_cap_cat_system',
capabilities: [SystemCapabilities.ACCESS_ADMIN, SystemCapabilities.READ_USAGE],
capabilities: [
SystemCapabilities.ACCESS_ADMIN,
SystemCapabilities.READ_USAGE,
SystemCapabilities.READ_AUDIT_LOG,
],
},
];