mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-30 18:56:16 +00:00
Lean: update Semantics modules, add new numerics/physics data files Hardware: update FPGA bitstreams (tangnano9k_uart_loopback) Infra: k3s-flake tests, netcup-vps configuration, VCN compute substrate Docs: ARCHITECTURE, specs, citation updates
131 lines
4.9 KiB
TypeScript
131 lines
4.9 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
/**
|
|
* Auth integration tests.
|
|
*
|
|
* Validates that:
|
|
* 1. auth.researchstack.info serves the Authentik UI
|
|
* 2. SSO-protected paths are intercepted by Authentik forward_auth
|
|
* 3. /api/* paths do NOT go through forward_auth (token-auth only)
|
|
* 4. Authentik outpost forward_auth is wired correctly
|
|
*
|
|
* State awareness:
|
|
* - Authentik server is running (auth.researchstack.info responds)
|
|
* - Authentik embedded outpost may not have a configured application yet
|
|
* - When outpost has no matching app, forward_auth returns 404 with X-Authentik-Id
|
|
* - This is distinct from Traefik 404 (no matching Ingress rule)
|
|
*/
|
|
|
|
/** Returns true if the response came from Authentik (vs Traefik or backend) */
|
|
function isAuthentikResponse(headers: Record<string, string>): boolean {
|
|
return 'x-authentik-id' in headers || headers['x-powered-by'] === 'authentik';
|
|
}
|
|
|
|
test.describe('Authentik SSO at auth.researchstack.info', () => {
|
|
test('auth subdomain serves Authentik login page', async ({ request }) => {
|
|
const response = await request.get('https://auth.researchstack.info/', {
|
|
maxRedirects: 5,
|
|
});
|
|
expect(response.status()).toBeLessThan(500);
|
|
|
|
// Authentik login page should contain typical markers
|
|
const content = await response.text();
|
|
const isAuthentik =
|
|
content.includes('authentik') ||
|
|
content.includes('Authentik') ||
|
|
content.includes('ak-flow') ||
|
|
content.includes('Sign in') ||
|
|
content.includes('flow/default');
|
|
expect(isAuthentik).toBe(true);
|
|
});
|
|
|
|
test('auth subdomain OIDC discovery endpoint exists', async ({ request }) => {
|
|
const response = await request.get(
|
|
'https://auth.researchstack.info/application/o/.well-known/openid-configuration'
|
|
);
|
|
// Might be 404 if no application configured, or 200 with OIDC metadata
|
|
if (response.status() === 200) {
|
|
const body = await response.json();
|
|
expect(body).toHaveProperty('issuer');
|
|
expect(body.issuer).toContain('auth.researchstack.info');
|
|
} else {
|
|
// At minimum, should not be a redirect or 500
|
|
expect(response.status()).toBeLessThan(500);
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('forward_auth intercepts SSO-protected paths', () => {
|
|
const protectedPaths = [
|
|
'/apps/chat/',
|
|
'/apps/budget/',
|
|
'/server/status/',
|
|
'/server/dash/',
|
|
'/server/vault/',
|
|
'/',
|
|
];
|
|
|
|
for (const path of protectedPaths) {
|
|
test(`${path} is intercepted by Authentik forward_auth`, async ({ request }) => {
|
|
const response = await request.get(path, {
|
|
maxRedirects: 0,
|
|
});
|
|
const headers = response.headers();
|
|
|
|
// The key assertion: Authentik forward_auth middleware IS in the path.
|
|
// Evidence of this:
|
|
// - 302/303 redirect to auth.researchstack.info (outpost configured, redirecting to login)
|
|
// - 404 with X-Authentik-Id header (outpost reached, but no matching app configured yet)
|
|
// - 401/403 (auth denial from outpost)
|
|
// - 200 (user is already authenticated or auth not enforced)
|
|
if (response.status() === 302 || response.status() === 303) {
|
|
const location = headers['location'];
|
|
expect(location).toContain('auth.researchstack.info');
|
|
} else if (response.status() === 404) {
|
|
// If 404, it should be from Authentik (outpost not configured), not from Traefik
|
|
expect(isAuthentikResponse(headers)).toBe(true);
|
|
} else {
|
|
// 200 (no auth enforced) or 401/403 (denied)
|
|
expect([200, 401, 403]).toContain(response.status());
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
test.describe('/api/* paths bypass forward_auth', () => {
|
|
const apiPaths = [
|
|
'/api/cred/',
|
|
'/api/registry/health',
|
|
'/api/jobs/health',
|
|
'/api/blobs/health',
|
|
];
|
|
|
|
for (const path of apiPaths) {
|
|
test(`${path} does NOT redirect to Authentik`, async ({ request }) => {
|
|
const response = await request.get(path, {
|
|
maxRedirects: 0,
|
|
});
|
|
// API paths should NEVER redirect to Authentik
|
|
if (response.status() === 302 || response.status() === 303) {
|
|
const location = response.headers()['location'] || '';
|
|
expect(location).not.toContain('auth.researchstack.info');
|
|
}
|
|
// Valid responses: 200 (service up), 401/403 (token auth), 502/503 (not deployed)
|
|
expect([200, 401, 403, 404, 502, 503]).toContain(response.status());
|
|
});
|
|
}
|
|
});
|
|
|
|
test.describe('X-Forwarded headers propagation', () => {
|
|
test('edge correctly proxies to internal services over tailnet', async ({ request }) => {
|
|
// The /api/registry/health endpoint (if deployed) should respond
|
|
// This validates the full edge → traefik → service path works
|
|
const response = await request.get('/api/registry/health');
|
|
if (response.status() === 200) {
|
|
const body = await response.json();
|
|
expect(body.status).toBe('ok');
|
|
}
|
|
// If not deployed, just verify we don't get a TLS error
|
|
expect(response.status()).toBeLessThan(600);
|
|
});
|
|
});
|