97 lines
4.3 KiB
TypeScript
97 lines
4.3 KiB
TypeScript
/** The fixed machine contract shared by the Authentik catalog and auth diagnostics. */
|
|
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: 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[]>;
|
|
}
|