test(security): isolate windows acl user fallback

This commit is contained in:
Peter Steinberger
2026-04-29 12:36:24 +01:00
parent 1446069707
commit de0f54b54a
2 changed files with 24 additions and 33 deletions

View File

@@ -2,24 +2,8 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { WindowsAclEntry, WindowsAclSummary } from "./windows-acl.js";
const MOCK_USERNAME = "MockUser";
const userInfoMock = vi.hoisted(() =>
vi.fn(() => ({
username: MOCK_USERNAME,
uid: -1,
gid: -1,
shell: "C:\\Windows\\System32\\cmd.exe",
homedir: "C:\\Users\\MockUser",
})),
);
vi.mock("node:os", async () => {
const { mockNodeBuiltinModule } = await import("openclaw/plugin-sdk/test-node-mocks");
return mockNodeBuiltinModule(
() => vi.importActual<typeof import("node:os")>("node:os"),
{ userInfo: userInfoMock as unknown as typeof import("node:os").userInfo },
{ mirrorToDefault: true },
);
});
const mockUserInfo = () => ({ username: MOCK_USERNAME });
const emptyUserInfo = () => ({ username: "" });
let createIcaclsResetCommand: typeof import("./windows-acl.js").createIcaclsResetCommand;
let formatIcaclsResetCommand: typeof import("./windows-acl.js").formatIcaclsResetCommand;
@@ -125,7 +109,7 @@ describe("windows-acl", () => {
it("falls back to os.userInfo when USERNAME is empty", () => {
// When USERNAME env is empty, falls back to os.userInfo().username
const env = { USERNAME: "", USERDOMAIN: "WORKGROUP" };
const result = resolveWindowsUserPrincipal(env);
const result = resolveWindowsUserPrincipal(env, mockUserInfo);
// Should return a username (from os.userInfo fallback) with WORKGROUP domain
expect(result).toBe(`WORKGROUP\\${MOCK_USERNAME}`);
});
@@ -653,6 +637,7 @@ Successfully processed 1 files`;
const result = formatIcaclsResetCommand("C:\\test\\file.txt", {
isDir: false,
env: {},
userInfo: mockUserInfo,
});
// Should contain the actual system username from os.userInfo
expect(result).toContain(`"${MOCK_USERNAME}:F"`);
@@ -678,6 +663,7 @@ Successfully processed 1 files`;
const result = createIcaclsResetCommand("C:\\test\\file.txt", {
isDir: false,
env: {},
userInfo: mockUserInfo,
});
// Should return a valid command using the system username
expect(result).not.toBeNull();
@@ -717,17 +703,10 @@ Successfully processed 1 files`;
});
it("returns null when no username can be resolved (line 348)", () => {
// Temporarily make os.userInfo().username empty so resolveWindowsUserPrincipal returns null
userInfoMock.mockReturnValueOnce({
username: "",
uid: -1,
gid: -1,
shell: "",
homedir: "",
});
const result = createIcaclsResetCommand("C:\\test\\file.txt", {
isDir: false,
env: { USERNAME: "", USERDOMAIN: "" },
userInfo: emptyUserInfo,
});
expect(result).toBeNull();
});

View File

@@ -22,6 +22,14 @@ export type WindowsAclSummary = {
error?: string;
};
export type WindowsUserInfoProvider = () => { username?: string | null };
export type IcaclsResetCommandOptions = {
isDir: boolean;
env?: NodeJS.ProcessEnv;
userInfo?: WindowsUserInfoProvider;
};
const INHERIT_FLAGS = new Set(["I", "OI", "CI", "IO", "NP"]);
const WORLD_PRINCIPALS = new Set([
"everyone",
@@ -66,14 +74,18 @@ const STATUS_PREFIXES = [
];
const normalize = (value: string) => normalizeLowercaseStringOrEmpty(value);
const defaultWindowsUserInfo: WindowsUserInfoProvider = () => os.userInfo();
function normalizeSid(value: string): string {
const normalized = normalize(value);
return normalized.startsWith("*") ? normalized.slice(1) : normalized;
}
export function resolveWindowsUserPrincipal(env?: NodeJS.ProcessEnv): string | null {
const username = env?.USERNAME?.trim() || os.userInfo().username?.trim();
export function resolveWindowsUserPrincipal(
env?: NodeJS.ProcessEnv,
userInfo: WindowsUserInfoProvider = defaultWindowsUserInfo,
): string | null {
const username = env?.USERNAME?.trim() || userInfo().username?.trim();
if (!username) {
return null;
}
@@ -361,18 +373,18 @@ export function formatWindowsAclSummary(summary: WindowsAclSummary): string {
export function formatIcaclsResetCommand(
targetPath: string,
opts: { isDir: boolean; env?: NodeJS.ProcessEnv },
opts: IcaclsResetCommandOptions,
): string {
const user = resolveWindowsUserPrincipal(opts.env) ?? "%USERNAME%";
const user = resolveWindowsUserPrincipal(opts.env, opts.userInfo) ?? "%USERNAME%";
const grant = opts.isDir ? "(OI)(CI)F" : "F";
return `icacls "${targetPath}" /inheritance:r /grant:r "${user}:${grant}" /grant:r "*S-1-5-18:${grant}"`;
}
export function createIcaclsResetCommand(
targetPath: string,
opts: { isDir: boolean; env?: NodeJS.ProcessEnv },
opts: IcaclsResetCommandOptions,
): { command: string; args: string[]; display: string } | null {
const user = resolveWindowsUserPrincipal(opts.env);
const user = resolveWindowsUserPrincipal(opts.env, opts.userInfo);
if (!user) {
return null;
}