mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 20:30:21 +00:00
refactor: add approval auth capabilities to more channels
This commit is contained in:
24
extensions/feishu/src/approval-auth.test.ts
Normal file
24
extensions/feishu/src/approval-auth.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { feishuApprovalAuth } from "./approval-auth.js";
|
||||
|
||||
describe("feishuApprovalAuth", () => {
|
||||
it("authorizes open_id approvers and ignores user_id-only allowlists", () => {
|
||||
expect(
|
||||
feishuApprovalAuth.authorizeActorAction({
|
||||
cfg: { channels: { feishu: { allowFrom: ["ou_owner"] } } },
|
||||
senderId: "ou_owner",
|
||||
action: "approve",
|
||||
approvalKind: "exec",
|
||||
}),
|
||||
).toEqual({ authorized: true });
|
||||
|
||||
expect(
|
||||
feishuApprovalAuth.authorizeActorAction({
|
||||
cfg: { channels: { feishu: { allowFrom: ["user_123"] } } },
|
||||
senderId: "ou_attacker",
|
||||
action: "approve",
|
||||
approvalKind: "exec",
|
||||
}),
|
||||
).toEqual({ authorized: true });
|
||||
});
|
||||
});
|
||||
24
extensions/feishu/src/approval-auth.ts
Normal file
24
extensions/feishu/src/approval-auth.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
createResolvedApproverActionAuthAdapter,
|
||||
resolveApprovalApprovers,
|
||||
} from "openclaw/plugin-sdk/approval-runtime";
|
||||
import { resolveFeishuAccount } from "./accounts.js";
|
||||
import { normalizeFeishuTarget } from "./targets.js";
|
||||
|
||||
function normalizeFeishuApproverId(value: string | number): string | undefined {
|
||||
const normalized = normalizeFeishuTarget(String(value));
|
||||
const trimmed = normalized?.trim().toLowerCase();
|
||||
return trimmed?.startsWith("ou_") ? trimmed : undefined;
|
||||
}
|
||||
|
||||
export const feishuApprovalAuth = createResolvedApproverActionAuthAdapter({
|
||||
channelLabel: "Feishu",
|
||||
resolveApprovers: ({ cfg, accountId }) => {
|
||||
const account = resolveFeishuAccount({ cfg, accountId }).config;
|
||||
return resolveApprovalApprovers({
|
||||
allowFrom: account.allowFrom,
|
||||
normalizeApprover: normalizeFeishuApproverId,
|
||||
});
|
||||
},
|
||||
normalizeSenderId: (value) => normalizeFeishuApproverId(value),
|
||||
});
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
listEnabledFeishuAccounts,
|
||||
resolveDefaultFeishuAccountId,
|
||||
} from "./accounts.js";
|
||||
import { feishuApprovalAuth } from "./approval-auth.js";
|
||||
import { FEISHU_CARD_INTERACTION_VERSION } from "./card-interaction.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { FeishuConfigSchema } from "./config-schema.js";
|
||||
@@ -612,6 +613,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount, FeishuProbeResul
|
||||
},
|
||||
}),
|
||||
},
|
||||
auth: feishuApprovalAuth,
|
||||
actions: {
|
||||
describeMessageTool: describeFeishuMessageTool,
|
||||
handleAction: async (ctx) => {
|
||||
|
||||
24
extensions/googlechat/src/approval-auth.test.ts
Normal file
24
extensions/googlechat/src/approval-auth.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { googleChatApprovalAuth } from "./approval-auth.js";
|
||||
|
||||
describe("googleChatApprovalAuth", () => {
|
||||
it("authorizes stable users/* ids and ignores email-style approvers", () => {
|
||||
expect(
|
||||
googleChatApprovalAuth.authorizeActorAction({
|
||||
cfg: { channels: { googlechat: { dm: { allowFrom: ["users/123"] } } } },
|
||||
senderId: "users/123",
|
||||
action: "approve",
|
||||
approvalKind: "exec",
|
||||
}),
|
||||
).toEqual({ authorized: true });
|
||||
|
||||
expect(
|
||||
googleChatApprovalAuth.authorizeActorAction({
|
||||
cfg: { channels: { googlechat: { dm: { allowFrom: ["owner@example.com"] } } } },
|
||||
senderId: "users/attacker",
|
||||
action: "approve",
|
||||
approvalKind: "exec",
|
||||
}),
|
||||
).toEqual({ authorized: true });
|
||||
});
|
||||
});
|
||||
31
extensions/googlechat/src/approval-auth.ts
Normal file
31
extensions/googlechat/src/approval-auth.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
createResolvedApproverActionAuthAdapter,
|
||||
resolveApprovalApprovers,
|
||||
} from "openclaw/plugin-sdk/approval-runtime";
|
||||
import { resolveGoogleChatAccount } from "./accounts.js";
|
||||
import { isGoogleChatUserTarget, normalizeGoogleChatTarget } from "./targets.js";
|
||||
|
||||
function normalizeGoogleChatApproverId(value: string | number): string | undefined {
|
||||
const normalized = normalizeGoogleChatTarget(String(value));
|
||||
if (!normalized || !isGoogleChatUserTarget(normalized)) {
|
||||
return undefined;
|
||||
}
|
||||
const suffix = normalized.slice("users/".length).trim().toLowerCase();
|
||||
if (!suffix || suffix.includes("@")) {
|
||||
return undefined;
|
||||
}
|
||||
return `users/${suffix}`;
|
||||
}
|
||||
|
||||
export const googleChatApprovalAuth = createResolvedApproverActionAuthAdapter({
|
||||
channelLabel: "Google Chat",
|
||||
resolveApprovers: ({ cfg, accountId }) => {
|
||||
const account = resolveGoogleChatAccount({ cfg, accountId }).config;
|
||||
return resolveApprovalApprovers({
|
||||
allowFrom: account.dm?.allowFrom,
|
||||
defaultTo: account.defaultTo,
|
||||
normalizeApprover: normalizeGoogleChatApproverId,
|
||||
});
|
||||
},
|
||||
normalizeSenderId: (value) => normalizeGoogleChatApproverId(value),
|
||||
});
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
type ResolvedGoogleChatAccount,
|
||||
} from "./accounts.js";
|
||||
import { googlechatMessageActions } from "./actions.js";
|
||||
import { googleChatApprovalAuth } from "./approval-auth.js";
|
||||
import { resolveGoogleChatGroupRequireMention } from "./group-policy.js";
|
||||
import { getGoogleChatRuntime } from "./runtime.js";
|
||||
import { googlechatSetupAdapter } from "./setup-core.js";
|
||||
@@ -163,6 +164,7 @@ export const googlechatPlugin = createChatChannelPlugin({
|
||||
},
|
||||
}),
|
||||
},
|
||||
auth: googleChatApprovalAuth,
|
||||
groups: {
|
||||
resolveRequireMention: resolveGoogleChatGroupRequireMention,
|
||||
},
|
||||
|
||||
23
extensions/matrix/src/approval-auth.test.ts
Normal file
23
extensions/matrix/src/approval-auth.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { matrixApprovalAuth } from "./approval-auth.js";
|
||||
|
||||
describe("matrixApprovalAuth", () => {
|
||||
it("normalizes Matrix user ids before authorizing", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
dm: { allowFrom: ["matrix:@Owner:Example.org"] },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
matrixApprovalAuth.authorizeActorAction({
|
||||
cfg,
|
||||
senderId: "@owner:example.org",
|
||||
action: "approve",
|
||||
approvalKind: "plugin",
|
||||
}),
|
||||
).toEqual({ authorized: true });
|
||||
});
|
||||
});
|
||||
24
extensions/matrix/src/approval-auth.ts
Normal file
24
extensions/matrix/src/approval-auth.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
createResolvedApproverActionAuthAdapter,
|
||||
resolveApprovalApprovers,
|
||||
} from "openclaw/plugin-sdk/approval-runtime";
|
||||
import { resolveMatrixAccount } from "./matrix/accounts.js";
|
||||
import { normalizeMatrixUserId } from "./matrix/monitor/allowlist.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
function normalizeMatrixApproverId(value: string | number): string | undefined {
|
||||
const normalized = normalizeMatrixUserId(String(value));
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
export const matrixApprovalAuth = createResolvedApproverActionAuthAdapter({
|
||||
channelLabel: "Matrix",
|
||||
resolveApprovers: ({ cfg, accountId }) => {
|
||||
const account = resolveMatrixAccount({ cfg: cfg as CoreConfig, accountId });
|
||||
return resolveApprovalApprovers({
|
||||
allowFrom: account.config.dm?.allowFrom,
|
||||
normalizeApprover: normalizeMatrixApproverId,
|
||||
});
|
||||
},
|
||||
normalizeSenderId: (value) => normalizeMatrixApproverId(value),
|
||||
});
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
createDefaultChannelRuntimeState,
|
||||
} from "openclaw/plugin-sdk/status-helpers";
|
||||
import { matrixMessageActions } from "./actions.js";
|
||||
import { matrixApprovalAuth } from "./approval-auth.js";
|
||||
import { MatrixConfigSchema } from "./config-schema.js";
|
||||
import {
|
||||
resolveMatrixGroupRequireMention,
|
||||
@@ -308,6 +309,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount, MatrixProbe> =
|
||||
},
|
||||
}),
|
||||
},
|
||||
auth: matrixApprovalAuth,
|
||||
groups: {
|
||||
resolveRequireMention: resolveMatrixGroupRequireMention,
|
||||
resolveToolPolicy: resolveMatrixGroupToolPolicy,
|
||||
|
||||
28
extensions/mattermost/src/approval-auth.test.ts
Normal file
28
extensions/mattermost/src/approval-auth.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { mattermostApprovalAuth } from "./approval-auth.js";
|
||||
|
||||
describe("mattermostApprovalAuth", () => {
|
||||
it("authorizes stable Mattermost user ids and ignores usernames", () => {
|
||||
expect(
|
||||
mattermostApprovalAuth.authorizeActorAction({
|
||||
cfg: {
|
||||
channels: { mattermost: { allowFrom: ["user:abcdefghijklmnopqrstuvwxyz"] } },
|
||||
},
|
||||
senderId: "abcdefghijklmnopqrstuvwxyz",
|
||||
action: "approve",
|
||||
approvalKind: "exec",
|
||||
}),
|
||||
).toEqual({ authorized: true });
|
||||
|
||||
expect(
|
||||
mattermostApprovalAuth.authorizeActorAction({
|
||||
cfg: {
|
||||
channels: { mattermost: { allowFrom: ["@owner"] } },
|
||||
},
|
||||
senderId: "attacker-user-id",
|
||||
action: "approve",
|
||||
approvalKind: "exec",
|
||||
}),
|
||||
).toEqual({ authorized: true });
|
||||
});
|
||||
});
|
||||
29
extensions/mattermost/src/approval-auth.ts
Normal file
29
extensions/mattermost/src/approval-auth.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
createResolvedApproverActionAuthAdapter,
|
||||
resolveApprovalApprovers,
|
||||
} from "openclaw/plugin-sdk/approval-runtime";
|
||||
import { resolveMattermostAccount } from "./mattermost/accounts.js";
|
||||
|
||||
const MATTERMOST_USER_ID_RE = /^[a-z0-9]{26}$/;
|
||||
|
||||
function normalizeMattermostApproverId(value: string | number): string | undefined {
|
||||
const normalized = String(value)
|
||||
.trim()
|
||||
.replace(/^(mattermost|user):/i, "")
|
||||
.replace(/^@/, "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return MATTERMOST_USER_ID_RE.test(normalized) ? normalized : undefined;
|
||||
}
|
||||
|
||||
export const mattermostApprovalAuth = createResolvedApproverActionAuthAdapter({
|
||||
channelLabel: "Mattermost",
|
||||
resolveApprovers: ({ cfg, accountId }) => {
|
||||
const account = resolveMattermostAccount({ cfg, accountId }).config;
|
||||
return resolveApprovalApprovers({
|
||||
allowFrom: account.allowFrom,
|
||||
normalizeApprover: normalizeMattermostApproverId,
|
||||
});
|
||||
},
|
||||
normalizeSenderId: (value) => normalizeMattermostApproverId(value),
|
||||
});
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
createComputedAccountStatusAdapter,
|
||||
createDefaultChannelRuntimeState,
|
||||
} from "openclaw/plugin-sdk/status-helpers";
|
||||
import { mattermostApprovalAuth } from "./approval-auth.js";
|
||||
import { MattermostChannelConfigSchema } from "./config-surface.js";
|
||||
import { resolveMattermostGroupRequireMention } from "./group-mentions.js";
|
||||
import {
|
||||
@@ -320,6 +321,7 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = create
|
||||
},
|
||||
}),
|
||||
},
|
||||
auth: mattermostApprovalAuth,
|
||||
groups: {
|
||||
resolveRequireMention: resolveMattermostGroupRequireMention,
|
||||
},
|
||||
|
||||
32
extensions/msteams/src/approval-auth.test.ts
Normal file
32
extensions/msteams/src/approval-auth.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { msTeamsApprovalAuth } from "./approval-auth.js";
|
||||
|
||||
describe("msTeamsApprovalAuth", () => {
|
||||
it("authorizes stable Teams user ids and ignores display-name allowlists", () => {
|
||||
expect(
|
||||
msTeamsApprovalAuth.authorizeActorAction({
|
||||
cfg: {
|
||||
channels: {
|
||||
msteams: {
|
||||
allowFrom: ["user:123e4567-e89b-12d3-a456-426614174000"],
|
||||
},
|
||||
},
|
||||
},
|
||||
senderId: "123e4567-e89b-12d3-a456-426614174000",
|
||||
action: "approve",
|
||||
approvalKind: "exec",
|
||||
}),
|
||||
).toEqual({ authorized: true });
|
||||
|
||||
expect(
|
||||
msTeamsApprovalAuth.authorizeActorAction({
|
||||
cfg: {
|
||||
channels: { msteams: { allowFrom: ["Owner Display"] } },
|
||||
},
|
||||
senderId: "attacker-aad",
|
||||
action: "approve",
|
||||
approvalKind: "exec",
|
||||
}),
|
||||
).toEqual({ authorized: true });
|
||||
});
|
||||
});
|
||||
37
extensions/msteams/src/approval-auth.ts
Normal file
37
extensions/msteams/src/approval-auth.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
createResolvedApproverActionAuthAdapter,
|
||||
resolveApprovalApprovers,
|
||||
} from "openclaw/plugin-sdk/approval-runtime";
|
||||
import type { OpenClawConfig } from "../runtime-api.js";
|
||||
import { normalizeMSTeamsMessagingTarget } from "./resolve-allowlist.js";
|
||||
|
||||
const MSTEAMS_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
function normalizeMSTeamsApproverId(value: string | number): string | undefined {
|
||||
const normalized = normalizeMSTeamsMessagingTarget(String(value));
|
||||
if (!normalized?.startsWith("user:")) {
|
||||
return undefined;
|
||||
}
|
||||
const id = normalized.slice("user:".length).trim().toLowerCase();
|
||||
return MSTEAMS_ID_RE.test(id) ? id : undefined;
|
||||
}
|
||||
|
||||
function resolveMSTeamsChannelConfig(cfg: OpenClawConfig) {
|
||||
return cfg.channels?.msteams;
|
||||
}
|
||||
|
||||
export const msTeamsApprovalAuth = createResolvedApproverActionAuthAdapter({
|
||||
channelLabel: "Microsoft Teams",
|
||||
resolveApprovers: ({ cfg }) => {
|
||||
const channel = resolveMSTeamsChannelConfig(cfg);
|
||||
return resolveApprovalApprovers({
|
||||
allowFrom: channel?.allowFrom,
|
||||
defaultTo: channel?.defaultTo,
|
||||
normalizeApprover: normalizeMSTeamsApproverId,
|
||||
});
|
||||
},
|
||||
normalizeSenderId: (value) => {
|
||||
const trimmed = value.trim().toLowerCase();
|
||||
return MSTEAMS_ID_RE.test(trimmed) ? trimmed : undefined;
|
||||
},
|
||||
});
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
} from "../runtime-api.js";
|
||||
import { msTeamsApprovalAuth } from "./approval-auth.js";
|
||||
import { MSTeamsChannelConfigSchema } from "./config-schema.js";
|
||||
import { resolveMSTeamsGroupToolPolicy } from "./policy.js";
|
||||
import type { ProbeMSTeamsResult } from "./probe.js";
|
||||
@@ -379,6 +380,7 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount, ProbeMSTeamsRe
|
||||
configured: account.configured,
|
||||
}),
|
||||
},
|
||||
auth: msTeamsApprovalAuth,
|
||||
setup: msteamsSetupAdapter,
|
||||
messaging: {
|
||||
normalizeTarget: normalizeMSTeamsMessagingTarget,
|
||||
|
||||
17
extensions/nextcloud-talk/src/approval-auth.test.ts
Normal file
17
extensions/nextcloud-talk/src/approval-auth.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { nextcloudTalkApprovalAuth } from "./approval-auth.js";
|
||||
|
||||
describe("nextcloudTalkApprovalAuth", () => {
|
||||
it("matches Nextcloud Talk actor ids case-insensitively", () => {
|
||||
const cfg = { channels: { "nextcloud-talk": { allowFrom: ["Owner"] } } };
|
||||
|
||||
expect(
|
||||
nextcloudTalkApprovalAuth.authorizeActorAction({
|
||||
cfg,
|
||||
senderId: "owner",
|
||||
action: "approve",
|
||||
approvalKind: "exec",
|
||||
}),
|
||||
).toEqual({ authorized: true });
|
||||
});
|
||||
});
|
||||
27
extensions/nextcloud-talk/src/approval-auth.ts
Normal file
27
extensions/nextcloud-talk/src/approval-auth.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
createResolvedApproverActionAuthAdapter,
|
||||
resolveApprovalApprovers,
|
||||
} from "openclaw/plugin-sdk/approval-runtime";
|
||||
import { resolveNextcloudTalkAccount } from "./accounts.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
function normalizeNextcloudTalkApproverId(value: string | number): string | undefined {
|
||||
const normalized = String(value)
|
||||
.trim()
|
||||
.replace(/^(nextcloud-talk|nc-talk|nc):/i, "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
export const nextcloudTalkApprovalAuth = createResolvedApproverActionAuthAdapter({
|
||||
channelLabel: "Nextcloud Talk",
|
||||
resolveApprovers: ({ cfg, accountId }) => {
|
||||
const account = resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId });
|
||||
return resolveApprovalApprovers({
|
||||
allowFrom: account.config.allowFrom,
|
||||
normalizeApprover: normalizeNextcloudTalkApproverId,
|
||||
});
|
||||
},
|
||||
normalizeSenderId: (value) => normalizeNextcloudTalkApproverId(value),
|
||||
});
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
resolveNextcloudTalkAccount,
|
||||
type ResolvedNextcloudTalkAccount,
|
||||
} from "./accounts.js";
|
||||
import { nextcloudTalkApprovalAuth } from "./approval-auth.js";
|
||||
import { NextcloudTalkConfigSchema } from "./config-schema.js";
|
||||
import { monitorNextcloudTalkProvider } from "./monitor.js";
|
||||
import {
|
||||
@@ -139,6 +140,7 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
||||
},
|
||||
}),
|
||||
},
|
||||
auth: nextcloudTalkApprovalAuth,
|
||||
groups: {
|
||||
resolveRequireMention: ({ cfg, accountId, groupId }) => {
|
||||
const account = resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId });
|
||||
|
||||
32
extensions/signal/src/approval-auth.test.ts
Normal file
32
extensions/signal/src/approval-auth.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { signalApprovalAuth } from "./approval-auth.js";
|
||||
|
||||
describe("signalApprovalAuth", () => {
|
||||
it("authorizes phone and uuid approvers with stable sender ids", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
signal: {
|
||||
allowFrom: ["uuid:ABCDEF12-3456-7890-ABCD-EF1234567890", "+1 (555) 123-0000"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
signalApprovalAuth.authorizeActorAction({
|
||||
cfg,
|
||||
senderId: "uuid:abcdef12-3456-7890-abcd-ef1234567890",
|
||||
action: "approve",
|
||||
approvalKind: "exec",
|
||||
}),
|
||||
).toEqual({ authorized: true });
|
||||
|
||||
expect(
|
||||
signalApprovalAuth.authorizeActorAction({
|
||||
cfg,
|
||||
senderId: "+15551230000",
|
||||
action: "approve",
|
||||
approvalKind: "exec",
|
||||
}),
|
||||
).toEqual({ authorized: true });
|
||||
});
|
||||
});
|
||||
33
extensions/signal/src/approval-auth.ts
Normal file
33
extensions/signal/src/approval-auth.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
createResolvedApproverActionAuthAdapter,
|
||||
resolveApprovalApprovers,
|
||||
} from "openclaw/plugin-sdk/approval-runtime";
|
||||
import { normalizeE164 } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { resolveSignalAccount } from "./accounts.js";
|
||||
import { looksLikeUuid } from "./identity.js";
|
||||
import { normalizeSignalMessagingTarget } from "./normalize.js";
|
||||
|
||||
function normalizeSignalApproverId(value: string | number): string | undefined {
|
||||
const normalized = normalizeSignalMessagingTarget(String(value));
|
||||
if (!normalized || normalized.startsWith("group:") || normalized.startsWith("username:")) {
|
||||
return undefined;
|
||||
}
|
||||
if (looksLikeUuid(normalized)) {
|
||||
return `uuid:${normalized}`;
|
||||
}
|
||||
const e164 = normalizeE164(normalized);
|
||||
return e164.length > 1 ? e164 : undefined;
|
||||
}
|
||||
|
||||
export const signalApprovalAuth = createResolvedApproverActionAuthAdapter({
|
||||
channelLabel: "Signal",
|
||||
resolveApprovers: ({ cfg, accountId }) => {
|
||||
const account = resolveSignalAccount({ cfg, accountId }).config;
|
||||
return resolveApprovalApprovers({
|
||||
allowFrom: account.allowFrom,
|
||||
defaultTo: account.defaultTo,
|
||||
normalizeApprover: normalizeSignalApproverId,
|
||||
});
|
||||
},
|
||||
normalizeSenderId: (value) => normalizeSignalApproverId(value),
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import { resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { buildOutboundBaseSessionKey, type RoutePeer } from "openclaw/plugin-sdk/routing";
|
||||
import { createComputedAccountStatusAdapter } from "openclaw/plugin-sdk/status-helpers";
|
||||
import { resolveSignalAccount, type ResolvedSignalAccount } from "./accounts.js";
|
||||
import { signalApprovalAuth } from "./approval-auth.js";
|
||||
import { markdownToSignalTextChunks } from "./format.js";
|
||||
import { signalMessageActions } from "./message-actions.js";
|
||||
import { looksLikeSignalTargetId, normalizeSignalMessagingTarget } from "./normalize.js";
|
||||
@@ -228,6 +229,7 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount, SignalProbe> =
|
||||
setup: signalSetupAdapter,
|
||||
}),
|
||||
actions: signalMessageActions,
|
||||
auth: signalApprovalAuth,
|
||||
allowlist: buildDmGroupAccountAllowlistAdapter({
|
||||
channelId: "signal",
|
||||
resolveAccount: resolveSignalAccount,
|
||||
|
||||
29
extensions/slack/src/approval-auth.test.ts
Normal file
29
extensions/slack/src/approval-auth.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { slackApprovalAuth } from "./approval-auth.js";
|
||||
|
||||
describe("slackApprovalAuth", () => {
|
||||
it("authorizes inferred Slack approvers by user id", () => {
|
||||
const cfg = { channels: { slack: { allowFrom: ["U_OWNER"] } } };
|
||||
|
||||
expect(
|
||||
slackApprovalAuth.authorizeActorAction({
|
||||
cfg,
|
||||
senderId: "U_OWNER",
|
||||
action: "approve",
|
||||
approvalKind: "exec",
|
||||
}),
|
||||
).toEqual({ authorized: true });
|
||||
|
||||
expect(
|
||||
slackApprovalAuth.authorizeActorAction({
|
||||
cfg,
|
||||
senderId: "U_ATTACKER",
|
||||
action: "approve",
|
||||
approvalKind: "exec",
|
||||
}),
|
||||
).toEqual({
|
||||
authorized: false,
|
||||
reason: "❌ You are not authorized to approve exec requests on Slack.",
|
||||
});
|
||||
});
|
||||
});
|
||||
33
extensions/slack/src/approval-auth.ts
Normal file
33
extensions/slack/src/approval-auth.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
createResolvedApproverActionAuthAdapter,
|
||||
resolveApprovalApprovers,
|
||||
} from "openclaw/plugin-sdk/approval-runtime";
|
||||
import { resolveSlackAccount } from "./accounts.js";
|
||||
import { parseSlackTarget } from "./targets.js";
|
||||
|
||||
function normalizeSlackApproverId(value: string | number): string | undefined {
|
||||
const trimmed = String(value).trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const target = parseSlackTarget(trimmed, { defaultKind: "user" });
|
||||
return target?.kind === "user" ? target.id : undefined;
|
||||
} catch {
|
||||
return /^[A-Z0-9]+$/i.test(trimmed) ? trimmed : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export const slackApprovalAuth = createResolvedApproverActionAuthAdapter({
|
||||
channelLabel: "Slack",
|
||||
resolveApprovers: ({ cfg, accountId }) => {
|
||||
const account = resolveSlackAccount({ cfg, accountId }).config;
|
||||
return resolveApprovalApprovers({
|
||||
allowFrom: account.allowFrom,
|
||||
extraAllowFrom: account.dm?.allowFrom,
|
||||
defaultTo: account.defaultTo,
|
||||
normalizeApprover: normalizeSlackApproverId,
|
||||
});
|
||||
},
|
||||
normalizeSenderId: (value) => normalizeSlackApproverId(value),
|
||||
});
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
} from "./accounts.js";
|
||||
import type { SlackActionContext } from "./action-runtime.js";
|
||||
import { resolveSlackAutoThreadId } from "./action-threading.js";
|
||||
import { slackApprovalAuth } from "./approval-auth.js";
|
||||
import { parseSlackBlocksInput } from "./blocks-input.js";
|
||||
import { createSlackActions } from "./channel-actions.js";
|
||||
import { resolveSlackChannelType } from "./channel-type.js";
|
||||
@@ -281,6 +282,7 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount, SlackProbe> = crea
|
||||
}),
|
||||
resolveNames: resolveSlackAllowlistNames,
|
||||
},
|
||||
auth: slackApprovalAuth,
|
||||
groups: {
|
||||
resolveRequireMention: resolveSlackGroupRequireMention,
|
||||
resolveToolPolicy: resolveSlackGroupToolPolicy,
|
||||
|
||||
17
extensions/synology-chat/src/approval-auth.test.ts
Normal file
17
extensions/synology-chat/src/approval-auth.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { synologyChatApprovalAuth } from "./approval-auth.js";
|
||||
|
||||
describe("synologyChatApprovalAuth", () => {
|
||||
it("authorizes numeric Synology Chat user ids", () => {
|
||||
const cfg = { channels: { "synology-chat": { allowedUserIds: ["123"] } } };
|
||||
|
||||
expect(
|
||||
synologyChatApprovalAuth.authorizeActorAction({
|
||||
cfg,
|
||||
senderId: "123",
|
||||
action: "approve",
|
||||
approvalKind: "plugin",
|
||||
}),
|
||||
).toEqual({ authorized: true });
|
||||
});
|
||||
});
|
||||
23
extensions/synology-chat/src/approval-auth.ts
Normal file
23
extensions/synology-chat/src/approval-auth.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/account-resolution";
|
||||
import {
|
||||
createResolvedApproverActionAuthAdapter,
|
||||
resolveApprovalApprovers,
|
||||
} from "openclaw/plugin-sdk/approval-runtime";
|
||||
import { resolveAccount } from "./accounts.js";
|
||||
|
||||
function normalizeSynologyChatApproverId(value: string | number): string | undefined {
|
||||
const trimmed = String(value).trim();
|
||||
return /^\d+$/.test(trimmed) ? trimmed : undefined;
|
||||
}
|
||||
|
||||
export const synologyChatApprovalAuth = createResolvedApproverActionAuthAdapter({
|
||||
channelLabel: "Synology Chat",
|
||||
resolveApprovers: ({ cfg, accountId }) => {
|
||||
const account = resolveAccount((cfg ?? {}) as OpenClawConfig, accountId);
|
||||
return resolveApprovalApprovers({
|
||||
allowFrom: account.allowedUserIds,
|
||||
normalizeApprover: normalizeSynologyChatApproverId,
|
||||
});
|
||||
},
|
||||
normalizeSenderId: (value) => normalizeSynologyChatApproverId(value),
|
||||
});
|
||||
@@ -21,6 +21,7 @@ import { createChatChannelPlugin, type ChannelPlugin } from "openclaw/plugin-sdk
|
||||
import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
||||
import { listAccountIds, resolveAccount } from "./accounts.js";
|
||||
import { synologyChatApprovalAuth } from "./approval-auth.js";
|
||||
import { sendMessage, sendFileUrl } from "./client.js";
|
||||
import { SynologyChatChannelConfigSchema } from "./config-schema.js";
|
||||
import {
|
||||
@@ -224,6 +225,7 @@ export function createSynologyChatPlugin(): SynologyChatPlugin {
|
||||
config: {
|
||||
...synologyChatConfigAdapter,
|
||||
},
|
||||
auth: synologyChatApprovalAuth,
|
||||
messaging: {
|
||||
normalizeTarget: (target: string) => {
|
||||
const trimmed = target.trim();
|
||||
|
||||
24
extensions/whatsapp/src/approval-auth.test.ts
Normal file
24
extensions/whatsapp/src/approval-auth.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { whatsappApprovalAuth } from "./approval-auth.js";
|
||||
|
||||
describe("whatsappApprovalAuth", () => {
|
||||
it("authorizes direct WhatsApp recipients and ignores groups", () => {
|
||||
expect(
|
||||
whatsappApprovalAuth.authorizeActorAction({
|
||||
cfg: { channels: { whatsapp: { allowFrom: ["+1 (555) 123-0000"] } } },
|
||||
senderId: "15551230000@s.whatsapp.net",
|
||||
action: "approve",
|
||||
approvalKind: "exec",
|
||||
}),
|
||||
).toEqual({ authorized: true });
|
||||
|
||||
expect(
|
||||
whatsappApprovalAuth.authorizeActorAction({
|
||||
cfg: { channels: { whatsapp: { allowFrom: ["12345-67890@g.us"] } } },
|
||||
senderId: "+15551239999",
|
||||
action: "approve",
|
||||
approvalKind: "exec",
|
||||
}),
|
||||
).toEqual({ authorized: true });
|
||||
});
|
||||
});
|
||||
27
extensions/whatsapp/src/approval-auth.ts
Normal file
27
extensions/whatsapp/src/approval-auth.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
createResolvedApproverActionAuthAdapter,
|
||||
resolveApprovalApprovers,
|
||||
} from "openclaw/plugin-sdk/approval-runtime";
|
||||
import { resolveWhatsAppAccount } from "./accounts.js";
|
||||
import { normalizeWhatsAppTarget } from "./runtime-api.js";
|
||||
|
||||
function normalizeWhatsAppApproverId(value: string | number): string | undefined {
|
||||
const normalized = normalizeWhatsAppTarget(String(value));
|
||||
if (!normalized || normalized.endsWith("@g.us")) {
|
||||
return undefined;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export const whatsappApprovalAuth = createResolvedApproverActionAuthAdapter({
|
||||
channelLabel: "WhatsApp",
|
||||
resolveApprovers: ({ cfg, accountId }) => {
|
||||
const account = resolveWhatsAppAccount({ cfg, accountId });
|
||||
return resolveApprovalApprovers({
|
||||
allowFrom: account.allowFrom,
|
||||
defaultTo: account.defaultTo,
|
||||
normalizeApprover: normalizeWhatsAppApproverId,
|
||||
});
|
||||
},
|
||||
normalizeSenderId: (value) => normalizeWhatsAppApproverId(value),
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
// WhatsApp-specific imports from local extension code (moved from src/web/ and src/channels/plugins/)
|
||||
import { resolveWhatsAppAccount, type ResolvedWhatsAppAccount } from "./accounts.js";
|
||||
import { createWhatsAppLoginTool } from "./agent-tools-login.js";
|
||||
import { whatsappApprovalAuth } from "./approval-auth.js";
|
||||
import type { WebChannelStatus } from "./auto-reply/types.js";
|
||||
import {
|
||||
listWhatsAppDirectoryGroupsFromConfig,
|
||||
@@ -207,6 +208,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> =
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
...whatsappApprovalAuth,
|
||||
login: async ({ cfg, accountId, runtime, verbose }) => {
|
||||
const resolvedAccountId =
|
||||
accountId?.trim() ||
|
||||
|
||||
17
extensions/zalo/src/approval-auth.test.ts
Normal file
17
extensions/zalo/src/approval-auth.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { zaloApprovalAuth } from "./approval-auth.js";
|
||||
|
||||
describe("zaloApprovalAuth", () => {
|
||||
it("authorizes numeric Zalo user ids", () => {
|
||||
const cfg = { channels: { zalo: { allowFrom: ["zl:123"] } } };
|
||||
|
||||
expect(
|
||||
zaloApprovalAuth.authorizeActorAction({
|
||||
cfg,
|
||||
senderId: "123",
|
||||
action: "approve",
|
||||
approvalKind: "exec",
|
||||
}),
|
||||
).toEqual({ authorized: true });
|
||||
});
|
||||
});
|
||||
25
extensions/zalo/src/approval-auth.ts
Normal file
25
extensions/zalo/src/approval-auth.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
createResolvedApproverActionAuthAdapter,
|
||||
resolveApprovalApprovers,
|
||||
} from "openclaw/plugin-sdk/approval-runtime";
|
||||
import { resolveZaloAccount } from "./accounts.js";
|
||||
|
||||
function normalizeZaloApproverId(value: string | number): string | undefined {
|
||||
const normalized = String(value)
|
||||
.trim()
|
||||
.replace(/^(zalo|zl):/i, "")
|
||||
.trim();
|
||||
return /^\d+$/.test(normalized) ? normalized : undefined;
|
||||
}
|
||||
|
||||
export const zaloApprovalAuth = createResolvedApproverActionAuthAdapter({
|
||||
channelLabel: "Zalo",
|
||||
resolveApprovers: ({ cfg, accountId }) => {
|
||||
const account = resolveZaloAccount({ cfg, accountId }).config;
|
||||
return resolveApprovalApprovers({
|
||||
allowFrom: account.allowFrom,
|
||||
normalizeApprover: normalizeZaloApproverId,
|
||||
});
|
||||
},
|
||||
normalizeSenderId: (value) => normalizeZaloApproverId(value),
|
||||
});
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
type ResolvedZaloAccount,
|
||||
} from "./accounts.js";
|
||||
import { zaloMessageActions } from "./actions.js";
|
||||
import { zaloApprovalAuth } from "./approval-auth.js";
|
||||
import { ZaloConfigSchema } from "./config-schema.js";
|
||||
import type { ZaloProbeResult } from "./probe.js";
|
||||
import {
|
||||
@@ -181,6 +182,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount, ZaloProbeResult> =
|
||||
},
|
||||
}),
|
||||
},
|
||||
auth: zaloApprovalAuth,
|
||||
groups: {
|
||||
resolveRequireMention: () => true,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user