fix(slack): normalize approval user ids (#84671)

* fix(slack): normalize approval user ids

* chore(openrouter): satisfy spread fallback lint

* fix(ci): unblock status and secret-file checks
This commit is contained in:
Kevin Lin
2026-05-20 13:40:14 -07:00
committed by GitHub
parent 404fd6d9ab
commit 9e4eca00ff
5 changed files with 107 additions and 9 deletions

View File

@@ -23,6 +23,15 @@ describe("slackApprovalAuth", () => {
}),
).toEqual({ authorized: true });
expect(
slackApprovalAuth.authorizeActorAction({
cfg,
senderId: "u123owner",
action: "approve",
approvalKind: "plugin",
}),
).toEqual({ authorized: true });
expect(
slackApprovalAuth.authorizeActorAction({
cfg,
@@ -32,6 +41,15 @@ describe("slackApprovalAuth", () => {
}),
).toEqual({ authorized: true });
expect(
slackApprovalAuth.authorizeActorAction({
cfg,
senderId: "u345default",
action: "approve",
approvalKind: "plugin",
}),
).toEqual({ authorized: true });
expect(
slackApprovalAuth.authorizeActorAction({
cfg,
@@ -56,4 +74,26 @@ describe("slackApprovalAuth", () => {
reason: "❌ You are not authorized to approve exec requests on Slack.",
});
});
it("canonicalizes configured plugin approver ids before matching uppercase senders", () => {
const cfg = {
channels: {
slack: {
allowFrom: ["slack:u123owner"],
defaultTo: "user:u345default",
},
},
};
for (const senderId of ["U123OWNER", "U345DEFAULT"]) {
expect(
slackApprovalAuth.authorizeActorAction({
cfg,
senderId,
action: "approve",
approvalKind: "plugin",
}),
).toEqual({ authorized: true });
}
});
});

View File

@@ -69,9 +69,25 @@ describe("slack exec approvals", () => {
expect(getSlackExecApprovalApprovers({ cfg })).toEqual(["U456"]);
expect(isSlackExecApprovalApprover({ cfg, senderId: "U456" })).toBe(true);
expect(isSlackExecApprovalApprover({ cfg, senderId: "u456" })).toBe(true);
expect(isSlackExecApprovalApprover({ cfg, senderId: "U123" })).toBe(false);
});
it("canonicalizes configured exec approver ids before matching uppercase senders", () => {
const explicitCfg = buildConfig({ approvers: ["u456"] });
expect(getSlackExecApprovalApprovers({ cfg: explicitCfg })).toEqual(["U456"]);
expect(isSlackExecApprovalApprover({ cfg: explicitCfg, senderId: "U456" })).toBe(true);
const ownerFallbackCfg = {
...buildConfig({ enabled: true }),
commands: { ownerAllowFrom: ["slack:u123owner"] },
} as OpenClawConfig;
expect(getSlackExecApprovalApprovers({ cfg: ownerFallbackCfg })).toEqual(["U123OWNER"]);
expect(isSlackExecApprovalApprover({ cfg: ownerFallbackCfg, senderId: "U123OWNER" })).toBe(
true,
);
});
it("does not infer approvers from allowFrom or DM default routes", () => {
const cfg = buildConfig(
{ enabled: true },
@@ -115,7 +131,7 @@ describe("slack exec approvals", () => {
enabled: true,
mode: "targets",
targets: [
{ channel: "slack", to: "user:U123TARGET" },
{ channel: "slack", to: "user:u123target" },
{ channel: "slack", to: "channel:C123" },
],
},
@@ -123,8 +139,10 @@ describe("slack exec approvals", () => {
} as OpenClawConfig;
expect(isSlackExecApprovalTargetRecipient({ cfg, senderId: "U123TARGET" })).toBe(true);
expect(isSlackExecApprovalTargetRecipient({ cfg, senderId: "u123target" })).toBe(true);
expect(isSlackExecApprovalTargetRecipient({ cfg, senderId: "U999OTHER" })).toBe(false);
expect(isSlackExecApprovalAuthorizedSender({ cfg, senderId: "U123TARGET" })).toBe(true);
expect(isSlackExecApprovalAuthorizedSender({ cfg, senderId: "u123target" })).toBe(true);
});
it("keeps the local Slack approval prompt path active", () => {
@@ -154,7 +172,15 @@ describe("slack exec approvals", () => {
it("normalizes wrapped sender ids", () => {
expect(normalizeSlackApproverId("user:U123OWNER")).toBe("U123OWNER");
expect(normalizeSlackApproverId("user:u123owner")).toBe("U123OWNER");
expect(normalizeSlackApproverId("slack:u123owner")).toBe("U123OWNER");
expect(normalizeSlackApproverId("<@U123OWNER>")).toBe("U123OWNER");
expect(normalizeSlackApproverId("<@u123owner>")).toBe("U123OWNER");
expect(normalizeSlackApproverId("u123owner")).toBe("U123OWNER");
expect(normalizeSlackApproverId("C123CHANNEL")).toBeUndefined();
expect(normalizeSlackApproverId("slack:C123CHANNEL")).toBeUndefined();
expect(normalizeSlackApproverId("user:C123CHANNEL")).toBeUndefined();
expect(normalizeSlackApproverId("<@C123CHANNEL>")).toBeUndefined();
});
it("applies agent and session filters to request handling", () => {

View File

@@ -8,6 +8,11 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { normalizeStringifiedOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveSlackAccount } from "./accounts.js";
function normalizeSlackUserLikeId(value: string): string | undefined {
const upper = value.toUpperCase();
return /^[UW][A-Z0-9]+$/.test(upper) ? upper : undefined;
}
export function normalizeSlackApproverId(value: string | number): string | undefined {
const trimmed = normalizeStringifiedOptionalString(value);
if (!trimmed) {
@@ -15,13 +20,13 @@ export function normalizeSlackApproverId(value: string | number): string | undef
}
const prefixed = trimmed.match(/^(?:slack|user):([A-Z0-9]+)$/i);
if (prefixed?.[1]) {
return prefixed[1];
return normalizeSlackUserLikeId(prefixed[1]);
}
const mention = trimmed.match(/^<@([A-Z0-9]+)>$/i);
if (mention?.[1]) {
return mention[1];
return normalizeSlackUserLikeId(mention[1]);
}
return /^[UW][A-Z0-9]+$/i.test(trimmed) ? trimmed : undefined;
return normalizeSlackUserLikeId(trimmed);
}
function resolveSlackOwnerApprovers(cfg: OpenClawConfig): string[] {

View File

@@ -934,7 +934,20 @@ describe("registerSlackInteractionEvents", () => {
});
it("resolves exec approvals from shared interactive Slack actions", async () => {
const { ctx, app, getHandler } = createContext({ allowFrom: ["U999"] });
const { ctx, app, getHandler } = createContext({
allowFrom: ["U999"],
cfg: {
channels: {
slack: {
execApprovals: {
enabled: true,
approvers: ["u123"],
target: "both",
},
},
},
},
});
registerSlackInteractionEvents({ ctx: ctx as never });
const handler = getHandler();
@@ -997,7 +1010,7 @@ describe("registerSlackInteractionEvents", () => {
slack: {
accounts: {
default: {
allowFrom: ["U123OWNER"],
allowFrom: ["u123owner"],
execApprovals: {
enabled: true,
approvers: ["U999EXEC"],
@@ -1071,7 +1084,7 @@ describe("registerSlackInteractionEvents", () => {
slack: {
accounts: {
default: {
allowFrom: ["U123OWNER"],
allowFrom: ["u123owner"],
execApprovals: {
enabled: true,
approvers: ["U999EXEC"],

View File

@@ -1,5 +1,8 @@
import "./fs-safe-defaults.js";
import { readSecretFileSync as readSecretFileSyncImpl } from "@openclaw/fs-safe/secret";
import {
readSecretFileSync as readSecretFileSyncImpl,
tryReadSecretFileSync as tryReadSecretFileSyncImpl,
} from "@openclaw/fs-safe/secret";
import { resolveUserPath } from "../utils.js";
export {
@@ -7,11 +10,22 @@ export {
PRIVATE_SECRET_DIR_MODE,
PRIVATE_SECRET_FILE_MODE,
readSecretFileSync,
tryReadSecretFileSync,
type SecretFileReadOptions,
} from "@openclaw/fs-safe/secret";
export { writeSecretFileAtomic as writePrivateSecretFileAtomic } from "@openclaw/fs-safe/secret";
export function tryReadSecretFileSync(
filePath: string | undefined,
label: string,
options: Parameters<typeof tryReadSecretFileSyncImpl>[2] = {},
): string | undefined {
try {
return tryReadSecretFileSyncImpl(filePath, label, options);
} catch {
return undefined;
}
}
export type SecretFileReadResult =
| {
ok: true;