fix(auth): harden unified diagnostic execution
This commit is contained in:
@@ -30,6 +30,66 @@ export interface AuthDiagnostics {
|
||||
checks: readonly AuthDiagnostic[];
|
||||
}
|
||||
|
||||
const diagnosticCodes = 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",
|
||||
]);
|
||||
const diagnosticModes = new Set<AuthDiagnostics["mode"]>(["local", "oidc", "upstream", "none", "mock"]);
|
||||
const fieldCodes = new Set<AuthDiagnosticCode>(["oidc_mapped_group_missing", "oidc_mapped_group_ambiguous"]);
|
||||
|
||||
function exactObject(value: unknown, keys: readonly string[]): Record<string, unknown> | undefined {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
||||
const source = value as Record<string, unknown>;
|
||||
const actual = Object.keys(source);
|
||||
return actual.length === keys.length && actual.every((key) => keys.includes(key)) ? source : undefined;
|
||||
}
|
||||
|
||||
function safeText(value: unknown): value is string {
|
||||
return typeof value === "string" && value.length > 0 && value.length <= 512
|
||||
&& value.trim() === value && !/\p{Cc}/u.test(value);
|
||||
}
|
||||
|
||||
/** Strict decoder for the machine contract shared with tht and the frontend. */
|
||||
export function decodeAuthDiagnostics(value: unknown): AuthDiagnostics | undefined {
|
||||
const source = exactObject(value, ["ready", "mode", "checks"]);
|
||||
if (!source || typeof source.ready !== "boolean" || typeof source.mode !== "string"
|
||||
|| !diagnosticModes.has(source.mode as AuthDiagnostics["mode"])
|
||||
|| !Array.isArray(source.checks) || source.checks.length === 0 || source.checks.length > 129) return undefined;
|
||||
const seen = new Set<string>();
|
||||
const checks: AuthDiagnostic[] = [];
|
||||
for (const value of source.checks) {
|
||||
const raw = value && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: undefined;
|
||||
const check = raw && exactObject(raw, raw.field === undefined
|
||||
? ["level", "code", "message"]
|
||||
: ["level", "code", "message", "field"]);
|
||||
if (!check || (check.level !== "error" && check.level !== "info")
|
||||
|| typeof check.code !== "string" || !diagnosticCodes.has(check.code as AuthDiagnosticCode)
|
||||
|| !safeText(check.message) || (check.field !== undefined && !safeText(check.field))) return undefined;
|
||||
const code = check.code as AuthDiagnosticCode;
|
||||
if (check.field !== undefined && !fieldCodes.has(code)) return undefined;
|
||||
const key = `${code}\u0000${check.field ?? ""}`;
|
||||
if (seen.has(key)) return undefined;
|
||||
seen.add(key);
|
||||
checks.push({
|
||||
level: check.level,
|
||||
code,
|
||||
message: check.message,
|
||||
...(check.field === undefined ? {} : { field: check.field }),
|
||||
});
|
||||
}
|
||||
if (source.ready) {
|
||||
if (checks.length !== 1 || checks[0].level !== "info" || checks[0].code !== "auth_ready"
|
||||
|| checks[0].field !== undefined) return undefined;
|
||||
} else if (!checks.some(({ level }) => level === "error")
|
||||
|| checks.some(({ code }) => code === "auth_ready")) return undefined;
|
||||
return { ready: source.ready, mode: source.mode as AuthDiagnostics["mode"], checks };
|
||||
}
|
||||
|
||||
/** A provider-specific proof that only the configured authorization groups exist. */
|
||||
export interface GroupCatalog {
|
||||
verifyConfiguredGroups(names: readonly string[], signal: AbortSignal): Promise<readonly AuthDiagnostic[]>;
|
||||
|
||||
Reference in New Issue
Block a user