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
@@ -37,9 +37,9 @@ const DEFAULT_DEVELOPER: DeveloperFields = {
};
export function DeveloperSection({ onDirtyChange }: DeveloperSectionProps) {
const { isAdmin } = useAuth();
const { can, permissionsReady } = useAuth();
const { activeNode } = useNodes();
const readOnly = !isAdmin;
const readOnly = !permissionsReady || !can('system:settings');
const { settings, setSettings, hasChanges, reset, markSaved } = useSettingsDirty<DeveloperFields>({ ...DEFAULT_DEVELOPER });
const { phase, isCurrentNodeLoaded, load, isSaveOwner, captureSaveGuard } = useNodeSettingsLoad(activeNode?.id);
const [isSaving, setIsSaving] = useState(false);
@@ -130,7 +130,7 @@ export function DeveloperSection({ onDirtyChange }: DeveloperSectionProps) {
</SettingsField>
</SettingsSection>
<SettingsActions hint={readOnly ? 'Read-only · admin access required to edit' : (hasChanges ? 'unsaved changes' : undefined)}>
<SettingsActions hint={readOnly ? 'Read-only · permission required to edit' : (hasChanges ? 'unsaved changes' : undefined)}>
{!readOnly && (
<SettingsPrimaryButton onClick={saveSettings} disabled={isSaving || !hasChanges || !isCurrentNodeLoaded}>
{isSaving ? (