151 lines
5.3 KiB
TypeScript
151 lines
5.3 KiB
TypeScript
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;
|
|
}
|
|
},
|
|
};
|
|
}
|