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
@@ -20,6 +20,7 @@ import type { useViewNavigationState } from './useViewNavigationState';
import type { Node } from '@/context/NodeContext';
import { useNodes } from '@/context/NodeContext';
import type { PermissionAction } from '@/context/AuthContext';
import { canManageNode } from '@/lib/canManageNode';
type StackListState = ReturnType<typeof useStackListState>;
type NavState = ReturnType<typeof useViewNavigationState>;
@@ -74,6 +75,7 @@ export function useSidebarContextMenu({
menuVisibility: stackActions.getStackMenuVisibility(file),
openAlertSheet: () => overlayState.openAlertSheet(file),
openAutoHeal: () => overlayState.openAutoHeal(file),
canCheckUpdates: canManageNode(can, nodeId),
checkUpdates: () => stackActions.checkUpdatesForStack(),
openStackApp: () => stackActions.openStackApp(file),
deploy: () => stackActions.executeStackActionByFile(file, 'deploy', 'deploy'),
@@ -179,7 +181,7 @@ export function useSidebarContextMenu({
// deps would force a rebuild on every parent render and defeat the memo.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
stackListState.stackStatuses, stackListState.stackPorts, stackListState.stackSelfFlags, isAdmin,
stackListState.stackStatuses, stackListState.stackPorts, stackListState.stackSelfFlags, isAdmin, can,
stackListState.isPinned, stackListState.labels, stackListState.stackLabelMap,
stackListState.pin, stackListState.unpin, activeNode?.type, activeNode?.api_url, activeNode?.id,
hasCapability, navState.openMuteRulesWithPrefill,