mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-01 02:20:24 +00:00
Security: split config-only fixer tests from permission path
This commit is contained in:
@@ -3,7 +3,8 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import { fixSecurityFootguns } from "./fix.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { applySecurityFixConfigMutations, fixSecurityFootguns } from "./fix.js";
|
||||
|
||||
const isWindows = process.platform === "win32";
|
||||
|
||||
@@ -95,35 +96,6 @@ describe("security fix", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const writeJsonConfig = async (configPath: string, config: Record<string, unknown>) => {
|
||||
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
||||
};
|
||||
|
||||
const writeWhatsAppConfig = async (configPath: string, whatsapp: Record<string, unknown>) => {
|
||||
await writeJsonConfig(configPath, {
|
||||
channels: {
|
||||
whatsapp,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const readParsedConfig = async (configPath: string) =>
|
||||
JSON.parse(await fs.readFile(configPath, "utf-8")) as Record<string, unknown>;
|
||||
|
||||
const runFixAndReadChannels = async (
|
||||
stateDir: string,
|
||||
configPath: string,
|
||||
channelPlugins?: ChannelPlugin[],
|
||||
) => {
|
||||
const env = createFixEnv(stateDir, configPath);
|
||||
const res = await fixSecurityFootguns({ env, stateDir, configPath, channelPlugins });
|
||||
const parsed = await readParsedConfig(configPath);
|
||||
return {
|
||||
res,
|
||||
channels: parsed.channels as Record<string, Record<string, unknown>>,
|
||||
};
|
||||
};
|
||||
|
||||
const expectTightenedStateAndConfigPerms = async (stateDir: string, configPath: string) => {
|
||||
const stateMode = (await fs.stat(stateDir)).mode & 0o777;
|
||||
expectPerms(stateMode, 0o700);
|
||||
@@ -132,29 +104,6 @@ describe("security fix", () => {
|
||||
expectPerms(configMode, 0o600);
|
||||
};
|
||||
|
||||
const runWhatsAppFixScenario = async (params: {
|
||||
stateDir: string;
|
||||
configPath: string;
|
||||
whatsapp: Record<string, unknown>;
|
||||
allowFromStore: string[];
|
||||
}) => {
|
||||
await writeWhatsAppConfig(params.configPath, params.whatsapp);
|
||||
await writeWhatsAppAllowFromStore(params.stateDir, params.allowFromStore);
|
||||
return runFixAndReadChannels(params.stateDir, params.configPath, [
|
||||
createWhatsAppConfigFixTestPlugin(params.allowFromStore),
|
||||
]);
|
||||
};
|
||||
|
||||
const writeWhatsAppAllowFromStore = async (stateDir: string, allowFrom: string[]) => {
|
||||
const credsDir = path.join(stateDir, "credentials");
|
||||
await fs.mkdir(credsDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(credsDir, "whatsapp-allowFrom.json"),
|
||||
`${JSON.stringify({ version: 1, allowFrom }, null, 2)}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
};
|
||||
|
||||
const expectWhatsAppGroupPolicy = (
|
||||
channels: Record<string, Record<string, unknown>>,
|
||||
expectedPolicy = "allowlist",
|
||||
@@ -173,20 +122,23 @@ describe("security fix", () => {
|
||||
return accounts;
|
||||
};
|
||||
|
||||
const fixWhatsAppScenario = async (params: {
|
||||
prefix: string;
|
||||
const fixWhatsAppConfigScenario = async (params: {
|
||||
whatsapp: Record<string, unknown>;
|
||||
allowFromStore: string[];
|
||||
}) => {
|
||||
const stateDir = await createStateDir(params.prefix);
|
||||
const configPath = path.join(stateDir, "openclaw.json");
|
||||
const result = await runWhatsAppFixScenario({
|
||||
stateDir,
|
||||
configPath,
|
||||
whatsapp: params.whatsapp,
|
||||
allowFromStore: params.allowFromStore,
|
||||
const fixed = await applySecurityFixConfigMutations({
|
||||
cfg: {
|
||||
channels: {
|
||||
whatsapp: params.whatsapp,
|
||||
},
|
||||
} satisfies OpenClawConfig,
|
||||
env: process.env,
|
||||
channelPlugins: [createWhatsAppConfigFixTestPlugin(params.allowFromStore)],
|
||||
});
|
||||
return { stateDir, configPath, ...result };
|
||||
return {
|
||||
res: { ok: true, changes: fixed.changes },
|
||||
channels: fixed.cfg.channels as Record<string, Record<string, unknown>>,
|
||||
};
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -200,11 +152,7 @@ describe("security fix", () => {
|
||||
});
|
||||
|
||||
it("tightens groupPolicy + filesystem perms", async () => {
|
||||
const stateDir = await createStateDir("tightens");
|
||||
await fs.chmod(stateDir, 0o755);
|
||||
|
||||
const configPath = path.join(stateDir, "openclaw.json");
|
||||
await writeJsonConfig(configPath, {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: { groupPolicy: "open" },
|
||||
whatsapp: { groupPolicy: "open" },
|
||||
@@ -213,21 +161,13 @@ describe("security fix", () => {
|
||||
imessage: { groupPolicy: "open" },
|
||||
},
|
||||
logging: { redactSensitive: "off" },
|
||||
});
|
||||
await fs.chmod(configPath, 0o644);
|
||||
|
||||
await writeWhatsAppAllowFromStore(stateDir, [" +15551234567 "]);
|
||||
const env = createFixEnv(stateDir, configPath);
|
||||
|
||||
const res = await fixSecurityFootguns({
|
||||
env,
|
||||
stateDir,
|
||||
configPath,
|
||||
} satisfies OpenClawConfig;
|
||||
const fixed = await applySecurityFixConfigMutations({
|
||||
cfg,
|
||||
env: process.env,
|
||||
channelPlugins: [createWhatsAppConfigFixTestPlugin(["+15551234567"])],
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.configWritten).toBe(true);
|
||||
expect(res.changes).toEqual(
|
||||
expect(fixed.changes).toEqual(
|
||||
expect.arrayContaining([
|
||||
"channels.telegram.groupPolicy=open -> allowlist",
|
||||
"channels.whatsapp.groupPolicy=open -> allowlist",
|
||||
@@ -238,10 +178,7 @@ describe("security fix", () => {
|
||||
]),
|
||||
);
|
||||
|
||||
await expectTightenedStateAndConfigPerms(stateDir, configPath);
|
||||
|
||||
const parsed = await readParsedConfig(configPath);
|
||||
const channels = parsed.channels as Record<string, Record<string, unknown>>;
|
||||
const channels = fixed.cfg.channels as Record<string, Record<string, unknown>>;
|
||||
expect(channels.telegram.groupPolicy).toBe("allowlist");
|
||||
expect(channels.whatsapp.groupPolicy).toBe("allowlist");
|
||||
expect(channels.discord.groupPolicy).toBe("allowlist");
|
||||
@@ -252,8 +189,7 @@ describe("security fix", () => {
|
||||
});
|
||||
|
||||
it("applies allowlist per-account and seeds WhatsApp groupAllowFrom from store", async () => {
|
||||
const { res, channels } = await fixWhatsAppScenario({
|
||||
prefix: "per-account",
|
||||
const { res, channels } = await fixWhatsAppConfigScenario({
|
||||
whatsapp: {
|
||||
accounts: {
|
||||
a1: { groupPolicy: "open" },
|
||||
@@ -267,8 +203,7 @@ describe("security fix", () => {
|
||||
});
|
||||
|
||||
it("does not seed WhatsApp groupAllowFrom if allowFrom is set", async () => {
|
||||
const { res, channels } = await fixWhatsAppScenario({
|
||||
prefix: "no-seed",
|
||||
const { res, channels } = await fixWhatsAppConfigScenario({
|
||||
whatsapp: {
|
||||
groupPolicy: "open",
|
||||
allowFrom: ["+15552223333"],
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { listBundledChannelPlugins } from "../channels/plugins/bundled.js";
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { createConfigIO } from "../config/config.js";
|
||||
@@ -245,6 +244,26 @@ function applyConfigFixes(params: { cfg: OpenClawConfig; env: NodeJS.ProcessEnv
|
||||
return { cfg: next, changes };
|
||||
}
|
||||
|
||||
export async function applySecurityFixConfigMutations(params: {
|
||||
cfg: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
channelPlugins?: ChannelPlugin[];
|
||||
}): Promise<{
|
||||
cfg: OpenClawConfig;
|
||||
changes: string[];
|
||||
}> {
|
||||
const fixed = applyConfigFixes({ cfg: params.cfg, env: params.env });
|
||||
const channelFixes = await collectChannelSecurityConfigFixMutation({
|
||||
cfg: fixed.cfg,
|
||||
env: params.env,
|
||||
channelPlugins: params.channelPlugins,
|
||||
});
|
||||
return {
|
||||
cfg: channelFixes.cfg,
|
||||
changes: [...fixed.changes, ...channelFixes.changes],
|
||||
};
|
||||
}
|
||||
|
||||
async function collectChannelSecurityConfigFixMutation(params: {
|
||||
cfg: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
@@ -252,7 +271,7 @@ async function collectChannelSecurityConfigFixMutation(params: {
|
||||
}) {
|
||||
let nextCfg = params.cfg;
|
||||
const changes: string[] = [];
|
||||
const collectPlugins = (): ChannelPlugin[] => {
|
||||
const collectPlugins = async (): Promise<ChannelPlugin[]> => {
|
||||
if (params.channelPlugins) {
|
||||
return params.channelPlugins;
|
||||
}
|
||||
@@ -262,13 +281,14 @@ async function collectChannelSecurityConfigFixMutation(params: {
|
||||
return [];
|
||||
}
|
||||
const wanted = new Set(pluginIds);
|
||||
const { listBundledChannelPlugins } = await import("../channels/plugins/bundled.js");
|
||||
return listBundledChannelPlugins().filter((plugin) => wanted.has(plugin.id));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
for (const plugin of collectPlugins()) {
|
||||
for (const plugin of await collectPlugins()) {
|
||||
const mutation = await plugin.security?.applyConfigFixes?.({
|
||||
cfg: nextCfg,
|
||||
env: params.env,
|
||||
@@ -382,17 +402,16 @@ export async function fixSecurityFootguns(opts?: {
|
||||
let configWritten = false;
|
||||
let changes: string[] = [];
|
||||
if (snap.valid) {
|
||||
const fixed = applyConfigFixes({ cfg: snap.config, env });
|
||||
const channelFixes = await collectChannelSecurityConfigFixMutation({
|
||||
cfg: fixed.cfg,
|
||||
const fixed = await applySecurityFixConfigMutations({
|
||||
cfg: snap.config,
|
||||
env,
|
||||
channelPlugins: opts?.channelPlugins,
|
||||
});
|
||||
changes = [...fixed.changes, ...channelFixes.changes];
|
||||
changes = fixed.changes;
|
||||
|
||||
if (changes.length > 0) {
|
||||
try {
|
||||
await io.writeConfigFile(channelFixes.cfg);
|
||||
await io.writeConfigFile(fixed.cfg);
|
||||
configWritten = true;
|
||||
} catch (err) {
|
||||
errors.push(`writeConfigFile failed: ${String(err)}`);
|
||||
|
||||
Reference in New Issue
Block a user