fix(auth): close CURRENT publication race
This commit is contained in:
@@ -24,6 +24,9 @@ import { createCurrentLocalUserRegistryResolver } from "../src/auth/local-regist
|
||||
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,
|
||||
}));
|
||||
@@ -48,7 +51,9 @@ vi.mock("node:fs", async (importOriginal) => {
|
||||
return result;
|
||||
},
|
||||
fstatSync(fd: number) {
|
||||
return observed(actual.fstatSync(fd));
|
||||
const result = observed(actual.fstatSync(fd));
|
||||
fsHook.fstatCallback?.(result);
|
||||
return result;
|
||||
},
|
||||
readdirSync(path: import("node:fs").PathLike) {
|
||||
if (fsHook.rejectPathReaddir)
|
||||
@@ -68,6 +73,7 @@ const roots: string[] = [];
|
||||
afterEach(() => {
|
||||
fsHook.path = undefined;
|
||||
fsHook.callback = undefined;
|
||||
fsHook.fstatCallback = undefined;
|
||||
fsHook.rejectPathReaddir = false;
|
||||
fsHook.foreignGid = undefined;
|
||||
for (const root of roots.splice(0))
|
||||
@@ -592,6 +598,56 @@ test("retries once when CURRENT is replaced after the final identity read", () =
|
||||
expect(observations).toBe(3);
|
||||
});
|
||||
|
||||
test("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);
|
||||
});
|
||||
|
||||
test("rejects a second CURRENT replacement after the one permitted retry", () => {
|
||||
const root = projectionRoot();
|
||||
const first = writeReadyProjection(
|
||||
|
||||
Reference in New Issue
Block a user