Files
ThothII/backend/test/auth-runtime-projection.test.ts
T
marcopan 610ae8c85a fix(auth): make local verification portable
Keep upstream identity visible while limiting logout to local auth. Inject the restore privilege gate so the deterministic core tests do not depend on the host OS, and confine descriptor-backed projection tests to Linux. Accept the real remaining Pi timeout budget instead of an exact millisecond.
2026-08-25 10:50:30 +02:00

875 lines
27 KiB
TypeScript

import { createHash } from "node:crypto";
import {
chmodSync,
chownSync,
existsSync,
linkSync,
lstatSync,
mkdirSync,
mkdtempSync,
readFileSync,
renameSync,
rmSync,
symlinkSync,
unlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { stringify } from "yaml";
import { afterEach, expect, test, vi } from "vitest";
import { createProjectedAuthenticationConfigProvider } from "../src/auth/runtime-projection.js";
import { createCurrentLocalUserRegistryResolver } from "../src/auth/local-registry.js";
import { loadConfig } from "../src/config.js";
const fsHook = vi.hoisted(() => ({
path: undefined as string | undefined,
callback: undefined as (() => void) | undefined,
fstatCallback: undefined as
| ((value: import("node:fs").Stats) => void)
| undefined,
rejectPathReaddir: false,
foreignGid: undefined as number | undefined,
}));
vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs")>();
const observed = <T extends import("node:fs").Stats>(value: T): T =>
fsHook.foreignGid === undefined
? value
: new Proxy(value, {
get(target, property) {
if (property === "gid") return fsHook.foreignGid;
const member = Reflect.get(target, property, target);
return typeof member === "function" ? member.bind(target) : member;
},
});
return {
...actual,
lstatSync(path: import("node:fs").PathLike) {
const result = observed(actual.lstatSync(path));
if (fsHook.path === String(path)) fsHook.callback?.();
return result;
},
fstatSync(fd: number) {
const result = observed(actual.fstatSync(fd));
fsHook.fstatCallback?.(result);
return result;
},
readdirSync(path: import("node:fs").PathLike) {
if (fsHook.rejectPathReaddir)
throw new Error("path readdir is forbidden");
return actual.readdirSync(path);
},
};
});
const sentinel = "$argon2id$synthetic-sentinel";
const password = "correct horse battery staple";
const passwordHash =
"$argon2id$v=19$m=65536,t=3,p=1$AAECAwQFBgcICQoLDA0ODw$DRo8ZSPI8G5OCvnFFapbVEjP69aDjy1Sw9i2743cPC4";
const userId = "6ba7b810-9dad-4ed1-80b4-00c04fd430c8";
const roots: string[] = [];
const linuxTest = test.runIf(process.platform === "linux");
afterEach(() => {
fsHook.path = undefined;
fsHook.callback = undefined;
fsHook.fstatCallback = undefined;
fsHook.rejectPathReaddir = false;
fsHook.foreignGid = undefined;
for (const root of roots.splice(0))
rmSync(root, { recursive: true, force: true });
});
interface ProjectionFixture {
username: string;
hash?: string;
publicUrl?: string;
}
function sha256(value: string): string {
return createHash("sha256").update(value).digest("hex");
}
function generationFor(auth: string, users: string): string {
return sha256(
`thothii-auth-projection-v1\nmode=local\nauth=${sha256(auth)}\nusers=${sha256(users)}\n`,
);
}
function localProjectionFixture(
username: string,
hash = passwordHash,
): ProjectionFixture {
return { username, hash };
}
function projectionRoot(): string {
const root = mkdtempSync(join(tmpdir(), "tht-auth-projection-"));
chmodSync(root, 0o700);
roots.push(root);
return root;
}
function currentDocument(
generation: string,
previousGenerations: readonly string[] = [],
): string {
return `${JSON.stringify({
version: 1,
state: "ready",
transaction: "a".repeat(32),
generation,
...(previousGenerations.length === 0 ? {} : { previousGenerations }),
})}\n`;
}
function projectionSources(fixture: ProjectionFixture): {
auth: string;
users: string;
} {
return {
auth: stringify({
version: 1,
mode: "local",
publicUrl: fixture.publicUrl ?? "http://127.0.0.1:8080",
local: { usersFile: "users.yaml" },
}),
users: stringify({
version: 1,
users: [
{
id: userId,
username: fixture.username,
passwordHash: fixture.hash ?? passwordHash,
roles: ["admin"],
enabled: true,
authRevision: 1,
},
],
}),
};
}
function writeGeneration(root: string, fixture: ProjectionFixture): string {
const { auth, users } = projectionSources(fixture);
const generation = generationFor(auth, users);
const directory = join(root, "generations", generation);
mkdirSync(directory, { recursive: true, mode: 0o700 });
chmodSync(directory, 0o700);
const manifest = `${JSON.stringify({
version: 1,
generation,
mode: "local",
canonicalRevision: `sha256:${generation}`,
files: [
{
name: "auth.yaml",
size: Buffer.byteLength(auth),
sha256: sha256(auth),
},
{
name: "users.yaml",
size: Buffer.byteLength(users),
sha256: sha256(users),
},
],
})}\n`;
for (const [name, source] of [
["manifest.json", manifest],
["auth.yaml", auth],
["users.yaml", users],
] as const) {
writeFileSync(join(directory, name), source, {
encoding: "utf8",
mode: 0o600,
});
chmodSync(join(directory, name), 0o600);
}
return generation;
}
function writeReadyProjection(
root: string,
fixture: ProjectionFixture,
): string {
mkdirSync(join(root, "generations"), { mode: 0o700 });
chmodSync(join(root, "generations"), 0o700);
const generation = writeGeneration(root, fixture);
writeFileSync(join(root, "CURRENT"), currentDocument(generation), {
encoding: "utf8",
mode: 0o600,
});
chmodSync(join(root, "CURRENT"), 0o600);
return generation;
}
function writeReadyOidcProjection(root: string): string {
mkdirSync(join(root, "generations"), { mode: 0o700 });
chmodSync(join(root, "generations"), 0o700);
const auth = stringify({
version: 1,
mode: "oidc",
publicUrl: "https://thothii.example.org",
oidc: {
issuer: "https://authentik.example.org/application/o/thothii/",
clientId: "thothii",
clientSecretRef: "THT_OIDC_CLIENT_SECRET",
scopes: ["openid", "profile", "email"],
groupsClaim: "groups",
},
groupCatalog: {
driver: "authentik",
baseUrl: "https://authentik.example.org",
apiTokenRef: "THT_AUTHENTIK_API_TOKEN",
},
authorization: {
groupRoles: { "TOT Users": ["user"], "TOT Admin": ["admin"] },
},
});
const generation = sha256(
`thothii-auth-projection-v1\nmode=oidc\nauth=${sha256(auth)}\nusers=-\n`,
);
const directory = join(root, "generations", generation);
mkdirSync(directory, { mode: 0o700 });
chmodSync(directory, 0o700);
const manifest = `${JSON.stringify({
version: 1,
generation,
mode: "oidc",
canonicalRevision: `sha256:${generation}`,
files: [
{
name: "auth.yaml",
size: Buffer.byteLength(auth),
sha256: sha256(auth),
},
],
})}\n`;
writeFileSync(join(directory, "manifest.json"), manifest, {
encoding: "utf8",
mode: 0o600,
});
writeFileSync(join(directory, "auth.yaml"), auth, {
encoding: "utf8",
mode: 0o600,
});
chmodSync(join(directory, "manifest.json"), 0o600);
chmodSync(join(directory, "auth.yaml"), 0o600);
writeFileSync(join(root, "CURRENT"), currentDocument(generation), {
encoding: "utf8",
mode: 0o600,
});
chmodSync(join(root, "CURRENT"), 0o600);
return generation;
}
function expectDenied(operation: () => unknown): void {
try {
operation();
throw new Error("operation unexpectedly succeeded");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
expect(message).toBe("authentication runtime projection is invalid");
expect(message).not.toContain(sentinel);
expect(message).not.toContain(passwordHash);
}
}
linuxTest("loads ready projection as one immutable auth and local-users snapshot", () => {
const root = projectionRoot();
const fixture = localProjectionFixture("synthetic-user", passwordHash);
const generation = writeReadyProjection(root, fixture);
const loaded = createProjectedAuthenticationConfigProvider(root).current();
expect(loaded.revision).toBe(`sha256:${generation}`);
expect(loaded.sourcePath).toBe(
join(root, "generations", generation, "auth.yaml"),
);
expect(loaded.runtimeProjection?.generation).toBe(generation);
expect(loaded.runtimeProjection?.canonicalRevision).toBe(
`sha256:${generation}`,
);
expect(Object.isFrozen(loaded.runtimeProjection)).toBe(true);
expect(Object.isFrozen(loaded.runtimeProjection?.localUsers)).toBe(true);
expect(Object.isFrozen(loaded.runtimeProjection?.localUsers?.[0])).toBe(true);
expect(
Object.isFrozen(loaded.runtimeProjection?.localUsers?.[0]?.roles),
).toBe(true);
});
linuxTest("loads a complete OIDC projection without a users snapshot", () => {
const root = projectionRoot();
const generation = writeReadyOidcProjection(root);
const loaded = createProjectedAuthenticationConfigProvider(root).current();
expect(loaded.value.mode).toBe("oidc");
expect(loaded.runtimeProjection).toEqual({
generation,
canonicalRevision: `sha256:${generation}`,
});
});
linuxTest("loadConfig selects an immutable projected local provider and its in-memory registry", async () => {
const root = projectionRoot();
writeReadyProjection(root, localProjectionFixture("projected-user", passwordHash));
const config = loadConfig({
THT_AUTH_RUNTIME_PROJECTION_ROOT: root,
THT_AUTH_STATE_ROOT: "/state/auth",
});
const loaded = config.authentication?.current();
expect(loaded).toMatchObject({ value: { mode: "local" }, runtimeProjection: expect.any(Object) });
const registry = createCurrentLocalUserRegistryResolver().resolve(loaded!);
expect(await registry?.findByUsername("PROJECTED-USER")).toMatchObject({ username: "projected-user" });
});
linuxTest("loadConfig selects an immutable projected OIDC provider without direct-file fallback", () => {
const root = projectionRoot();
writeReadyOidcProjection(root);
const config = loadConfig({
THT_AUTH_RUNTIME_PROJECTION_ROOT: root,
THT_AUTH_CONFIG_FILE: "/run/thothii-auth/auth.yaml",
THT_AUTH_STATE_ROOT: "/state/auth",
});
expect(config.authentication?.current()).toMatchObject({
value: { mode: "oidc" },
runtimeProjection: expect.any(Object),
});
});
linuxTest("rejects a trailing-slash runtime root", () => {
const root = projectionRoot();
writeReadyProjection(
root,
localProjectionFixture("synthetic-user", passwordHash),
);
expectDenied(() =>
createProjectedAuthenticationConfigProvider(`${root}/`).current(),
);
});
linuxTest.each([
["missing", undefined],
[
"blocked",
`${JSON.stringify({ version: 1, state: "blocked", transaction: "a".repeat(32) })}\n`,
],
["malformed", "{not-json}\n"],
[
"duplicate-field",
`{"version":1,"version":1,"state":"ready","transaction":"${"a".repeat(32)}","generation":"${"b".repeat(64)}"}\n`,
],
[
"unknown-version",
`${JSON.stringify({ version: 2, state: "ready", transaction: "a".repeat(32), generation: "b".repeat(64) })}\n`,
],
])("rejects %s CURRENT without secret disclosure", (_label, contents) => {
const root = projectionRoot();
writeReadyProjection(
root,
localProjectionFixture("synthetic-user", sentinel),
);
if (contents === undefined) unlinkSync(join(root, "CURRENT"));
else
writeFileSync(join(root, "CURRENT"), contents, {
encoding: "utf8",
mode: 0o600,
});
expectDenied(() =>
createProjectedAuthenticationConfigProvider(root).current(),
);
});
linuxTest.each([
"root traversal",
"CURRENT symlink",
"CURRENT hardlink",
"permissive mode",
"unexpected root entry",
])("rejects %s without secret disclosure", (kind) => {
const root = projectionRoot();
writeReadyProjection(
root,
localProjectionFixture("synthetic-user", sentinel),
);
const current = join(root, "CURRENT");
if (kind === "root traversal") {
expectDenied(() =>
createProjectedAuthenticationConfigProvider(
`${root}/../${root.split("/").at(-1)!}`,
).current(),
);
return;
}
if (kind === "CURRENT symlink") {
renameSync(current, join(root, "current-target"));
symlinkSync(join(root, "current-target"), current);
} else if (kind === "CURRENT hardlink") {
linkSync(current, join(root, "current-link"));
} else if (kind === "permissive mode") {
chmodSync(current, 0o640);
} else {
writeFileSync(join(root, "unexpected"), "x", {
encoding: "utf8",
mode: 0o600,
});
}
expectDenied(() =>
createProjectedAuthenticationConfigProvider(root).current(),
);
});
test.runIf(process.geteuid?.() === 0)(
"rejects wrong owner at every projection boundary without secret disclosure",
() => {
const root = projectionRoot();
writeReadyProjection(
root,
localProjectionFixture("synthetic-user", sentinel),
);
// Root may safely construct a synthetic foreign-owned fixture; no real account is touched.
chownSync(join(root, "CURRENT"), 1, 1);
expectDenied(() =>
createProjectedAuthenticationConfigProvider(root).current(),
);
},
);
linuxTest("rejects a foreign group with the correct owner", () => {
const root = projectionRoot();
writeReadyProjection(
root,
localProjectionFixture("synthetic-user", passwordHash),
);
const gid = process.getegid?.() ?? 0;
fsHook.foreignGid = gid === 1 ? 2 : 1;
expectDenied(() =>
createProjectedAuthenticationConfigProvider(root).current(),
);
});
linuxTest("enumerates closed namespaces without path-based readdirSync", () => {
const root = projectionRoot();
const generation = writeReadyProjection(
root,
localProjectionFixture("synthetic-user", passwordHash),
);
fsHook.rejectPathReaddir = true;
expect(
createProjectedAuthenticationConfigProvider(root).current()
.runtimeProjection?.generation,
).toBe(generation);
});
linuxTest("rejects a symlinked runtime root", () => {
const root = projectionRoot();
writeReadyProjection(
root,
localProjectionFixture("synthetic-user", passwordHash),
);
const linkedRoot = `${root}-link`;
symlinkSync(root, linkedRoot, "dir");
roots.push(linkedRoot);
expectDenied(() =>
createProjectedAuthenticationConfigProvider(linkedRoot).current(),
);
});
linuxTest.each([
"root",
"generations",
"selected generation",
"manifest",
"auth",
"users",
])("rejects unsafe mode on %s", (boundary) => {
const root = projectionRoot();
const generation = writeReadyProjection(
root,
localProjectionFixture("synthetic-user", passwordHash),
);
const selected = join(root, "generations", generation);
const path =
boundary === "root"
? root
: boundary === "generations"
? join(root, "generations")
: boundary === "selected generation"
? selected
: join(
selected,
boundary === "manifest" ? "manifest.json" : `${boundary}.yaml`,
);
chmodSync(
path,
boundary === "root" ||
boundary === "generations" ||
boundary === "selected generation"
? 0o750
: 0o640,
);
expectDenied(() =>
createProjectedAuthenticationConfigProvider(root).current(),
);
});
linuxTest.each(["manifest", "generation", "size", "digest"])(
"rejects changed %s integrity data without secret disclosure",
(kind) => {
const root = projectionRoot();
const generation = writeReadyProjection(
root,
localProjectionFixture("synthetic-user", sentinel),
);
const manifestPath = join(root, "generations", generation, "manifest.json");
const manifest = JSON.parse(String(readFileSync(manifestPath))) as Record<
string,
any
>;
if (kind === "manifest") manifest.unexpected = true;
if (kind === "generation") manifest.generation = "b".repeat(64);
if (kind === "size") manifest.files[0].size += 1;
if (kind === "digest") manifest.files[1].sha256 = "b".repeat(64);
writeFileSync(manifestPath, `${JSON.stringify(manifest)}\n`, {
encoding: "utf8",
mode: 0o600,
});
chmodSync(manifestPath, 0o600);
expectDenied(() =>
createProjectedAuthenticationConfigProvider(root).current(),
);
},
);
linuxTest("switches atomically to a later complete generation", () => {
const root = projectionRoot();
const first = writeReadyProjection(
root,
localProjectionFixture("first", passwordHash),
);
const provider = createProjectedAuthenticationConfigProvider(root);
expect(provider.current().runtimeProjection?.generation).toBe(first);
const second = writeGeneration(
root,
localProjectionFixture("second", passwordHash),
);
const temporary = join(root, ".current-switch.tmp");
writeFileSync(temporary, currentDocument(second, [first]), {
encoding: "utf8",
mode: 0o600,
});
chmodSync(temporary, 0o600);
renameSync(temporary, join(root, "CURRENT"));
expect(provider.current().runtimeProjection?.generation).toBe(second);
});
linuxTest("retries once when CURRENT is atomically replaced between lstat and open", () => {
const root = projectionRoot();
const first = writeReadyProjection(
root,
localProjectionFixture("first", passwordHash),
);
const second = writeGeneration(
root,
localProjectionFixture("second", passwordHash),
);
const temporary = join(root, ".current-replacement.tmp");
fsHook.path = join(root, "CURRENT");
fsHook.callback = () => {
fsHook.callback = undefined;
writeFileSync(temporary, currentDocument(second, [first]), {
encoding: "utf8",
mode: 0o600,
});
chmodSync(temporary, 0o600);
renameSync(temporary, join(root, "CURRENT"));
};
expect(
createProjectedAuthenticationConfigProvider(root).current()
.runtimeProjection?.generation,
).toBe(second);
});
linuxTest("retries once when CURRENT is replaced after the final identity read", () => {
const root = projectionRoot();
const first = writeReadyProjection(
root,
localProjectionFixture("first", passwordHash),
);
const second = writeGeneration(
root,
localProjectionFixture("second", passwordHash),
);
const stagedParent = mkdtempSync(
join(tmpdir(), "tht-auth-projection-late-generation-"),
);
roots.push(stagedParent);
renameSync(join(root, "generations", second), join(stagedParent, second));
let observations = 0;
fsHook.path = join(root, "CURRENT");
fsHook.callback = () => {
observations += 1;
if (observations !== 3) return;
fsHook.callback = undefined;
renameSync(join(stagedParent, second), join(root, "generations", second));
const temporary = join(root, ".current-late-replacement.tmp");
writeFileSync(temporary, currentDocument(second, [first]), {
encoding: "utf8",
mode: 0o600,
});
chmodSync(temporary, 0o600);
renameSync(temporary, join(root, "CURRENT"));
};
expect(
createProjectedAuthenticationConfigProvider(root).current()
.runtimeProjection?.generation,
).toBe(second);
expect(observations).toBe(3);
});
linuxTest("retries once when CURRENT is replaced between root descriptor and path observations", () => {
const root = projectionRoot();
const first = writeReadyProjection(
root,
localProjectionFixture("first", passwordHash),
);
const second = writeGeneration(
root,
localProjectionFixture("second", passwordHash),
);
const stagedParent = mkdtempSync(
join(tmpdir(), "tht-auth-projection-root-observation-"),
);
roots.push(stagedParent);
renameSync(join(root, "generations", second), join(stagedParent, second));
const rootIdentity = lstatSync(root);
let armed = false;
let replacedCurrent = false;
fsHook.path = join(root, "generations", first, "users.yaml");
fsHook.callback = () => {
armed = true;
fsHook.callback = undefined;
};
fsHook.fstatCallback = (value) => {
if (
!armed ||
replacedCurrent ||
value.dev !== rootIdentity.dev ||
value.ino !== rootIdentity.ino
)
return;
replacedCurrent = true;
renameSync(join(stagedParent, second), join(root, "generations", second));
const temporary = join(root, ".current-root-observation.tmp");
writeFileSync(temporary, currentDocument(second, [first]), {
encoding: "utf8",
mode: 0o600,
});
chmodSync(temporary, 0o600);
renameSync(temporary, join(root, "CURRENT"));
};
expect(
createProjectedAuthenticationConfigProvider(root).current()
.runtimeProjection?.generation,
).toBe(second);
expect(replacedCurrent).toBe(true);
});
linuxTest("rejects a second CURRENT replacement after the one permitted retry", () => {
const root = projectionRoot();
const first = writeReadyProjection(
root,
localProjectionFixture("first", passwordHash),
);
const second = writeGeneration(
root,
localProjectionFixture("second", passwordHash),
);
const third = writeGeneration(
root,
localProjectionFixture("third", passwordHash),
);
const replacements = [
{ generation: second, previous: [first] },
{ generation: third, previous: [second, first] },
];
fsHook.path = join(root, "CURRENT");
fsHook.callback = () => {
const replacement = replacements.shift();
if (!replacement) return;
const temporary = join(
root,
`.current-replacement-${replacement.generation}.tmp`,
);
writeFileSync(
temporary,
currentDocument(replacement.generation, replacement.previous),
{ encoding: "utf8", mode: 0o600 },
);
chmodSync(temporary, 0o600);
renameSync(temporary, join(root, "CURRENT"));
};
expectDenied(() =>
createProjectedAuthenticationConfigProvider(root).current(),
);
});
linuxTest("fails deterministically when generations is replaced during a load", () => {
const root = projectionRoot();
const generation = writeReadyProjection(
root,
localProjectionFixture("synthetic-user", passwordHash),
);
const replacement = mkdtempSync(
join(tmpdir(), "tht-auth-projection-replacement-"),
);
roots.push(replacement);
chmodSync(replacement, 0o700);
mkdirSync(join(replacement, generation), { recursive: true, mode: 0o700 });
chmodSync(join(replacement, generation), 0o700);
for (const name of ["manifest.json", "auth.yaml", "users.yaml"]) {
const source = join(root, "generations", generation, name);
writeFileSync(join(replacement, generation, name), readFileSync(source), {
mode: 0o600,
});
chmodSync(join(replacement, generation, name), 0o600);
}
fsHook.path = join(root, "generations");
fsHook.callback = () => {
fsHook.callback = undefined;
renameSync(join(root, "generations"), join(root, "generations-retired"));
renameSync(replacement, join(root, "generations"));
};
expectDenied(() =>
createProjectedAuthenticationConfigProvider(root).current(),
);
});
linuxTest("fails deterministically when the selected generation directory is replaced during a load", () => {
const root = projectionRoot();
const generation = writeReadyProjection(
root,
localProjectionFixture("synthetic-user", passwordHash),
);
const replacement = mkdtempSync(
join(tmpdir(), "tht-auth-projection-generation-replacement-"),
);
roots.push(replacement);
chmodSync(replacement, 0o700);
for (const name of ["manifest.json", "auth.yaml", "users.yaml"]) {
const source = join(root, "generations", generation, name);
writeFileSync(join(replacement, name), readFileSync(source), {
mode: 0o600,
});
chmodSync(join(replacement, name), 0o600);
}
fsHook.path = join(root, "generations", generation);
fsHook.callback = () => {
fsHook.callback = undefined;
renameSync(
join(root, "generations", generation),
join(root, "generation-retired"),
);
renameSync(replacement, join(root, "generations", generation));
};
expectDenied(() =>
createProjectedAuthenticationConfigProvider(root).current(),
);
});
linuxTest.each(["corrupt", "symlink"])(
"rejects a %s retained predecessor generation",
(kind) => {
const root = projectionRoot();
const first = writeReadyProjection(
root,
localProjectionFixture("first", passwordHash),
);
const second = writeGeneration(
root,
localProjectionFixture("second", passwordHash),
);
writeFileSync(join(root, "CURRENT"), currentDocument(second, [first]), {
encoding: "utf8",
mode: 0o600,
});
chmodSync(join(root, "CURRENT"), 0o600);
const predecessor = join(root, "generations", first);
if (kind === "corrupt") {
writeFileSync(join(predecessor, "auth.yaml"), "tampered", {
encoding: "utf8",
mode: 0o600,
});
chmodSync(join(predecessor, "auth.yaml"), 0o600);
} else {
const replacement = mkdtempSync(
join(tmpdir(), "tht-auth-projection-history-"),
);
roots.push(replacement);
chmodSync(replacement, 0o700);
rmSync(predecessor, { recursive: true, force: true });
symlinkSync(replacement, predecessor, "dir");
}
expectDenied(() =>
createProjectedAuthenticationConfigProvider(root).current(),
);
},
);
linuxTest("has no direct-file fallback when CURRENT is absent", () => {
const root = projectionRoot();
const generation = writeReadyProjection(
root,
localProjectionFixture("synthetic-user", sentinel),
);
unlinkSync(join(root, "CURRENT"));
writeFileSync(
join(root, "auth.yaml"),
projectionSources(localProjectionFixture("synthetic-user", sentinel)).auth,
{ mode: 0o600 },
);
expect(existsSync(join(root, "generations", generation, "auth.yaml"))).toBe(
true,
);
expectDenied(() =>
createProjectedAuthenticationConfigProvider(root).current(),
);
});
linuxTest("in-flight snapshot authenticates A after selection B and deletion A, while a new load sees B", async () => {
const root = projectionRoot();
const first = writeReadyProjection(
root,
localProjectionFixture("first", passwordHash),
);
const provider = createProjectedAuthenticationConfigProvider(root);
const loadedA = provider.current();
const second = writeGeneration(
root,
localProjectionFixture("second", passwordHash),
);
const temporary = join(root, ".current-switch.tmp");
writeFileSync(temporary, currentDocument(second), {
encoding: "utf8",
mode: 0o600,
});
chmodSync(temporary, 0o600);
renameSync(temporary, join(root, "CURRENT"));
rmSync(join(root, "generations", first), { recursive: true, force: true });
const resolver = createCurrentLocalUserRegistryResolver();
const registryA = resolver.resolve(loadedA);
const userA = await registryA?.findByUsername("FIRST");
await expect(registryA?.verify(userA, password)).resolves.toBe(true);
const loadedB = provider.current();
const registryB = resolver.resolve(loadedB);
await expect(registryB?.findByUsername("second")).resolves.toMatchObject({
username: "second",
});
await expect(registryB?.findByUsername("first")).resolves.toBeUndefined();
});