mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
Reflect global settings for non-admin viewers
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -177,6 +177,7 @@ export interface RuntimeDisplayResponse {
|
||||
theme?: string;
|
||||
fullWidthMode?: boolean;
|
||||
disableDockerUpdateActions?: boolean;
|
||||
telemetryEnabled?: boolean;
|
||||
reduceProUpsellNoise?: boolean;
|
||||
}
|
||||
|
||||
|
||||
+23
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user