Fix authenticated platform bootstrap for proxy and SSO

This commit is contained in:
rcourtman
2026-07-23 00:18:21 +01:00
parent 61a4c232bc
commit fe1e8dec24
5 changed files with 169 additions and 26 deletions
@@ -251,6 +251,13 @@ avoids a cloud-control-plane report data path across clients.
describing relay transport mechanics alone.
2. `frontend-modern/src/components/Settings/MonitoredSystemImpactPreview.tsx` shared with `agent-lifecycle`: the monitored-system impact preview is both a platform-connections lifecycle surface and a canonical cloud-paid monitored-system presentation boundary.
3. `frontend-modern/src/useAppRuntimeState.ts` shared with `performance-and-scalability`: the authenticated app runtime bootstrap is both a hosted commercial org-context boundary and a protected app-shell performance boundary.
Every recognized authenticated browser session -- local, proxy-auth, or
SSO -- must enter the runtime through the same protected `/api/state`
bootstrap before organization hydration and WebSocket startup. Proxy-auth
and SSO security-status shortcuts may supply visible identity and logout
context, but they must not bypass that canonical server-owned resource
snapshot or leave platform navigation dependent on the first WebSocket
delivery.
The app runtime may use `ssoSessionDisplayName` from security status for
visible signed-in chrome, but hosted/commercial organization context must
remain bound to the stable authenticated principal carried separately in
@@ -158,6 +158,12 @@ admission records a stable refusal without invoking executor or network code.
or retained server-owned runtime state. Platform navigation may consume
that signal without adding a second state fetch, websocket subscription,
route preload, or browser-local cache read on the authenticated hot path.
Local, proxy-auth, and SSO sessions must share exactly one protected
`/api/state` hydration after authentication is established. That single
request must resolve the cold app shell before organization hydration and
WebSocket startup, so a delayed or blocked first stream snapshot cannot
hide server-owned platform scopes; auth-mode branches must not fork
additional state probes or perform the same hydration twice.
17. `internal/api/slo.go` shared with `api-contracts`: the SLO endpoint is both an API contract surface and a protected performance hot-path boundary.
Governed action decisions preserve SQLite and MemoryStore parity through one
@@ -466,7 +466,14 @@ describe('App architecture', () => {
'const connectionStatus = createMemo<AppConnectionStatus>(() => {',
);
expect(appRuntimeStateSource).toContain('const showOrgSwitcher = createMemo(() => {');
expect(appRuntimeStateSource).toContain('const beginAuthenticatedRuntime = async () =>');
expect(appRuntimeStateSource).toContain(
'const loadAuthenticatedBootstrapState = async (): Promise<boolean> => {',
);
expect(appRuntimeStateSource).toContain(
'const beginAuthenticatedRuntime = async (): Promise<boolean> => {',
);
expect(appRuntimeStateSource).toContain('if (!(await loadAuthenticatedBootstrapState())) {');
expect(appRuntimeStateSource.match(/apiFetch\('\/api\/state'/g)).toHaveLength(1);
expect(appRuntimeStateSource).toContain(
'const [backendHealthy, setBackendHealthy] = createSignal(false);',
);
@@ -41,6 +41,19 @@ const makeWebSocketState = (overrides: Partial<State> = {}): State => ({
...overrides,
});
const makeTrueNASResource = (id = 'truenas-1'): Resource => ({
id,
name: id,
displayName: id,
type: 'agent',
platformId: id,
platformType: 'truenas',
sourceType: 'api',
sources: ['truenas'],
status: 'online',
lastSeen: 1_700_000_000_000,
});
describe('useAppRuntimeState', () => {
let useAppRuntimeState: UseAppRuntimeStateModule['useAppRuntimeState'];
let apiFetchMock: ReturnType<typeof vi.fn>;
@@ -333,8 +346,9 @@ describe('useAppRuntimeState', () => {
dispose();
});
it('uses the SSO display name for app chrome without replacing the stable principal', async () => {
it('uses the SSO display name while bootstrapping TrueNAS state before websocket data', async () => {
const principal = 'sso:oidc:test-oidc:stable-principal';
const bootstrapResource = makeTrueNASResource('truenas-sso');
apiFetchMock.mockImplementation(async (url: string) => {
if (url === '/api/security/status') {
return new Response(
@@ -349,7 +363,13 @@ describe('useAppRuntimeState', () => {
);
}
if (url === '/api/state') {
return new Response('{}', { status: 200 });
return new Response(
JSON.stringify({
resources: [bootstrapResource],
lastUpdate: 1_700_000_000_000,
}),
{ status: 200 },
);
}
if (url === '/api/health') {
return new Response('{}', { status: 200 });
@@ -364,11 +384,94 @@ describe('useAppRuntimeState', () => {
username: 'alice@example.com',
logoutURL: '/api/oidc/test-oidc/logout',
});
expect(hookState.state().resources).toEqual([bootstrapResource]);
});
expect(hookState.securityStatus()?.ssoSessionUsername).toBe(principal);
expect(hookState.hasAuth()).toBe(true);
expect(hookState.needsAuth()).toBe(false);
expect(hookState.enhancedStore()?.initialDataReceived()).toBe(false);
expect(hookState.runtimeStateResolved()).toBe(true);
expect(apiFetchMock.mock.calls.filter(([url]) => url === '/api/state')).toHaveLength(1);
dispose();
});
it('bootstraps proxy-auth TrueNAS state before websocket data', async () => {
const bootstrapResource = makeTrueNASResource('truenas-proxy');
apiFetchMock.mockImplementation(async (url: string) => {
if (url === '/api/security/status') {
return new Response(
JSON.stringify({
hasAuthentication: true,
hasProxyAuth: true,
proxyAuthUsername: 'proxy-operator',
proxyAuthLogoutURL: '/proxy/logout',
}),
{ status: 200 },
);
}
if (url === '/api/state') {
return new Response(
JSON.stringify({
resources: [bootstrapResource],
lastUpdate: 1_700_000_000_000,
}),
{ status: 200 },
);
}
if (url === '/api/health') {
return new Response('{}', { status: 200 });
}
throw new Error(`Unhandled apiFetch URL: ${url}`);
});
const { hookState, dispose } = mountHook();
await waitFor(() => {
expect(hookState.state().resources).toEqual([bootstrapResource]);
});
expect(hookState.proxyAuthInfo()).toEqual({
username: 'proxy-operator',
logoutURL: '/proxy/logout',
});
expect(hookState.needsAuth()).toBe(false);
expect(hookState.enhancedStore()?.initialDataReceived()).toBe(false);
expect(hookState.runtimeStateResolved()).toBe(true);
expect(apiFetchMock.mock.calls.filter(([url]) => url === '/api/state')).toHaveLength(1);
dispose();
});
it('does not start the proxy-auth runtime when protected state rejects the session', async () => {
apiFetchMock.mockImplementation(async (url: string) => {
if (url === '/api/security/status') {
return new Response(
JSON.stringify({
hasAuthentication: true,
hasProxyAuth: true,
proxyAuthUsername: 'proxy-operator',
}),
{ status: 200 },
);
}
if (url === '/api/state') {
return new Response('{}', { status: 401 });
}
throw new Error(`Unhandled apiFetch URL: ${url}`);
});
const { hookState, dispose } = mountHook();
await waitFor(() => {
expect(hookState.isLoading()).toBe(false);
});
expect(hookState.needsAuth()).toBe(true);
expect(hookState.enhancedStore()).toBeNull();
expect(orgsListMock).not.toHaveBeenCalled();
expect(apiFetchMock.mock.calls.filter(([url]) => url === '/api/state')).toHaveLength(1);
dispose();
});
@@ -412,6 +515,7 @@ describe('useAppRuntimeState', () => {
});
expect(hookState.runtimeStateResolved()).toBe(true);
expect(hookState.enhancedStore()?.initialDataReceived()).toBe(false);
expect(apiFetchMock.mock.calls.filter(([url]) => url === '/api/state')).toHaveLength(1);
dispose();
});
@@ -447,6 +551,9 @@ describe('useAppRuntimeState', () => {
{ status: 200 },
);
}
if (url === '/api/state') {
return new Response('{}', { status: 200 });
}
if (url === '/api/health') {
return new Response('{}', { status: 200 });
}
@@ -463,7 +570,7 @@ describe('useAppRuntimeState', () => {
expect(hookState.enhancedStore()?.initialDataReceived()).toBe(false);
expect(hookState.runtimeStateResolved()).toBe(true);
expect(hookState.state().resources).toEqual([retainedResource]);
expect(apiFetchMock.mock.calls.some(([url]) => url === '/api/state')).toBe(false);
expect(apiFetchMock.mock.calls.filter(([url]) => url === '/api/state')).toHaveLength(1);
dispose();
});
+38 -22
View File
@@ -338,7 +338,34 @@ export const useAppRuntimeState = () => {
aiChatStore.setEnabled(securityData?.sessionCapabilities?.assistantEnabled === true);
};
const beginAuthenticatedRuntime = async () => {
const loadAuthenticatedBootstrapState = async (): Promise<boolean> => {
const stateResponse = await apiFetch('/api/state', {
headers: {
'X-Requested-With': 'XMLHttpRequest',
Accept: 'application/json',
},
});
if (stateResponse.status === 401) {
setBootstrapState(null);
setNeedsAuth(true);
return false;
}
const protectedState = await stateResponse
.clone()
.json()
.then(normalizeBootstrapState)
.catch(() => null);
setBootstrapState(protectedState);
return true;
};
const beginAuthenticatedRuntime = async (): Promise<boolean> => {
if (!(await loadAuthenticatedBootstrapState())) {
return false;
}
setNeedsAuth(false);
await loadOrganizations();
setWsStore(acquireWsStore());
@@ -348,6 +375,7 @@ export const useAppRuntimeState = () => {
if (!presentationPolicyHidesUpgradePrompts()) {
void loadCommercialPosture();
}
return true;
};
const checkBackendHealth = async () => {
@@ -685,7 +713,10 @@ export const useAppRuntimeState = () => {
username: securityData.proxyAuthUsername,
logoutURL: securityData.proxyAuthLogoutURL,
});
await beginAuthenticatedRuntime();
if (!(await beginAuthenticatedRuntime())) {
setIsLoading(false);
return;
}
void syncVersionInfoFromUpdateStore();
setIsLoading(false);
return;
@@ -700,7 +731,10 @@ export const useAppRuntimeState = () => {
username: ssoDisplayName,
logoutURL: securityData.ssoLogoutURL,
});
await beginAuthenticatedRuntime();
if (!(await beginAuthenticatedRuntime())) {
setIsLoading(false);
return;
}
void syncVersionInfoFromUpdateStore();
setIsLoading(false);
return;
@@ -725,25 +759,7 @@ export const useAppRuntimeState = () => {
return;
}
const stateResponse = await apiFetch('/api/state', {
headers: {
'X-Requested-With': 'XMLHttpRequest',
Accept: 'application/json',
},
});
if (stateResponse.status === 401) {
setBootstrapState(null);
setNeedsAuth(true);
} else {
const protectedState = await stateResponse
.clone()
.json()
.then(normalizeBootstrapState)
.catch(() => null);
setBootstrapState(protectedState);
await beginAuthenticatedRuntime();
}
await beginAuthenticatedRuntime();
} catch (error) {
logger.error('Auth check error', error);
try {