feat(rbac): make Settings authorization permission-aware (#1738)

* feat(rbac): make Settings authorization permission-aware

Align Settings visibility and mutations with the existing permission matrix so Node Admin can edit node-scoped operational settings while system and credential surfaces stay Admin-protected.

* fix(rbac): tighten settings permission buckets and tests

Collapse settings key permission maps into one source of truth, and cover mixed PATCH atomicity plus image-update enabled writes.

* fix(rbac): tighten Settings scoped grants and CI assertions

Empty settings PATCH fails closed, node:manage is scoped to the active
node, system-only Settings stay hidden without system:settings, and
Check updates / webhooks mutate gates follow the permission matrix.

* fix(rbac): defer Settings section fallback until authz is ready

Keep deep links to permission-gated sections (e.g. license) intact while
can() is still fail-closed during permission metadata load.

* docs(settings): clarify Notifications channels vs routing authz

Channels use node:manage via /api/agents; routing and mute stay Admin-only.
This commit is contained in:
Anso
2026-07-30 10:25:13 -04:00
committed by GitHub
parent c704cb54d2
commit a3026f47a8
46 changed files with 812 additions and 180 deletions
@@ -95,4 +95,37 @@ describe('reachability', () => {
const off = ctx({ experimental: false, experimentalReady: true, isAdmin: true });
expect(isSettingsSectionHidden('fleet-mesh', off)).toBe(false);
});
it('defers settings permission hides until authz is ready', () => {
const loading = ctx({
permissionsStatus: 'loading',
isAdmin: false,
can: () => false,
});
expect(isSettingsSectionHidden('webhooks', loading)).toBe(false);
expect(isSettingsSectionHidden('license', loading)).toBe(false);
});
it('hides requiredPermission sections when the operator lacks the permission', () => {
const nodeAdmin = ctx({
isAdmin: false,
can: (a) => a === 'node:read' || a === 'node:manage',
});
expect(isSettingsSectionHidden('webhooks', nodeAdmin)).toBe(true);
expect(isSettingsSectionHidden('license', nodeAdmin)).toBe(true);
expect(isSettingsSectionHidden('users', nodeAdmin)).toBe(true);
expect(isSettingsSectionHidden('api-tokens', nodeAdmin)).toBe(true);
expect(isSettingsSectionHidden('registries', nodeAdmin)).toBe(true);
expect(isSettingsSectionHidden('nodes', nodeAdmin)).toBe(false);
expect(isSettingsSectionHidden('host-alerts', nodeAdmin)).toBe(false);
expect(isSettingsSectionHidden('developer', nodeAdmin)).toBe(true);
expect(isSettingsSectionHidden('data-retention', nodeAdmin)).toBe(true);
expect(isSettingsSectionHidden('image-updates', nodeAdmin)).toBe(true);
});
it('hides adminOnly settings sections for non-admins', () => {
const nodeAdmin = ctx({ isAdmin: false, can: () => true });
expect(isSettingsSectionHidden('sso', nodeAdmin)).toBe(true);
expect(isSettingsSectionHidden('recovery', nodeAdmin)).toBe(true);
});
});