mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-28 23:02:02 +03:00
fix(googlechat): deprecate users/<email> allowlists (#16243)
This commit is contained in:
committed by
GitHub
parent
3967ece625
commit
c8424bf29a
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
- Security: fix Chutes manual OAuth login state validation (thanks @aether-ai-agent). (#16058)
|
- Security: fix Chutes manual OAuth login state validation (thanks @aether-ai-agent). (#16058)
|
||||||
- macOS: hard-limit unkeyed `openclaw://agent` deep links and ignore `deliver` / `to` / `channel` unless a valid unattended key is provided. Thanks @Cillian-Collins.
|
- macOS: hard-limit unkeyed `openclaw://agent` deep links and ignore `deliver` / `to` / `channel` unless a valid unattended key is provided. Thanks @Cillian-Collins.
|
||||||
|
- Security/Google Chat: deprecate `users/<email>` allowlists (treat `users/...` as immutable user id only); keep raw email allowlists for usability. Thanks @vincentkoc.
|
||||||
|
|
||||||
## 2026.2.14
|
## 2026.2.14
|
||||||
|
|
||||||
|
|||||||
@@ -153,7 +153,8 @@ Configure your tunnel's ingress rules to only route the webhook path:
|
|||||||
|
|
||||||
Use these identifiers for delivery and allowlists:
|
Use these identifiers for delivery and allowlists:
|
||||||
|
|
||||||
- Direct messages: `users/<userId>` or `users/<email>` (email addresses are accepted).
|
- Direct messages: `users/<userId>` (recommended) or raw email `name@example.com` (mutable principal).
|
||||||
|
- Deprecated: `users/<email>` is treated as a user id, not an email allowlist.
|
||||||
- Spaces: `spaces/<spaceId>`.
|
- Spaces: `spaces/<spaceId>`.
|
||||||
|
|
||||||
## Config highlights
|
## Config highlights
|
||||||
|
|||||||
@@ -2,21 +2,21 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { isSenderAllowed } from "./monitor.js";
|
import { isSenderAllowed } from "./monitor.js";
|
||||||
|
|
||||||
describe("isSenderAllowed", () => {
|
describe("isSenderAllowed", () => {
|
||||||
it("matches allowlist entries with users/<email>", () => {
|
|
||||||
expect(isSenderAllowed("users/123", "Jane@Example.com", ["users/jane@example.com"])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("matches allowlist entries with raw email", () => {
|
it("matches allowlist entries with raw email", () => {
|
||||||
expect(isSenderAllowed("users/123", "Jane@Example.com", ["jane@example.com"])).toBe(true);
|
expect(isSenderAllowed("users/123", "Jane@Example.com", ["jane@example.com"])).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not treat users/<email> entries as email allowlist (deprecated form)", () => {
|
||||||
|
expect(isSenderAllowed("users/123", "Jane@Example.com", ["users/jane@example.com"])).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("still matches user id entries", () => {
|
it("still matches user id entries", () => {
|
||||||
expect(isSenderAllowed("users/abc", "jane@example.com", ["users/abc"])).toBe(true);
|
expect(isSenderAllowed("users/abc", "jane@example.com", ["users/abc"])).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects non-matching emails", () => {
|
it("rejects non-matching raw email entries", () => {
|
||||||
expect(isSenderAllowed("users/123", "jane@example.com", ["users/other@example.com"])).toBe(
|
expect(isSenderAllowed("users/123", "jane@example.com", ["other@example.com"])).toBe(false);
|
||||||
false,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,6 +61,31 @@ function logVerbose(core: GoogleChatCoreRuntime, runtime: GoogleChatRuntimeEnv,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const warnedDeprecatedUsersEmailAllowFrom = new Set<string>();
|
||||||
|
function warnDeprecatedUsersEmailEntries(
|
||||||
|
core: GoogleChatCoreRuntime,
|
||||||
|
runtime: GoogleChatRuntimeEnv,
|
||||||
|
entries: string[],
|
||||||
|
) {
|
||||||
|
const deprecated = entries.map((v) => String(v).trim()).filter((v) => /^users\/.+@.+/i.test(v));
|
||||||
|
if (deprecated.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const key = deprecated
|
||||||
|
.map((v) => v.toLowerCase())
|
||||||
|
.sort()
|
||||||
|
.join(",");
|
||||||
|
if (warnedDeprecatedUsersEmailAllowFrom.has(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
warnedDeprecatedUsersEmailAllowFrom.add(key);
|
||||||
|
logVerbose(
|
||||||
|
core,
|
||||||
|
runtime,
|
||||||
|
`Deprecated allowFrom entry detected: "users/<email>" is no longer treated as an email allowlist. Use raw email (alice@example.com) or immutable user id (users/<id>). entries=${deprecated.join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeWebhookPath(raw: string): string {
|
function normalizeWebhookPath(raw: string): string {
|
||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
@@ -285,6 +310,11 @@ function normalizeUserId(raw?: string | null): string {
|
|||||||
return trimmed.replace(/^users\//i, "").toLowerCase();
|
return trimmed.replace(/^users\//i, "").toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isEmailLike(value: string): boolean {
|
||||||
|
// Keep this intentionally loose; allowlists are user-provided config.
|
||||||
|
return value.includes("@");
|
||||||
|
}
|
||||||
|
|
||||||
export function isSenderAllowed(
|
export function isSenderAllowed(
|
||||||
senderId: string,
|
senderId: string,
|
||||||
senderEmail: string | undefined,
|
senderEmail: string | undefined,
|
||||||
@@ -300,22 +330,19 @@ export function isSenderAllowed(
|
|||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (normalized === normalizedSenderId) {
|
|
||||||
return true;
|
// Accept `googlechat:<id>` but treat `users/...` as an *ID* only (deprecated `users/<email>`).
|
||||||
|
const withoutPrefix = normalized.replace(/^(googlechat|google-chat|gchat):/i, "");
|
||||||
|
if (withoutPrefix.startsWith("users/")) {
|
||||||
|
return normalizeUserId(withoutPrefix) === normalizedSenderId;
|
||||||
}
|
}
|
||||||
if (normalizedEmail && normalized === normalizedEmail) {
|
|
||||||
return true;
|
// Raw email allowlist entries remain supported for usability.
|
||||||
|
if (normalizedEmail && isEmailLike(withoutPrefix)) {
|
||||||
|
return withoutPrefix === normalizedEmail;
|
||||||
}
|
}
|
||||||
if (normalizedEmail && normalized.replace(/^users\//i, "") === normalizedEmail) {
|
|
||||||
return true;
|
return withoutPrefix.replace(/^users\//i, "") === normalizedSenderId;
|
||||||
}
|
|
||||||
if (normalized.replace(/^users\//i, "") === normalizedSenderId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (normalized.replace(/^(googlechat|google-chat|gchat):/i, "") === normalizedSenderId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,6 +500,11 @@ async function processMessageWithPipeline(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (groupUsers.length > 0) {
|
if (groupUsers.length > 0) {
|
||||||
|
warnDeprecatedUsersEmailEntries(
|
||||||
|
core,
|
||||||
|
runtime,
|
||||||
|
groupUsers.map((v) => String(v)),
|
||||||
|
);
|
||||||
const ok = isSenderAllowed(
|
const ok = isSenderAllowed(
|
||||||
senderId,
|
senderId,
|
||||||
senderEmail,
|
senderEmail,
|
||||||
@@ -493,6 +525,7 @@ async function processMessageWithPipeline(params: {
|
|||||||
? await core.channel.pairing.readAllowFromStore("googlechat").catch(() => [])
|
? await core.channel.pairing.readAllowFromStore("googlechat").catch(() => [])
|
||||||
: [];
|
: [];
|
||||||
const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom];
|
const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom];
|
||||||
|
warnDeprecatedUsersEmailEntries(core, runtime, effectiveAllowFrom);
|
||||||
const commandAllowFrom = isGroup ? groupUsers.map((v) => String(v)) : effectiveAllowFrom;
|
const commandAllowFrom = isGroup ? groupUsers.map((v) => String(v)) : effectiveAllowFrom;
|
||||||
const useAccessGroups = config.commands?.useAccessGroups !== false;
|
const useAccessGroups = config.commands?.useAccessGroups !== false;
|
||||||
const senderAllowedForCommands = isSenderAllowed(senderId, senderEmail, commandAllowFrom);
|
const senderAllowedForCommands = isSenderAllowed(senderId, senderEmail, commandAllowFrom);
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ async function promptAllowFrom(params: {
|
|||||||
}): Promise<OpenClawConfig> {
|
}): Promise<OpenClawConfig> {
|
||||||
const current = params.cfg.channels?.["googlechat"]?.dm?.allowFrom ?? [];
|
const current = params.cfg.channels?.["googlechat"]?.dm?.allowFrom ?? [];
|
||||||
const entry = await params.prompter.text({
|
const entry = await params.prompter.text({
|
||||||
message: "Google Chat allowFrom (user id or email)",
|
message: "Google Chat allowFrom (users/<id> or raw email; avoid users/<email>)",
|
||||||
placeholder: "users/123456789, name@example.com",
|
placeholder: "users/123456789, name@example.com",
|
||||||
initialValue: current[0] ? String(current[0]) : undefined,
|
initialValue: current[0] ? String(current[0]) : undefined,
|
||||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||||
|
|||||||
Reference in New Issue
Block a user