Skip to content

Commit aac2bbc

Browse files
committed
fix: restrict system health UI to admins
1 parent 4fd3d1c commit aac2bbc

2 files changed

Lines changed: 47 additions & 17 deletions

File tree

‎tests/integration/specs/system-health.ui.spec.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,29 @@ test.describe('System Health operator view @ui @system', () => {
8181
await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible();
8282
});
8383
});
84+
85+
test.describe('System Health authorization @ui @system', () => {
86+
test('does not expose administrator health data to viewers', async ({ page }) => {
87+
await login(page, USERS.viewer);
88+
await page.route('**/api/system/info', (route) => route.fulfill({ json: systemInfo }));
89+
90+
let healthRequestCount = 0;
91+
page.on('request', (request) => {
92+
const path = new URL(request.url()).pathname;
93+
if (path === '/api/system/health' || path === '/api/system/health/incidents') {
94+
healthRequestCount += 1;
95+
}
96+
});
97+
98+
const roleLoaded = page.waitForResponse((response) =>
99+
new URL(response.url()).pathname === '/api/auth/verify' && response.status() === 200);
100+
await page.goto('/system.html', { waitUntil: 'domcontentloaded' });
101+
const session = await (await roleLoaded).json();
102+
103+
expect(session.role).toBe('viewer');
104+
await expect(page.getByTestId('system-tab')).toBeVisible();
105+
await expect(page.getByTestId('health-tab')).toHaveCount(0);
106+
await expect(page.getByTestId('system-health-view')).toHaveCount(0);
107+
expect(healthRequestCount).toBe(0);
108+
});
109+
});

‎web/js/components/preact/SystemView.jsx‎

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ export function SystemView() {
127127
const roleLoading = userRole === null;
128128
// Only admin can restart/shutdown system; default to false while loading
129129
const canControlSystem = !roleLoading && userRole === 'admin';
130+
// Operational health endpoints require the same system-admin permission.
131+
const canViewSystemHealth = canControlSystem;
130132

131133
// Define all query hooks next
132134
const {
@@ -263,22 +265,24 @@ export function SystemView() {
263265

264266
<div className="mb-4 border-b border-border" role="tablist" aria-label={t('system.sections')}>
265267
<div className="flex gap-2">
266-
<button
267-
type="button"
268-
id="health-tab"
269-
role="tab"
270-
data-testid="health-tab"
271-
aria-selected={activeTab === 'health'}
272-
aria-controls="health-panel"
273-
className={`rounded-t-lg px-4 py-2 text-sm font-medium transition-colors ${
274-
activeTab === 'health'
275-
? 'bg-card text-card-foreground border border-border border-b-0 -mb-px'
276-
: 'text-muted-foreground hover:text-foreground'
277-
}`}
278-
onClick={() => setActiveTab('health')}
279-
>
280-
{t('system.health.tab')}
281-
</button>
268+
{canViewSystemHealth && (
269+
<button
270+
type="button"
271+
id="health-tab"
272+
role="tab"
273+
data-testid="health-tab"
274+
aria-selected={activeTab === 'health'}
275+
aria-controls="health-panel"
276+
className={`rounded-t-lg px-4 py-2 text-sm font-medium transition-colors ${
277+
activeTab === 'health'
278+
? 'bg-card text-card-foreground border border-border border-b-0 -mb-px'
279+
: 'text-muted-foreground hover:text-foreground'
280+
}`}
281+
onClick={() => setActiveTab('health')}
282+
>
283+
{t('system.health.tab')}
284+
</button>
285+
)}
282286
<button
283287
type="button"
284288
id="system-tab"
@@ -314,7 +318,7 @@ export function SystemView() {
314318
</div>
315319
</div>
316320

317-
{activeTab === 'health' ? (
321+
{activeTab === 'health' && canViewSystemHealth ? (
318322
<div role="tabpanel" id="health-panel" aria-labelledby="health-tab">
319323
<SystemHealth />
320324
</div>

0 commit comments

Comments
 (0)