Reflect global settings for non-admin viewers

This commit is contained in:
Pulse Autonomous Maintainer
2026-08-11 19:42:56 +01:00
parent 5015755245
commit 986a281006
13 changed files with 156 additions and 23 deletions
@@ -1238,6 +1238,12 @@ instead of lifecycle-local centered icon/text shells.
## Extension Points
The authenticated runtime-display projection under shared `internal/api/` may
carry the effective global `disableDockerUpdateActions` boolean so non-admin
viewers render container updates read-only. This is API/settings presentation
only: it neither disables image-update detection nor changes agent update,
registration, profile, command, enrollment, or fleet-control authority.
Manual scoped Patrol work that reaches `internal/api/ai_handlers.go` (such as an
alert-initiated targeted Patrol check via `POST /api/ai/patrol/run`) is
investigation-only over agent-reporting resources: it must not alter agent
@@ -9198,7 +9198,8 @@ case honest against live 403s on `/api/config/nodes` and `/api/system/settings`.
`internal/api/router_routes_registration.go`, uses `RequireAuth` plus
`monitoring:read`, matching the sibling `/api/runtime/branding` route. Its
explicit `RuntimeDisplayResponse` whitelist contains only `theme`,
`fullWidthMode`, `disableDockerUpdateActions`, and `reduceProUpsellNoise`.
`fullWidthMode`, `disableDockerUpdateActions`, `telemetryEnabled`, and
`reduceProUpsellNoise`.
Authenticated app bootstrap needs those values for every role. It must not read
them from `GET /api/system/settings`, whose `RequireAdmin` plus `settings:read`
@@ -9207,12 +9208,22 @@ URL, webhook-network, telemetry, and login configuration. The admin route and
payload remain unchanged; the new route never embeds or projects
`config.SystemSettings`, so future settings fields are not published by
omission. `disableDockerUpdateActions` comes from effective runtime config so
the environment override remains authoritative.
the environment override remains authoritative. `telemetryEnabled` is the
effective boolean only: it lets a read-only General panel reflect the
operator's global privacy choice, but it never exposes the telemetry preview,
rotating install ID, environment overrides, or any other admin setting.
When the General settings state owner receives the expected 403 from the admin
settings route, it must fall back to this narrow runtime projection for those
two global booleans and synchronize the shared Docker-action store. It must not
retry through a broader route or replace an operator-disabled telemetry value
with the frontend default.
`TestContract_RuntimeDisplayServesPresentationValuesWithoutAdmin` pins a viewer
receiving 200 from the runtime route while still receiving 403 from the admin
route. `TestHandleGetRuntimeDisplay_PublishesOnlyPresentationFields` pins the
serialized whitelist, and the frontend bootstrap tests pin that
serialized whitelist, the frontend settings/API tests pin the read-only
fallback and effective boolean values, and the frontend bootstrap tests pin that
`useAppRuntimeState` never calls `getSystemSettings()`.
### Security status names every admin-only Settings panel capability
@@ -312,6 +312,14 @@ the `white_label` branding entitlement.
metadata, or remediation output.
2. Change security policy, hardening guidance, or supported auth boundaries through `SECURITY.md`.
3. Change telemetry/privacy settings state handling through `frontend-modern/src/components/Settings/useSystemSettingsState.ts`.
A non-admin General panel may read the effective `telemetryEnabled` boolean
from the authenticated `/api/runtime/display` whitelist after the complete
admin settings request is refused. That fallback must expose no preview
payload, install ID, environment override, origin, webhook-network, or login
configuration, and it must preserve an operator-selected `false` rather than
reverting to the frontend's enabled default. The same response may carry
the effective Docker-action display boolean because both values are needed
to render read-only global state; this does not widen settings write access.
Relay runtime access through `internal/api/router.go` must stay behind the
existing protected route and API-token gates. Testable router seams may
expose relay status to onboarding validation, but they must not broaden
@@ -198,6 +198,12 @@ states do not change the underlying storage evidence or active lifecycle.
## Extension Points
The authenticated runtime-display projection under shared `internal/api/` may
publish effective Docker-action visibility and outbound-telemetry booleans to
read-only viewers. Those presentation/privacy values are not backup evidence,
storage health, recovery readiness, restore scope, or remediation authority,
and storage/recovery consumers must not infer any of those states from them.
Agent-token validation added to `internal/api/connections_aggregator.go` and
its runtime input assembly in `internal/api/connections_alerts.go` is
fleet-governance evidence only. A missing, revoked, or expired host-agent
+15 -17
View File
@@ -1,17 +1,17 @@
{
"version": 1,
"base_sha": "791a2f86bf16871eefe19acf3bedadb4d0d3edaa",
"verified_at": "2026-08-11T14:40:54Z",
"base_sha": "501575524519294805e98b47dd3f0b746df8d175",
"verified_at": "2026-08-11T18:56:00Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/utils/alertDestinationsPresentation.ts"
"frontend-modern/src/api/settings.ts",
"frontend-modern/src/components/Settings/useSystemSettingsState.ts"
],
"content_sha256": {
"frontend-modern/src/utils/alertDestinationsPresentation.ts": "f0b392eb161f3117f543c6a97fcede9f2c42dd079eb3d40a3e491e7f5c287ebd"
"frontend-modern/src/api/settings.ts": "160fdba69870f19682cb234eb582f0f6ea6fb83ef67e0ca3662d2ac0da4b4325",
"frontend-modern/src/components/Settings/useSystemSettingsState.ts": "6e3fc2e3ea80458b30178463fea8f74ffff3498e992c6aa38578daa684653a99"
},
"routes": [
"/alerts/notifications"
],
"routes": ["/settings/system-general", "/docker/containers"],
"viewports": [
{
"width": 1440,
@@ -23,17 +23,15 @@
}
],
"states": [
"Notifications with one failed delivery retained for 7 days and two dead-lettered deliveries retained for 30 days.",
"The delivery warning explained that expired records are removed hourly and that the warning clears after the last retained failure reaches its retention limit when no new terminal failures occur.",
"Desktop Notifications at 1440 by 1000 with the warning and Refresh delivery status control visible and no document or body horizontal overflow.",
"Narrow Notifications at 390 by 844 with the warning and refresh control visible and no document or body horizontal overflow.",
"Healthy queue state after controlled fixture cleanup, with the warning removed."
"The running mock backend had Docker update actions disabled and outbound usage telemetry disabled, while the complete admin settings request was deliberately answered with 403 to exercise the viewer fallback.",
"System General reflected Hide update buttons as enabled and Outbound usage telemetry as disabled from the authenticated runtime projection at desktop and narrow widths.",
"Docker Containers rendered no Update or Update all action buttons while the global hide policy was active.",
"System General had no document or body horizontal overflow at either viewport."
],
"interactions": [
"Inserted three synthetic terminal queue records with no destination or customer data into the ignored isolated runtime database, then opened Alerts and Notifications through visible navigation controls.",
"Confirmed the retained-delivery counts, hourly cleanup explanation, retention-limit clearing condition, and refresh control in the real warning card.",
"Refreshed delivery status at desktop width and confirmed the warning remained while retained failures existed.",
"Repeated the warning and overflow checks at the narrow viewport.",
"Deleted the three synthetic records, refreshed delivery status, and confirmed the warning cleared."
"Started the current managed mock runtime and persisted disableDockerUpdateActions=true plus telemetryEnabled=false through the live settings API.",
"Intercepted only GET /api/system/settings with the expected 403 refusal, then opened System General through the current browser build and confirmed the fallback values from GET /api/runtime/display.",
"Inspected full-page desktop and narrow screenshots and measured document and body overflow.",
"Navigated to Docker Containers at both viewports and confirmed the shared global policy left zero Update or Update all buttons."
]
}
@@ -215,6 +215,7 @@ describe('SettingsAPI', () => {
theme: 'dark',
fullWidthMode: true,
disableDockerUpdateActions: true,
telemetryEnabled: false,
reduceProUpsellNoise: false,
};
vi.mocked(apiFetchJSON).mockResolvedValueOnce(display);
+1
View File
@@ -177,6 +177,7 @@ export interface RuntimeDisplayResponse {
theme?: string;
fullWidthMode?: boolean;
disableDockerUpdateActions?: boolean;
telemetryEnabled?: boolean;
reduceProUpsellNoise?: boolean;
}
@@ -11,6 +11,7 @@ import { useSystemSettingsState } from '../useSystemSettingsState';
// identical to the sibling test.
const mocks = vi.hoisted(() => ({
getSystemSettingsMock: vi.fn(),
getRuntimeDisplayMock: vi.fn(),
updateSystemSettingsMock: vi.fn(),
getTelemetryPreviewMock: vi.fn(),
resetTelemetryInstallIDMock: vi.fn(),
@@ -35,6 +36,7 @@ const mocks = vi.hoisted(() => ({
vi.mock('@/api/settings', () => ({
SettingsAPI: {
getSystemSettings: mocks.getSystemSettingsMock,
getRuntimeDisplay: mocks.getRuntimeDisplayMock,
getTelemetryPreview: mocks.getTelemetryPreviewMock,
resetTelemetryInstallID: mocks.resetTelemetryInstallIDMock,
updateSystemSettings: mocks.updateSystemSettingsMock,
@@ -100,6 +102,7 @@ const flushAsync = async () => {
describe('useSystemSettingsState branch coverage', () => {
beforeEach(() => {
mocks.getSystemSettingsMock.mockResolvedValue({});
mocks.getRuntimeDisplayMock.mockResolvedValue({});
mocks.updateSystemSettingsMock.mockResolvedValue(undefined);
mocks.loadRuntimeBrandingMock.mockResolvedValue(undefined);
mocks.getUpdatePlanMock.mockResolvedValue({
@@ -154,6 +157,26 @@ describe('useSystemSettingsState branch coverage', () => {
return mounted;
};
describe('read-only runtime settings fallback', () => {
it('shows effective Docker-action and telemetry state when admin settings are forbidden', async () => {
mocks.getSystemSettingsMock.mockRejectedValueOnce(new Error('Admin privileges required'));
mocks.getRuntimeDisplayMock.mockResolvedValueOnce({
disableDockerUpdateActions: true,
telemetryEnabled: false,
});
const { hookState, dispose } = mountHook();
await hookState.initializeSystemSettingsState();
await flushAsync();
expect(mocks.getRuntimeDisplayMock).toHaveBeenCalledTimes(1);
expect(hookState.disableDockerUpdateActions()).toBe(true);
expect(hookState.telemetryEnabled()).toBe(false);
expect(mocks.updateDockerUpdateActionsSettingMock).toHaveBeenCalledWith(true);
dispose();
});
});
describe.each([
{
name: 'temperatureMonitoringLocked',
@@ -179,6 +179,19 @@ export function useSystemSettingsState({
}
} catch (error) {
logger.error('Failed to load settings', error);
// Viewers cannot read the admin settings payload, but they still need to
// see the effective server-wide state of read-only controls. Use the
// deliberately narrow authenticated-session projection for those values.
try {
const runtimeDisplay = await SettingsAPI.getRuntimeDisplay();
const dockerActionsDisabled = runtimeDisplay.disableDockerUpdateActions ?? false;
setDisableDockerUpdateActions(dockerActionsDisabled);
updateDockerUpdateActionsSetting(dockerActionsDisabled);
setTelemetryEnabled(runtimeDisplay.telemetryEnabled ?? true);
} catch (runtimeDisplayError) {
logger.warn('Failed to load read-only runtime settings', runtimeDisplayError);
}
}
try {
+9
View File
@@ -24109,6 +24109,9 @@ func TestContract_RuntimeDisplayServesPresentationValuesWithoutAdmin(t *testing.
settings := config.DefaultSystemSettings()
settings.Theme = "dark"
settings.FullWidthMode = true
settings.DisableDockerUpdateActions = true
telemetryEnabled := false
settings.TelemetryEnabled = &telemetryEnabled
if err := persistence.SaveSystemSettings(*settings); err != nil {
t.Fatalf("save system settings: %v", err)
}
@@ -24143,6 +24146,12 @@ func TestContract_RuntimeDisplayServesPresentationValuesWithoutAdmin(t *testing.
if !display.FullWidthMode {
t.Fatal("fullWidthMode = false, want true")
}
if !display.DisableDockerUpdateActions {
t.Fatal("disableDockerUpdateActions = false, want true")
}
if display.TelemetryEnabled {
t.Fatal("telemetryEnabled = true, want false")
}
}
// The refusal itself is the contract; its log severity is not. This pins both
+12 -2
View File
@@ -20,6 +20,7 @@ type RuntimeDisplayResponse struct {
Theme string `json:"theme"`
FullWidthMode bool `json:"fullWidthMode"`
DisableDockerUpdateActions bool `json:"disableDockerUpdateActions"`
TelemetryEnabled bool `json:"telemetryEnabled"`
ReduceProUpsellNoise bool `json:"reduceProUpsellNoise"`
}
@@ -31,11 +32,14 @@ func (h *SystemSettingsHandler) HandleGetRuntimeDisplay(w http.ResponseWriter, r
return
}
response := RuntimeDisplayResponse{}
response := RuntimeDisplayResponse{TelemetryEnabled: true}
if h != nil && h.config != nil {
// Match HandleGetSystemSettings so the environment override wins over
// the persisted value for every role.
// the persisted value for every role. These booleans reveal only the
// effective operator policy, not the admin-only configuration or
// telemetry payload.
response.DisableDockerUpdateActions = h.config.DisableDockerUpdateActions
response.TelemetryEnabled = h.config.TelemetryEnabled
}
if h == nil || h.persistence == nil {
@@ -53,6 +57,12 @@ func (h *SystemSettingsHandler) HandleGetRuntimeDisplay(w http.ResponseWriter, r
response.Theme = settings.Theme
response.FullWidthMode = settings.FullWidthMode
response.ReduceProUpsellNoise = settings.ReduceProUpsellNoise
if h.config == nil || (!h.config.EnvOverrides["disableDockerUpdateActions"] && !h.config.EnvOverrides["PULSE_DISABLE_DOCKER_UPDATE_ACTIONS"]) {
response.DisableDockerUpdateActions = settings.DisableDockerUpdateActions
}
if settings.TelemetryEnabled != nil && (h.config == nil || (!h.config.EnvOverrides["telemetryEnabled"] && !h.config.EnvOverrides["PULSE_TELEMETRY"])) {
response.TelemetryEnabled = *settings.TelemetryEnabled
}
}
if err := utils.WriteJSONResponse(w, response); err != nil {
+25 -1
View File
@@ -76,6 +76,7 @@ func TestHandleGetRuntimeDisplay_PublishesOnlyPresentationFields(t *testing.T) {
"theme": {},
"fullWidthMode": {},
"disableDockerUpdateActions": {},
"telemetryEnabled": {},
"reduceProUpsellNoise": {},
}
for key := range raw {
@@ -94,7 +95,12 @@ func TestHandleGetRuntimeDisplay_UsesEffectiveDockerUpdateActionsSetting(t *test
settings := config.DefaultSystemSettings()
settings.DisableDockerUpdateActions = false
handler := newRuntimeDisplayHandler(t, &config.Config{DisableDockerUpdateActions: true}, settings)
handler := newRuntimeDisplayHandler(t, &config.Config{
DisableDockerUpdateActions: true,
EnvOverrides: map[string]bool{
"PULSE_DISABLE_DOCKER_UPDATE_ACTIONS": true,
},
}, settings)
got, _ := fetchRuntimeDisplay(t, handler)
if !got.DisableDockerUpdateActions {
@@ -102,6 +108,24 @@ func TestHandleGetRuntimeDisplay_UsesEffectiveDockerUpdateActionsSetting(t *test
}
}
func TestHandleGetRuntimeDisplay_UsesEffectiveTelemetrySetting(t *testing.T) {
settings := config.DefaultSystemSettings()
enabled := true
settings.TelemetryEnabled = &enabled
handler := newRuntimeDisplayHandler(t, &config.Config{
TelemetryEnabled: false,
EnvOverrides: map[string]bool{
"PULSE_TELEMETRY": true,
},
}, settings)
got, _ := fetchRuntimeDisplay(t, handler)
if got.TelemetryEnabled {
t.Fatal("telemetryEnabled = true, want the effective config override to win")
}
}
func TestHandleGetRuntimeDisplay_RejectsNonGET(t *testing.T) {
handler := newRuntimeDisplayHandler(t, &config.Config{}, config.DefaultSystemSettings())
req := httptest.NewRequest(http.MethodPost, "/api/runtime/display", nil)
@@ -197,6 +197,29 @@ func TestTelemetryUpdate_GetReturnsEffectiveValue(t *testing.T) {
}
}
func TestRuntimeDisplayPublishesOnlyEffectiveTelemetryState(t *testing.T) {
settings := config.DefaultSystemSettings()
persistedEnabled := true
settings.TelemetryEnabled = &persistedEnabled
handler := newRuntimeDisplayHandler(t, &config.Config{
TelemetryEnabled: false,
EnvOverrides: map[string]bool{
"PULSE_TELEMETRY": true,
},
}, settings)
display, raw := fetchRuntimeDisplay(t, handler)
if display.TelemetryEnabled {
t.Fatal("runtime telemetry state = enabled, want effective disabled override")
}
if _, ok := raw["telemetryEnabled"]; !ok {
t.Fatal("runtime display omitted telemetryEnabled")
}
if _, ok := raw["telemetryPreview"]; ok {
t.Fatal("runtime display exposed admin-only telemetry preview")
}
}
func TestTelemetryPreview_ReturnsCurrentPayload(t *testing.T) {
tempDir := t.TempDir()
cfg := &config.Config{