fix(auth): pin native auth storage operations
This commit is contained in:
+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;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user