mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 23:34:34 +00:00
Auto-reply: scope allowlist store writes by account (#39015)
* Auto-reply: scope allowlist store writes * Tests: cover allowlist store account scoping * Changelog: note allowlist store scoping hardening
This commit is contained in:
@@ -240,6 +240,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Config/compaction safeguard settings: regression-test `agents.defaults.compaction.recentTurnsPreserve` through `loadConfig()` and cover the new help metadata entry so the exposed preserve knob stays wired through schema validation and config UX. (#25557) thanks @rodrigouroz.
|
- Config/compaction safeguard settings: regression-test `agents.defaults.compaction.recentTurnsPreserve` through `loadConfig()` and cover the new help metadata entry so the exposed preserve knob stays wired through schema validation and config UX. (#25557) thanks @rodrigouroz.
|
||||||
- iOS/Quick Setup presentation: skip automatic Quick Setup when a gateway is already configured (active connect config, last-known connection, preferred gateway, or manual host), so reconnecting installs no longer get prompted to connect again. (#38964) Thanks @ngutman.
|
- iOS/Quick Setup presentation: skip automatic Quick Setup when a gateway is already configured (active connect config, last-known connection, preferred gateway, or manual host), so reconnecting installs no longer get prompted to connect again. (#38964) Thanks @ngutman.
|
||||||
- CLI/Docs memory help accuracy: clarify `openclaw memory status --deep` behavior and align memory command examples/docs with the current search options. (#31803) Thanks @JasonOA888 and @Avi974.
|
- CLI/Docs memory help accuracy: clarify `openclaw memory status --deep` behavior and align memory command examples/docs with the current search options. (#31803) Thanks @JasonOA888 and @Avi974.
|
||||||
|
- Auto-reply/allowlist store account scoping: keep `/allowlist ... --store` writes scoped to the selected account and clear legacy unscoped entries when removing default-account store access, preventing cross-account default allowlist bleed-through from legacy pairing-store reads. Thanks @vincentkoc.
|
||||||
- Security/Nostr: harden profile mutation/import loopback guards by failing closed on non-loopback forwarded client headers (`x-forwarded-for` / `x-real-ip`) and rejecting `sec-fetch-site: cross-site`; adds regression coverage for proxy-forwarded and browser cross-site mutation attempts.
|
- Security/Nostr: harden profile mutation/import loopback guards by failing closed on non-loopback forwarded client headers (`x-forwarded-for` / `x-real-ip`) and rejecting `sec-fetch-site: cross-site`; adds regression coverage for proxy-forwarded and browser cross-site mutation attempts.
|
||||||
|
|
||||||
## 2026.3.2
|
## 2026.3.2
|
||||||
|
|||||||
@@ -196,6 +196,31 @@ function extractConfigAllowlist(account: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updatePairingStoreAllowlist(params: {
|
||||||
|
action: "add" | "remove";
|
||||||
|
channelId: ChannelId;
|
||||||
|
accountId?: string;
|
||||||
|
entry: string;
|
||||||
|
}) {
|
||||||
|
const storeEntry = {
|
||||||
|
channel: params.channelId,
|
||||||
|
entry: params.entry,
|
||||||
|
accountId: params.accountId,
|
||||||
|
};
|
||||||
|
if (params.action === "add") {
|
||||||
|
await addChannelAllowFromStoreEntry(storeEntry);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await removeChannelAllowFromStoreEntry(storeEntry);
|
||||||
|
if (params.accountId === DEFAULT_ACCOUNT_ID) {
|
||||||
|
await removeChannelAllowFromStoreEntry({
|
||||||
|
channel: params.channelId,
|
||||||
|
entry: params.entry,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function resolveAccountTarget(
|
function resolveAccountTarget(
|
||||||
parsed: Record<string, unknown>,
|
parsed: Record<string, unknown>,
|
||||||
channelId: ChannelId,
|
channelId: ChannelId,
|
||||||
@@ -695,11 +720,12 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shouldTouchStore) {
|
if (shouldTouchStore) {
|
||||||
if (parsed.action === "add") {
|
await updatePairingStoreAllowlist({
|
||||||
await addChannelAllowFromStoreEntry({ channel: channelId, entry: parsed.entry });
|
action: parsed.action,
|
||||||
} else if (parsed.action === "remove") {
|
channelId,
|
||||||
await removeChannelAllowFromStoreEntry({ channel: channelId, entry: parsed.entry });
|
accountId,
|
||||||
}
|
entry: parsed.entry,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionLabel = parsed.action === "add" ? "added" : "removed";
|
const actionLabel = parsed.action === "add" ? "added" : "removed";
|
||||||
@@ -727,11 +753,12 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.action === "add") {
|
await updatePairingStoreAllowlist({
|
||||||
await addChannelAllowFromStoreEntry({ channel: channelId, entry: parsed.entry });
|
action: parsed.action,
|
||||||
} else if (parsed.action === "remove") {
|
channelId,
|
||||||
await removeChannelAllowFromStoreEntry({ channel: channelId, entry: parsed.entry });
|
accountId,
|
||||||
}
|
entry: parsed.entry,
|
||||||
|
});
|
||||||
|
|
||||||
const actionLabel = parsed.action === "add" ? "added" : "removed";
|
const actionLabel = parsed.action === "add" ? "added" : "removed";
|
||||||
const scopeLabel = scope === "dm" ? "DM" : "group";
|
const scopeLabel = scope === "dm" ? "DM" : "group";
|
||||||
|
|||||||
@@ -704,10 +704,74 @@ describe("handleCommands /allowlist", () => {
|
|||||||
expect(addChannelAllowFromStoreEntryMock).toHaveBeenCalledWith({
|
expect(addChannelAllowFromStoreEntryMock).toHaveBeenCalledWith({
|
||||||
channel: "telegram",
|
channel: "telegram",
|
||||||
entry: "789",
|
entry: "789",
|
||||||
|
accountId: "default",
|
||||||
});
|
});
|
||||||
expect(result.reply?.text).toContain("DM allowlist added");
|
expect(result.reply?.text).toContain("DM allowlist added");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("writes store entries to the selected account scope", async () => {
|
||||||
|
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||||
|
valid: true,
|
||||||
|
parsed: {
|
||||||
|
channels: { telegram: { accounts: { work: { allowFrom: ["123"] } } } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
validateConfigObjectWithPluginsMock.mockImplementation((config: unknown) => ({
|
||||||
|
ok: true,
|
||||||
|
config,
|
||||||
|
}));
|
||||||
|
addChannelAllowFromStoreEntryMock.mockResolvedValueOnce({
|
||||||
|
changed: true,
|
||||||
|
allowFrom: ["123", "789"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const cfg = {
|
||||||
|
commands: { text: true, config: true },
|
||||||
|
channels: { telegram: { accounts: { work: { allowFrom: ["123"] } } } },
|
||||||
|
} as OpenClawConfig;
|
||||||
|
const params = buildPolicyParams("/allowlist add dm --account work 789", cfg, {
|
||||||
|
AccountId: "work",
|
||||||
|
});
|
||||||
|
const result = await handleCommands(params);
|
||||||
|
|
||||||
|
expect(result.shouldContinue).toBe(false);
|
||||||
|
expect(addChannelAllowFromStoreEntryMock).toHaveBeenCalledWith({
|
||||||
|
channel: "telegram",
|
||||||
|
entry: "789",
|
||||||
|
accountId: "work",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes default-account entries from scoped and legacy pairing stores", async () => {
|
||||||
|
removeChannelAllowFromStoreEntryMock
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
changed: true,
|
||||||
|
allowFrom: [],
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
changed: true,
|
||||||
|
allowFrom: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const cfg = {
|
||||||
|
commands: { text: true, config: true },
|
||||||
|
channels: { telegram: { allowFrom: ["123"] } },
|
||||||
|
} as OpenClawConfig;
|
||||||
|
const params = buildPolicyParams("/allowlist remove dm --store 789", cfg);
|
||||||
|
const result = await handleCommands(params);
|
||||||
|
|
||||||
|
expect(result.shouldContinue).toBe(false);
|
||||||
|
expect(removeChannelAllowFromStoreEntryMock).toHaveBeenNthCalledWith(1, {
|
||||||
|
channel: "telegram",
|
||||||
|
entry: "789",
|
||||||
|
accountId: "default",
|
||||||
|
});
|
||||||
|
expect(removeChannelAllowFromStoreEntryMock).toHaveBeenNthCalledWith(2, {
|
||||||
|
channel: "telegram",
|
||||||
|
entry: "789",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects blocked account ids and keeps Object.prototype clean", async () => {
|
it("rejects blocked account ids and keeps Object.prototype clean", async () => {
|
||||||
delete (Object.prototype as Record<string, unknown>).allowFrom;
|
delete (Object.prototype as Record<string, unknown>).allowFrom;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user