mirror of https://github.com/openclaw/openclaw.git
refactor: add approval auth capabilities to more channels
This commit is contained in:
parent
63cbc097b5
commit
c2cbdea28c
|
|
@ -43,6 +43,7 @@ Most channel plugins do not need approval-specific code.
|
|||
- Use `outbound.shouldSuppressLocalPayloadPrompt` or `outbound.beforeDeliverPayload` for channel-specific payload lifecycle behavior such as hiding duplicate local approval prompts or sending typing indicators before delivery.
|
||||
- Use `approvals.delivery` only for native approval routing or fallback suppression.
|
||||
- Use `approvals.render` only when a channel truly needs custom approval payloads instead of the shared renderer.
|
||||
- If a channel can infer stable owner-like DM identities from existing config, use `createResolvedApproverActionAuthAdapter` from `openclaw/plugin-sdk/approval-runtime` to restrict same-chat `/approve` without adding approval-specific core logic.
|
||||
|
||||
For Slack, Matrix, Microsoft Teams, and similar chat channels, the default path is usually enough: core handles approvals and the plugin just exposes normal outbound and auth capabilities.
|
||||
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
@ -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() ||
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { createResolvedApproverActionAuthAdapter } from "./approval-auth-helpers.js";
|
||||
|
||||
describe("createResolvedApproverActionAuthAdapter", () => {
|
||||
it("falls back to generic same-chat auth when no approvers resolve", () => {
|
||||
const auth = createResolvedApproverActionAuthAdapter({
|
||||
channelLabel: "Slack",
|
||||
resolveApprovers: () => [],
|
||||
});
|
||||
|
||||
expect(
|
||||
auth.authorizeActorAction({
|
||||
cfg: {},
|
||||
senderId: "U_OWNER",
|
||||
action: "approve",
|
||||
approvalKind: "exec",
|
||||
}),
|
||||
).toEqual({ authorized: true });
|
||||
});
|
||||
|
||||
it("allows matching normalized approvers and rejects others", () => {
|
||||
const auth = createResolvedApproverActionAuthAdapter({
|
||||
channelLabel: "Signal",
|
||||
resolveApprovers: () => ["uuid:owner"],
|
||||
normalizeSenderId: (value) => value.trim().toLowerCase(),
|
||||
});
|
||||
|
||||
expect(
|
||||
auth.authorizeActorAction({
|
||||
cfg: {},
|
||||
senderId: " UUID:OWNER ",
|
||||
action: "approve",
|
||||
approvalKind: "plugin",
|
||||
}),
|
||||
).toEqual({ authorized: true });
|
||||
|
||||
expect(
|
||||
auth.authorizeActorAction({
|
||||
cfg: {},
|
||||
senderId: "uuid:attacker",
|
||||
action: "approve",
|
||||
approvalKind: "plugin",
|
||||
}),
|
||||
).toEqual({
|
||||
authorized: false,
|
||||
reason: "❌ You are not authorized to approve plugin requests on Signal.",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import type { OpenClawConfig } from "./config-runtime.js";
|
||||
|
||||
type ApprovalKind = "exec" | "plugin";
|
||||
|
||||
function defaultNormalizeSenderId(value: string): string | undefined {
|
||||
const trimmed = value.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
export function createResolvedApproverActionAuthAdapter(params: {
|
||||
channelLabel: string;
|
||||
resolveApprovers: (params: { cfg: OpenClawConfig; accountId?: string | null }) => string[];
|
||||
normalizeSenderId?: (value: string) => string | undefined;
|
||||
}) {
|
||||
const normalizeSenderId = params.normalizeSenderId ?? defaultNormalizeSenderId;
|
||||
|
||||
return {
|
||||
authorizeActorAction({
|
||||
cfg,
|
||||
accountId,
|
||||
senderId,
|
||||
approvalKind,
|
||||
}: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
senderId?: string | null;
|
||||
action: "approve";
|
||||
approvalKind: ApprovalKind;
|
||||
}) {
|
||||
const approvers = params.resolveApprovers({ cfg, accountId });
|
||||
if (approvers.length === 0) {
|
||||
return { authorized: true } as const;
|
||||
}
|
||||
const normalizedSenderId = senderId ? normalizeSenderId(senderId) : undefined;
|
||||
if (normalizedSenderId && approvers.includes(normalizedSenderId)) {
|
||||
return { authorized: true } as const;
|
||||
}
|
||||
return {
|
||||
authorized: false,
|
||||
reason: `❌ You are not authorized to approve ${approvalKind} requests on ${params.channelLabel}.`,
|
||||
} as const;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ export {
|
|||
type PluginApprovalRequestPayload,
|
||||
type PluginApprovalResolved,
|
||||
} from "../infra/plugin-approvals.js";
|
||||
export { createResolvedApproverActionAuthAdapter } from "./approval-auth-helpers.js";
|
||||
export { createApproverRestrictedNativeApprovalAdapter } from "./approval-delivery-helpers.js";
|
||||
export { resolveApprovalApprovers } from "./approval-approvers.js";
|
||||
export {
|
||||
|
|
|
|||
Loading…
Reference in New Issue