fix(auth): close diagnostic filesystem races

This commit is contained in:
2026-08-17 13:04:15 +02:00
parent be1724890a
commit 6d438f4c7e
16 changed files with 545 additions and 90 deletions
+91 -4
View File
@@ -7,6 +7,7 @@ import { createAuthenticationConfigProvider } from "../src/auth/config.js";
import { createAuthentikGroupCatalog } from "../src/auth/authentik-group-catalog.js";
import { createLocalUserRegistry } from "../src/auth/local-registry.js";
import { createOidcProtocol, OidcJwksUnavailableError } from "../src/auth/oidc-client.js";
import { validateAuthSessionRoot } from "../src/auth/session-store.js";
import type { LoadedAuthConfig } from "../src/auth/types.js";
const sentinels = [
@@ -151,6 +152,7 @@ test("distinguishes a valid registry without an enabled admin from a malformed r
chmodSync(validUsers, 0o600);
const validReport = await createAuthDiagnoser({
authMode: "local", authStateRoot: validRoot,
sessionRootValidator: validateAuthSessionRoot,
authentication: { current: () => localConfig(join(validRoot, "auth.yaml")) },
localUserRegistry: createLocalUserRegistry(validUsers),
}).inspect({ live: false });
@@ -162,6 +164,7 @@ test("distinguishes a valid registry without an enabled admin from a malformed r
chmodSync(malformedUsers, 0o600);
const malformedReport = await createAuthDiagnoser({
authMode: "local", authStateRoot: malformedRoot,
sessionRootValidator: validateAuthSessionRoot,
authentication: { current: () => localConfig(join(malformedRoot, "auth.yaml")) },
localUserRegistry: createLocalUserRegistry(malformedUsers),
}).inspect({ live: false });
@@ -179,6 +182,7 @@ test("maps unsafe auth.yaml storage from the real provider to a redacted config
chmodSync(unsafePath, 0o640);
const report = await createAuthDiagnoser({
authMode: "local", authStateRoot: root,
sessionRootValidator: validateAuthSessionRoot,
authentication: createAuthenticationConfigProvider(unsafePath),
}).inspect({ live: false });
@@ -367,6 +371,42 @@ test.each([
id_token_signing_alg_values_supported: ["RS256"],
}],
["schema-invalid metadata", 200, { issuer: "https://different-issuer.example.test" }],
["an unsafe foreign issuer", 200, {
issuer: "http://different-issuer.example.test",
authorization_endpoint: "https://issuer.example.test/authorize",
token_endpoint: "https://issuer.example.test/token",
jwks_uri: "https://issuer.example.test/jwks",
response_types_supported: ["code"],
subject_types_supported: ["public"],
id_token_signing_alg_values_supported: ["RS256"],
}],
["an unsafe authorization endpoint", 200, {
issuer: "https://different-issuer.example.test",
authorization_endpoint: "http://127.0.0.1/authorize",
token_endpoint: "https://issuer.example.test/token",
jwks_uri: "https://issuer.example.test/jwks",
response_types_supported: ["code"],
subject_types_supported: ["public"],
id_token_signing_alg_values_supported: ["RS256"],
}],
["an unsafe token endpoint", 200, {
issuer: "https://different-issuer.example.test",
authorization_endpoint: "https://issuer.example.test/authorize",
token_endpoint: "https://operator:secret@issuer.example.test/token",
jwks_uri: "https://issuer.example.test/jwks",
response_types_supported: ["code"],
subject_types_supported: ["public"],
id_token_signing_alg_values_supported: ["RS256"],
}],
["an unsafe JWKS endpoint", 200, {
issuer: "https://different-issuer.example.test",
authorization_endpoint: "https://issuer.example.test/authorize",
token_endpoint: "https://issuer.example.test/token",
jwks_uri: "https://issuer.example.test/jwks#fragment",
response_types_supported: ["code"],
subject_types_supported: ["public"],
id_token_signing_alg_values_supported: ["RS256"],
}],
])("does not classify %s containing an issuer as an issuer mismatch", async (_label, status, body) => {
const loaded = oidcConfig();
const fetch = vi.fn<typeof globalThis.fetch>(async () => Response.json(body, { status }));
@@ -403,8 +443,13 @@ test("redacts exceptional configuration, registry, protocol, and catalog errors"
});
test.skipIf(process.platform === "win32")("uses the runtime validator for canonical, private session roots", async () => {
const dependencies = (authStateRoot: string) => ({
authMode: "none" as const,
authStateRoot,
sessionRootValidator: validateAuthSessionRoot,
});
const valid = privateRoot();
await expect(createAuthDiagnoser({ authMode: "none", authStateRoot: valid }).inspect({ live: false }))
await expect(createAuthDiagnoser(dependencies(valid)).inspect({ live: false }))
.resolves.toMatchObject({ ready: true, checks: [expect.objectContaining({ code: "auth_ready" })] });
const realRoot = join(privateRoot(), "real-auth");
@@ -419,22 +464,64 @@ test.skipIf(process.platform === "win32")("uses the runtime validator for canoni
writeFileSync(blockedParent, "blocked", { mode: 0o600 });
const traversal = `${valid}/../${basename(valid)}`;
const missingReport = await createAuthDiagnoser({ authMode: "none", authStateRoot: absent }).inspect({ live: false });
const missingReport = await createAuthDiagnoser(dependencies(absent)).inspect({ live: false });
expect(missingReport).toMatchObject({ ready: true, checks: [expect.objectContaining({ code: "auth_ready" })] });
expect(existsSync(absent)).toBe(false);
for (const unsafe of [traversal, linkedRoot, absentNested, join(blockedParent, "auth")]) {
const report = await createAuthDiagnoser({ authMode: "none", authStateRoot: unsafe }).inspect({ live: false });
const report = await createAuthDiagnoser(dependencies(unsafe)).inspect({ live: false });
expect(report).toMatchObject({ ready: false, checks: [expect.objectContaining({ code: "auth_session_store_invalid" })] });
expect(JSON.stringify(report)).not.toContain(unsafe);
}
expect(existsSync(absentParent)).toBe(false);
chmodSync(valid, 0o750);
await expect(createAuthDiagnoser({ authMode: "none", authStateRoot: valid }).inspect({ live: false }))
await expect(createAuthDiagnoser(dependencies(valid)).inspect({ live: false }))
.resolves.toMatchObject({ ready: false, checks: [expect.objectContaining({ code: "auth_session_store_invalid" })] });
});
test.skipIf(process.platform === "win32")("diagnoses unsafe existing session-store children without creating missing children", async () => {
const root = privateRoot();
const outside = privateRoot();
symlinkSync(outside, join(root, "sessions"));
const linked = await createAuthDiagnoser({
authMode: "none", authStateRoot: root, sessionRootValidator: validateAuthSessionRoot,
}).inspect({ live: false });
expect(linked).toMatchObject({
ready: false,
checks: [expect.objectContaining({ code: "auth_session_store_invalid" })],
});
expect(existsSync(join(root, "oidc"))).toBe(false);
expect(JSON.stringify(linked)).not.toContain(root);
rmSync(join(root, "sessions"));
mkdirSync(join(root, "sessions"), { mode: 0o700 });
chmodSync(join(root, "sessions"), 0o700);
mkdirSync(join(root, "oidc"), { mode: 0o700 });
chmodSync(join(root, "oidc"), 0o750);
const nonPrivate = await createAuthDiagnoser({
authMode: "none", authStateRoot: root, sessionRootValidator: validateAuthSessionRoot,
}).inspect({ live: false });
expect(nonPrivate).toMatchObject({
ready: false,
checks: [expect.objectContaining({ code: "auth_session_store_invalid" })],
});
});
test.skipIf(process.platform === "win32")("routes production POSIX static validation through the native auth-storage bridge", async () => {
const validateRoot = vi.fn(async () => undefined);
const report = await createAuthDiagnoser({
authMode: "none",
authStateRoot: "/var/lib/thothii/auth",
posixStorageBridge: { validateRoot },
}).inspect({ live: false });
expect(report).toMatchObject({ ready: true });
expect(validateRoot).toHaveBeenCalledOnce();
expect(validateRoot).toHaveBeenCalledWith("/var/lib/thothii/auth");
});
test("routes native Windows static session-root validation through the auth-storage bridge", async () => {
const originalPlatform = process.platform;
const validateRoot = vi.fn(async () => undefined);