fix(auth): pin native auth storage operations
This commit is contained in:
+4
-1
@@ -1,4 +1,5 @@
|
||||
import { createFileAuthSessionStore } from "../../src/auth/session-store.js";
|
||||
import { createFixturePosixAuthStorageBridge } from "./posix-auth-storage-bridge.mjs";
|
||||
|
||||
const root = process.env.THT_TEST_SESSION_ROOT;
|
||||
const state = process.env.THT_TEST_OIDC_STATE;
|
||||
@@ -9,7 +10,9 @@ process.send("ready");
|
||||
process.once("message", async (message) => {
|
||||
if (message !== "consume") process.exit(3);
|
||||
try {
|
||||
const record = await createFileAuthSessionStore(root).consumeOidcState(state);
|
||||
const record = await createFileAuthSessionStore(root, undefined, {
|
||||
posixStorageBridge: createFixturePosixAuthStorageBridge(),
|
||||
}).consumeOidcState(state);
|
||||
process.send?.({ consumed: record !== undefined });
|
||||
process.exit(0);
|
||||
} catch {
|
||||
|
||||
+5
-1
@@ -1,4 +1,5 @@
|
||||
import { createFileAuthSessionStore } from "../../src/auth/session-store.js";
|
||||
import { createFixturePosixAuthStorageBridge } from "./posix-auth-storage-bridge.mjs";
|
||||
|
||||
const root = process.env.THT_TEST_SESSION_ROOT;
|
||||
const attempts = Number(process.env.THT_TEST_OIDC_ATTEMPTS);
|
||||
@@ -7,7 +8,10 @@ const capacity = Number(process.env.THT_TEST_OIDC_CAPACITY);
|
||||
if (!root || !Number.isSafeInteger(attempts) || attempts < 1 || Number.isNaN(now.getTime())
|
||||
|| !Number.isSafeInteger(capacity) || capacity < 1) process.exit(2);
|
||||
|
||||
const store = createFileAuthSessionStore(root, undefined, { oidcStateCapacity: capacity });
|
||||
const store = createFileAuthSessionStore(root, undefined, {
|
||||
oidcStateCapacity: capacity,
|
||||
posixStorageBridge: createFixturePosixAuthStorageBridge(),
|
||||
});
|
||||
process.send?.("ready");
|
||||
process.once("message", async (message) => {
|
||||
if (message !== "create") process.exit(3);
|
||||
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
import {
|
||||
chmodSync,
|
||||
existsSync,
|
||||
linkSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
opendirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type {
|
||||
WindowsAuthStorageBridge,
|
||||
WindowsAuthStorageDirectory,
|
||||
} from "../../src/auth/windows-auth-storage.js";
|
||||
|
||||
// Test-process fixture only. The production POSIX implementation is the Go auth-storage bridge;
|
||||
// the Go package covers retained-descriptor adversarial races separately.
|
||||
export function createFixturePosixAuthStorageBridge(): WindowsAuthStorageBridge {
|
||||
const ensure = async (root: string): Promise<void> => {
|
||||
if (!existsSync(root)) {
|
||||
mkdirSync(root, { mode: 0o700 });
|
||||
chmodSync(root, 0o700);
|
||||
}
|
||||
for (const name of ["sessions", "oidc"]) {
|
||||
const path = join(root, name);
|
||||
if (!existsSync(path)) {
|
||||
mkdirSync(path, { mode: 0o700 });
|
||||
chmodSync(path, 0o700);
|
||||
}
|
||||
}
|
||||
};
|
||||
const path = async (root: string, directory: WindowsAuthStorageDirectory, filename: string): Promise<string> => {
|
||||
await ensure(root);
|
||||
return join(root, directory, filename);
|
||||
};
|
||||
const names = async (root: string, directory: WindowsAuthStorageDirectory): Promise<string[]> => {
|
||||
await ensure(root);
|
||||
const handle = opendirSync(join(root, directory));
|
||||
const result: string[] = [];
|
||||
try {
|
||||
for (;;) {
|
||||
const entry = handle.readSync();
|
||||
if (entry === null) break;
|
||||
result.push(entry.name);
|
||||
}
|
||||
} finally {
|
||||
handle.closeSync();
|
||||
}
|
||||
return result.sort();
|
||||
};
|
||||
const missing = (error: unknown): boolean => (error as { code?: unknown })?.code === "ENOENT";
|
||||
return {
|
||||
validateRoot: ensure,
|
||||
ensureLayout: ensure,
|
||||
readAuthConfig: (value) => readFileSync(value),
|
||||
readLocalUsers: async (value) => readFileSync(value),
|
||||
create: async (root, directory, filename, contents) => {
|
||||
try {
|
||||
const value = await path(root, directory, filename);
|
||||
writeFileSync(value, contents, { flag: "wx", mode: 0o600 });
|
||||
chmodSync(value, 0o600);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (missing(error) || (error as { code?: unknown })?.code === "EEXIST") return false;
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
read: async (root, directory, filename) => {
|
||||
try { return readFileSync(await path(root, directory, filename)); } catch (error) {
|
||||
if (missing(error)) return undefined;
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
replace: async (root, directory, filename, contents) => {
|
||||
const value = await path(root, directory, filename);
|
||||
const temporary = `${value}.fixture-replacement`;
|
||||
writeFileSync(temporary, contents, { flag: "wx", mode: 0o600 });
|
||||
renameSync(temporary, value);
|
||||
},
|
||||
remove: async (root, directory, filename) => {
|
||||
try {
|
||||
unlinkSync(await path(root, directory, filename));
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (missing(error)) return false;
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
list: async (root, directory, maximumEntries = 256) => {
|
||||
const result = await names(root, directory);
|
||||
if (result.length > maximumEntries) throw new Error("fixture list overflow");
|
||||
return result.map((name) => ({ name, modifiedUnixMs: lstatSync(join(root, directory, name)).mtimeMs }));
|
||||
},
|
||||
listPage: async (root, directory, afterName, maximumEntries) => {
|
||||
if (directory !== "sessions") throw new Error("fixture directory invalid");
|
||||
const selected = (await names(root, directory)).filter((name) => afterName === undefined || name > afterName);
|
||||
return {
|
||||
entries: selected.slice(0, maximumEntries).map((name) => ({
|
||||
name, modifiedUnixMs: lstatSync(join(root, directory, name)).mtimeMs,
|
||||
})),
|
||||
more: selected.length > maximumEntries,
|
||||
};
|
||||
},
|
||||
claimConsume: async (root, filename) => {
|
||||
const source = await path(root, "oidc", filename);
|
||||
const claim = source.replace(/\.json$/, ".claim");
|
||||
try {
|
||||
linkSync(source, claim);
|
||||
} catch (error) {
|
||||
if (missing(error) || (error as { code?: unknown })?.code === "EEXIST") return undefined;
|
||||
throw error;
|
||||
}
|
||||
const contents = readFileSync(source);
|
||||
unlinkSync(source);
|
||||
unlinkSync(claim);
|
||||
return contents;
|
||||
},
|
||||
readClaim: async (root, filename) => {
|
||||
const source = await path(root, "oidc", filename);
|
||||
const claim = source.replace(/\.json$/, ".claim");
|
||||
try {
|
||||
const left = lstatSync(source);
|
||||
const right = lstatSync(claim);
|
||||
if (left.ino !== right.ino || left.dev !== right.dev || left.nlink !== 2 || right.nlink !== 2) return undefined;
|
||||
return readFileSync(source);
|
||||
} catch (error) {
|
||||
if (missing(error)) return undefined;
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
removeClaim: async (root, filename) => {
|
||||
const source = await path(root, "oidc", filename);
|
||||
const claim = source.replace(/\.json$/, ".claim");
|
||||
try {
|
||||
const left = lstatSync(source);
|
||||
const right = lstatSync(claim);
|
||||
if (left.ino !== right.ino || left.dev !== right.dev || left.nlink !== 2 || right.nlink !== 2) return false;
|
||||
unlinkSync(source);
|
||||
unlinkSync(claim);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (missing(error)) return false;
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -6,7 +6,7 @@ writeFileSync(marker, `started:${process.pid}\n`);
|
||||
process.on("exit", () => appendFileSync(marker, "exited\n"));
|
||||
process.on("SIGTERM", () => {
|
||||
appendFileSync(marker, "terminated\n");
|
||||
process.exit(0);
|
||||
if (mode !== "timeout") process.exit(0);
|
||||
});
|
||||
|
||||
if (mode === "stdin") {
|
||||
|
||||
Reference in New Issue
Block a user