feat(auth): include authentication in workspace and tht diagnostics

This commit is contained in:
2026-08-17 15:51:16 +02:00
parent cb0e873ed7
commit 3ed00ff086
19 changed files with 1050 additions and 34 deletions
+38
View File
@@ -8,6 +8,8 @@ import {
getWorkspaceRuntimeConfiguration,
listWorkspaces,
saveWorkspaceSecrets,
testWorkspace,
validateWorkspace,
} from "./workspaces";
const workspace = canonicalWorkspaceFixture("psd-clinical");
@@ -98,3 +100,39 @@ test("forget targets one declared requirement", async () => {
.resolves.toEqual(runtimeConfiguration);
expect(called).toBe(true);
});
test("decodes the shared authentication diagnostics on static validation and live connection tests", async () => {
const authentication = {
ready: false,
mode: "oidc" as const,
checks: [{
level: "error" as const,
code: "oidc_mapped_group_missing" as const,
field: "Thoth Administrators",
message: "A configured authorization group does not exist.",
}],
};
server.use(
http.post("/api/workspaces/validate", () => HttpResponse.json({
workspace,
contract: {},
activatable: false,
diagnostics: [],
authentication,
})),
http.post("/api/workspaces/psd-clinical/test", () => HttpResponse.json({
activatable: false,
diagnostics: [],
authentication,
})),
);
await expect(validateWorkspace(workspace)).resolves.toMatchObject({
activatable: false,
authentication,
});
await expect(testWorkspace("psd-clinical")).resolves.toMatchObject({
activatable: false,
authentication,
});
});
+101 -4
View File
@@ -138,9 +138,46 @@ export interface WorkspaceDiagnostic {
message: string;
}
export type AuthDiagnosticCode =
| "auth_ready"
| "auth_config_incomplete"
| "auth_config_invalid"
| "auth_session_store_invalid"
| "local_user_registry_invalid"
| "local_admin_missing"
| "oidc_secret_missing"
| "oidc_discovery_unreachable"
| "oidc_issuer_mismatch"
| "oidc_jwks_unreachable"
| "oidc_group_catalog_unreachable"
| "oidc_group_catalog_unauthorized"
| "oidc_mapped_group_missing"
| "oidc_mapped_group_ambiguous"
| "oidc_groups_claim_invalid"
| "oidc_device_flow_unavailable";
export interface AuthDiagnostic {
level: "error" | "info";
code: AuthDiagnosticCode;
message: string;
field?: string;
}
export interface AuthDiagnostics {
ready: boolean;
mode: "local" | "oidc" | "upstream" | "none" | "mock";
checks: AuthDiagnostic[];
}
export interface WorkspaceDiagnostics {
activatable: boolean;
diagnostics: WorkspaceDiagnostic[];
authentication: AuthDiagnostics;
}
export interface WorkspaceValidationResult extends WorkspaceDiagnostics {
workspace: CanonicalWorkspace;
contract: unknown;
}
export interface WorkspaceSecretRequirement {
@@ -179,6 +216,62 @@ function object(value: unknown): Record<string, unknown> | undefined {
: undefined;
}
const authDiagnosticCodes = new Set<AuthDiagnosticCode>([
"auth_ready", "auth_config_incomplete", "auth_config_invalid", "auth_session_store_invalid",
"local_user_registry_invalid", "local_admin_missing", "oidc_secret_missing",
"oidc_discovery_unreachable", "oidc_issuer_mismatch", "oidc_jwks_unreachable",
"oidc_group_catalog_unreachable", "oidc_group_catalog_unauthorized", "oidc_mapped_group_missing",
"oidc_mapped_group_ambiguous", "oidc_groups_claim_invalid", "oidc_device_flow_unavailable",
]);
function text(value: unknown): value is string {
return typeof value === "string" && value.length > 0 && value.length <= 512 && !/[\u0000-\u001f\u007f]/.test(value);
}
function decodeAuthentication(value: unknown): AuthDiagnostics {
const source = object(value);
if (!source || typeof source.ready !== "boolean"
|| !["local", "oidc", "upstream", "none", "mock"].includes(String(source.mode))
|| !Array.isArray(source.checks)) throw new Error("Workspace API returned invalid authentication diagnostics");
const checks = source.checks.map((item): AuthDiagnostic => {
const check = object(item);
if (!check || (check.level !== "error" && check.level !== "info")
|| typeof check.code !== "string" || !authDiagnosticCodes.has(check.code as AuthDiagnosticCode)
|| !text(check.message) || (check.field !== undefined && !text(check.field))) {
throw new Error("Workspace API returned invalid authentication diagnostics");
}
return {
level: check.level,
code: check.code as AuthDiagnosticCode,
message: check.message,
...(check.field === undefined ? {} : { field: check.field }),
};
});
return { ready: source.ready, mode: source.mode as AuthDiagnostics["mode"], checks };
}
function decodeWorkspaceDiagnostics(value: unknown): WorkspaceDiagnostics {
const source = object(value);
if (!source || typeof source.activatable !== "boolean" || !Array.isArray(source.diagnostics)) {
throw new Error("Workspace API returned an invalid diagnostic result");
}
const diagnostics = source.diagnostics.map((item): WorkspaceDiagnostic => {
const diagnostic = object(item);
if (!diagnostic || (diagnostic.level !== "error" && diagnostic.level !== "warning" && diagnostic.level !== "info")
|| typeof diagnostic.code !== "string" || !text(diagnostic.message)
|| (diagnostic.field !== undefined && !text(diagnostic.field))) {
throw new Error("Workspace API returned an invalid diagnostic result");
}
return {
level: diagnostic.level,
code: diagnostic.code as WorkspaceDiagnostic["code"],
message: diagnostic.message,
...(diagnostic.field === undefined ? {} : { field: diagnostic.field }),
};
});
return { activatable: source.activatable, diagnostics, authentication: decodeAuthentication(source.authentication) };
}
function exactObject(value: unknown, keys: readonly string[]): Record<string, unknown> | undefined {
const source = object(value);
return source && Object.keys(source).every((key) => keys.includes(key)) ? source : undefined;
@@ -278,18 +371,22 @@ export const getWorkspace = async (id: string): Promise<WorkspaceRecord> => {
export const getWorkspaceRegistryStatus = () => apiFetch<WorkspaceRegistryStatus>("/workspace-registry/status");
export const pullWorkspaceRegistry = () => apiFetch<WorkspaceRegistryStatus>("/workspace-registry/pull", { method: "POST" });
export const validateWorkspace = async (workspace: CanonicalWorkspace) => {
export const validateWorkspace = async (workspace: CanonicalWorkspace): Promise<WorkspaceValidationResult> => {
const safe = requireCanonicalWorkspace(workspace);
const response = await apiFetch<unknown>("/workspaces/validate", {
method: "POST", body: JSON.stringify({ workspace: safe }),
});
const source = object(response);
if (!source) throw new Error("Workspace API returned an invalid validation result");
return { workspace: requireCanonicalWorkspace(source.workspace), contract: source.contract };
return {
workspace: requireCanonicalWorkspace(source.workspace),
contract: source.contract,
...decodeWorkspaceDiagnostics(source),
};
};
export const testWorkspace = (id: string) =>
apiFetch<WorkspaceDiagnostics>(`/workspaces/${encodeURIComponent(id)}/test`, { method: "POST" });
export const testWorkspace = async (id: string): Promise<WorkspaceDiagnostics> =>
decodeWorkspaceDiagnostics(await apiFetch<unknown>(`/workspaces/${encodeURIComponent(id)}/test`, { method: "POST" }));
function runtimeConfiguration(value: unknown, expectedId: string): WorkspaceRuntimeConfiguration {
const source = exactObject(value, [