wip: doctor/cli/docs/api to vector db consolidation; api hardening for descriptions, tenant, and scopes; migrations and conversions of all DALs to EF v10

This commit is contained in:
master
2026-02-23 15:30:50 +02:00
parent bd8fee6ed8
commit e746577380
1424 changed files with 81225 additions and 25251 deletions

View File

@@ -0,0 +1,270 @@
import type { Page } from '@playwright/test';
type StubAuthSession = {
subjectId: string;
tenant: string;
scopes: string[];
};
export type CapturedApiRequest = {
url: string;
tenantId: string | null;
};
const mockConfig = {
authority: {
issuer: 'http://127.0.0.1:4400/authority',
clientId: 'stella-ops-ui',
authorizeEndpoint: 'http://127.0.0.1:4400/authority/connect/authorize',
tokenEndpoint: 'http://127.0.0.1:4400/authority/connect/token',
logoutEndpoint: 'http://127.0.0.1:4400/authority/connect/logout',
redirectUri: 'http://127.0.0.1:4400/auth/callback',
postLogoutRedirectUri: 'http://127.0.0.1:4400/',
scope:
'openid profile email ui.read ui.admin authority:tenants.read authority:clients.read authority:clients.write findings:read orch:read orch:operate advisory:read vex:read exceptions:read exceptions:approve aoc:verify scanner:read policy:read policy:author policy:review policy:approve policy:simulate policy:audit release:read release:write release:publish sbom:read',
audience: 'http://127.0.0.1:4400/gateway',
dpopAlgorithms: ['ES256'],
refreshLeewaySeconds: 60,
},
apiBaseUrls: {
authority: '/authority',
scanner: '/scanner',
policy: '/policy',
concelier: '/concelier',
attestor: '/attestor',
gateway: '/gateway',
},
quickstartMode: true,
setup: 'complete',
};
const oidcConfig = {
issuer: mockConfig.authority.issuer,
authorization_endpoint: mockConfig.authority.authorizeEndpoint,
token_endpoint: mockConfig.authority.tokenEndpoint,
jwks_uri: 'http://127.0.0.1:4400/authority/.well-known/jwks.json',
response_types_supported: ['code'],
subject_types_supported: ['public'],
id_token_signing_alg_values_supported: ['RS256'],
};
const tenantCatalog = [
{
id: 'tenant-alpha',
displayName: 'Tenant Alpha',
status: 'active',
isolationMode: 'shared',
defaultRoles: ['admin'],
},
{
id: 'tenant-bravo',
displayName: 'Tenant Bravo',
status: 'active',
isolationMode: 'shared',
defaultRoles: ['admin'],
},
] as const;
export type MultiTenantFixture = {
capturedApiRequests: CapturedApiRequest[];
clearCapturedApiRequests: () => void;
};
export async function installMultiTenantSessionFixture(page: Page): Promise<MultiTenantFixture> {
const capturedApiRequests: CapturedApiRequest[] = [];
let selectedTenant = 'tenant-alpha';
const adminSession: StubAuthSession = {
subjectId: 'e2e-tenant-admin',
tenant: selectedTenant,
scopes: [
'ui.read',
'ui.admin',
'authority:tenants.read',
'authority:clients.read',
'authority:clients.write',
'findings:read',
'orch:read',
'orch:operate',
'advisory:read',
'vex:read',
'exceptions:read',
'exceptions:approve',
'aoc:verify',
'scanner:read',
'policy:read',
'policy:author',
'policy:review',
'policy:approve',
'policy:simulate',
'policy:audit',
'release:read',
'release:write',
'release:publish',
'sbom:read',
'admin',
],
};
await page.addInitScript((session: StubAuthSession) => {
let seededTenant = session.tenant;
try {
const raw = window.sessionStorage.getItem('stellaops.auth.session.full');
if (raw) {
const parsed = JSON.parse(raw) as { tenantId?: string | null };
if (typeof parsed.tenantId === 'string' && parsed.tenantId.trim().length > 0) {
seededTenant = parsed.tenantId.trim();
}
}
} catch {
// ignore malformed persisted session values
}
(window as { __stellaopsTestSession?: unknown }).__stellaopsTestSession = {
...session,
tenant: seededTenant,
};
}, adminSession);
await page.route('**/platform/envsettings.json', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockConfig),
}),
);
await page.route('**/config.json', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockConfig),
}),
);
await page.route('**/authority/.well-known/openid-configuration', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(oidcConfig),
}),
);
await page.route('**/.well-known/openid-configuration', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(oidcConfig),
}),
);
await page.route('**/authority/.well-known/jwks.json', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ keys: [] }),
}),
);
await page.route('**/authority/connect/**', (route) =>
route.fulfill({
status: 400,
contentType: 'application/json',
body: JSON.stringify({ error: 'not-used-in-e2e-fixture' }),
}),
);
await page.route('**/console/tenants**', (route) => {
const requestedTenant = resolveTenantFromRequestHeaders(route.request().headers()) ?? selectedTenant;
if (tenantCatalog.some((tenant) => tenant.id === requestedTenant)) {
selectedTenant = requestedTenant;
}
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
tenants: tenantCatalog,
selectedTenant,
}),
});
});
await page.route('**/console/profile**', (route) => {
const tenant = resolveTenantFromRequestHeaders(route.request().headers()) ?? selectedTenant;
selectedTenant = tenant;
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
subjectId: adminSession.subjectId,
username: 'tenant-admin',
displayName: 'Tenant Admin',
tenant,
roles: ['admin'],
scopes: adminSession.scopes,
audiences: ['stellaops'],
authenticationMethods: ['pwd'],
}),
});
});
await page.route('**/console/token/introspect**', (route) => {
const tenant = resolveTenantFromRequestHeaders(route.request().headers()) ?? selectedTenant;
selectedTenant = tenant;
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
active: true,
tenant,
subject: adminSession.subjectId,
clientId: 'stellaops-console',
scopes: adminSession.scopes,
audiences: ['stellaops'],
}),
});
});
await page.route('**/api/**', (route) => {
const tenantId = resolveTenantFromRequestHeaders(route.request().headers());
capturedApiRequests.push({
url: route.request().url(),
tenantId,
});
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
tenantId: tenantId ?? selectedTenant,
items: [],
}),
});
});
return {
capturedApiRequests,
clearCapturedApiRequests: () => {
capturedApiRequests.length = 0;
},
};
}
function resolveTenantFromRequestHeaders(headers: Record<string, string>): string | null {
const headerCandidates = [
headers['x-stellaops-tenant'],
headers['x-stella-tenant'],
headers['x-tenant-id'],
];
for (const candidate of headerCandidates) {
if (typeof candidate === 'string' && candidate.trim().length > 0) {
return candidate.trim();
}
}
return null;
}

View File

@@ -0,0 +1,48 @@
export type TenantPageMatrixEntry = {
section: string;
route: string;
expectedBreadcrumb: string;
};
export const tenantSwitchPageMatrix: readonly TenantPageMatrixEntry[] = [
{
section: 'Mission Control',
route: '/mission-control/board',
expectedBreadcrumb: 'Mission Board',
},
{
section: 'Releases',
route: '/releases/overview',
expectedBreadcrumb: 'Release Overview',
},
{
section: 'Security',
route: '/security/posture',
expectedBreadcrumb: 'Security',
},
{
section: 'Security',
route: '/security/unknowns',
expectedBreadcrumb: 'Unknowns',
},
{
section: 'Evidence',
route: '/evidence/overview',
expectedBreadcrumb: 'Evidence',
},
{
section: 'Ops',
route: '/ops/operations',
expectedBreadcrumb: 'Operations',
},
{
section: 'Setup',
route: '/setup/topology/overview',
expectedBreadcrumb: 'Topology',
},
{
section: 'Admin',
route: '/setup/identity-access',
expectedBreadcrumb: 'Identity & Access',
},
];

View File

@@ -0,0 +1,173 @@
import { expect, test, type Page } from '@playwright/test';
import { type CapturedApiRequest, installMultiTenantSessionFixture } from './support/multi-tenant-session.fixture';
import { tenantSwitchPageMatrix } from './support/tenant-switch-page-matrix';
test.describe.configure({ mode: 'serial' });
test.describe('Multi-tenant switch matrix', () => {
test('switches tenant from header and persists across primary sections (desktop)', async ({ page }) => {
const fixture = await installMultiTenantSessionFixture(page);
await page.setViewportSize({ width: 1440, height: 900 });
await go(page, '/mission-control/board');
await expectTenantLabelContains(page, 'alpha');
await switchTenant(page, 'Tenant Bravo');
await expectTenantLabelContains(page, 'bravo');
fixture.clearCapturedApiRequests();
for (const entry of tenantSwitchPageMatrix) {
await navigateInApp(page, entry.route);
await expect(page.locator('main')).toBeVisible();
await expectTenantLabelContains(page, 'bravo');
}
const tenantScopedRequests = fixture.capturedApiRequests.filter((request) => {
const url = request.url.toLowerCase();
return !url.includes('/api/auth/') &&
!url.includes('/health') &&
!url.includes('/ready') &&
!url.includes('/metrics');
});
expect(tenantScopedRequests.length).toBeGreaterThan(0);
for (const request of tenantScopedRequests) {
expect(request.tenantId).toBe('tenant-bravo');
}
});
test('keeps tenant selector usable and persistent on mobile viewport', async ({ page }) => {
await installMultiTenantSessionFixture(page);
await page.setViewportSize({ width: 390, height: 844 });
await go(page, '/mission-control/board');
await expect(page.locator('.topbar__tenant-btn')).toBeVisible();
await switchTenant(page, 'Tenant Bravo');
await expectTenantLabelContains(page, 'bravo');
await navigateInApp(page, '/setup/identity-access');
await expectTenantLabelContains(page, 'bravo');
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => null);
await expectTenantLabelContains(page, 'bravo');
});
for (const entry of tenantSwitchPageMatrix) {
test(`applies selected tenant on ${entry.section} route`, async ({ page }) => {
const fixture = await installMultiTenantSessionFixture(page);
await page.setViewportSize({ width: 1440, height: 900 });
await go(page, '/mission-control/board');
await switchTenant(page, 'Tenant Bravo');
await expectTenantLabelContains(page, 'bravo');
fixture.clearCapturedApiRequests();
await navigateInApp(page, entry.route);
await expect.poll(() => new URL(page.url()).pathname).toBe(entry.route);
const routeHints = buildRouteHints(entry.section, entry.expectedBreadcrumb);
await expect
.poll(async () => {
const mainText = (await page.locator('main').innerText()).toLowerCase();
return routeHints.some((hint) => mainText.includes(hint.toLowerCase()));
})
.toBe(true);
await expectTenantLabelContains(page, 'bravo');
const tenantScopedRequests = toTenantScopedRequests(fixture.capturedApiRequests);
const crossTenantRequests = tenantScopedRequests.filter((request) => request.tenantId && request.tenantId !== 'tenant-bravo');
expect(crossTenantRequests.length).toBe(0);
for (const request of tenantScopedRequests) {
expect(request.tenantId).toBe('tenant-bravo');
}
});
}
test('switches tenant in both directions without stale request headers', async ({ page }) => {
const fixture = await installMultiTenantSessionFixture(page);
await page.setViewportSize({ width: 1440, height: 900 });
await go(page, '/mission-control/board');
await switchTenant(page, 'Tenant Bravo');
await expectTenantLabelContains(page, 'bravo');
fixture.clearCapturedApiRequests();
await navigateInApp(page, '/setup/topology/overview');
const bravoRequests = toTenantScopedRequests(fixture.capturedApiRequests);
const crossTenantBravoRequests = bravoRequests.filter((request) => request.tenantId && request.tenantId !== 'tenant-bravo');
expect(crossTenantBravoRequests.length).toBe(0);
for (const request of bravoRequests) {
expect(request.tenantId).toBe('tenant-bravo');
}
await switchTenant(page, 'Tenant Alpha');
await expectTenantLabelContains(page, 'alpha');
fixture.clearCapturedApiRequests();
await navigateInApp(page, '/security/posture');
const alphaRequests = toTenantScopedRequests(fixture.capturedApiRequests);
const crossTenantAlphaRequests = alphaRequests.filter((request) => request.tenantId && request.tenantId !== 'tenant-alpha');
expect(crossTenantAlphaRequests.length).toBe(0);
for (const request of alphaRequests) {
expect(request.tenantId).toBe('tenant-alpha');
}
});
});
function toTenantScopedRequests(capturedApiRequests: readonly CapturedApiRequest[]): CapturedApiRequest[] {
return capturedApiRequests.filter((request) => {
const url = request.url.toLowerCase();
return !url.includes('/api/auth/') &&
!url.includes('/health') &&
!url.includes('/ready') &&
!url.includes('/metrics');
});
}
function buildRouteHints(section: string, expectedBreadcrumb: string): readonly string[] {
const hints = new Set<string>();
const sectionHint = section.trim();
const singularSectionHint = sectionHint.endsWith('s') ? sectionHint.slice(0, -1) : sectionHint;
hints.add(expectedBreadcrumb.trim());
hints.add(sectionHint);
hints.add(singularSectionHint);
return [...hints].filter((hint) => hint.length > 0);
}
async function switchTenant(page: Page, displayName: string): Promise<void> {
const trigger = page.locator('.topbar__tenant-btn');
await trigger.click();
const option = page.locator('.topbar__tenant-option-name', {
hasText: displayName,
});
await expect(option).toBeVisible();
await option.click();
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => null);
}
async function go(page: Page, path: string): Promise<void> {
await page.goto(path, { waitUntil: 'domcontentloaded' });
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => null);
}
async function navigateInApp(page: Page, path: string): Promise<void> {
await page.evaluate((nextPath) => {
window.history.pushState({}, '', nextPath);
window.dispatchEvent(new PopStateEvent('popstate'));
}, path);
await expect.poll(() => new URL(page.url()).pathname).toBe(path);
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => null);
}
async function expectTenantLabelContains(page: Page, tenantHint: string): Promise<void> {
await expect
.poll(async () => (await page.locator('.topbar__tenant-btn').innerText()).toLowerCase())
.toContain(tenantHint.toLowerCase());
}