diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 37f27cc9..867ac0fa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,4 +25,5 @@ jobs: channel-id: ${{ secrets.SLACK_CHANNEL_ID }} mention-to-user: ${{ secrets.SLACK_MENTION_TO_USER }} mention-to-group: ${{ secrets.SLACK_MENTION_TO_GROUP }} + authorized-users: ${{ secrets.SLACK_AUTHORIZED_USERS }} timeout-minutes: 5 diff --git a/README.md b/README.md index b6c2301c..cf2bfe54 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ jobs: channel-id: ${{ secrets.SLACK_CHANNEL_ID }} mention-to-user: ${{ secrets.SLACK_MENTION_TO_USER }} mention-to-group: ${{ secrets.SLACK_MENTION_TO_GROUP }} + authorized-users: ${{ secrets.SLACK_AUTHORIZED_USERS }} timeout-minutes: 10 ``` @@ -44,6 +45,10 @@ jobs: - Optional. Slack user ID to mention. - `mention-to-group` - Optional. Slack group ID to mention. + - `authorized-users` + - Optional. Slack user IDs who are authorized to approve or reject. Comma separated. + - e.g., + - `authorized-users: xxxxxx,yyyyyy` - Set `timeout-minutes` - Set the time to wait for approval. diff --git a/action.yml b/action.yml index d89ad4a0..54156fdc 100644 --- a/action.yml +++ b/action.yml @@ -21,6 +21,9 @@ inputs: mention-to-group: description: "Slack group ID to mention" required: false + authorized-users: + description: "Slack user IDs who are authorized to approve or reject" + required: false branding: icon: plus diff --git a/dist/index.js b/dist/index.js index 14831b07..46bab95e 100644 --- a/dist/index.js +++ b/dist/index.js @@ -24933,7 +24933,7 @@ RedirectableRequest.prototype._processResponse = function (response) { redirectUrl.protocol !== "https:" || redirectUrl.host !== currentHost && !isSubdomain(redirectUrl.host, currentHost)) { - removeMatchingHeaders(/^(?:authorization|cookie)$/i, this._options.headers); + removeMatchingHeaders(/^(?:(?:proxy-)?authorization|cookie)$/i, this._options.headers); } // Evaluate the beforeRedirect callback @@ -76792,6 +76792,26 @@ try { } catch (er) {} +/***/ }), + +/***/ 69042: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.Inputs = void 0; +exports.Inputs = { + BotToken: "bot-token", + SigningSecret: "signing-secret", + AppToken: "app-token", + ChannelId: "channel-id", + MentionToUser: "mention-to-user", + MentionToGroup: "mention-to-group", + AuthorizedUsers: "authorized-users", +}; + + /***/ }), /***/ 70885: @@ -76856,13 +76876,15 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.getInputs = void 0; const core = __importStar(__nccwpck_require__(42186)); const Option_1 = __nccwpck_require__(2569); +const constants_1 = __nccwpck_require__(69042); function getInputs() { - const botToken = getRequiredInput("bot-token"); - const signingSecret = getRequiredInput("signing-secret"); - const appToken = getRequiredInput("app-token"); - const channelId = getRequiredInput("channel-id"); - const mentionToUser = getOptionalInput("mention-to-user"); - const mentionToGroup = getOptionalInput("mention-to-group"); + const botToken = getRequiredInput(constants_1.Inputs.BotToken); + const signingSecret = getRequiredInput(constants_1.Inputs.SigningSecret); + const appToken = getRequiredInput(constants_1.Inputs.AppToken); + const channelId = getRequiredInput(constants_1.Inputs.ChannelId); + const mentionToUser = getOptionalInput(constants_1.Inputs.MentionToUser); + const mentionToGroup = getOptionalInput(constants_1.Inputs.MentionToGroup); + const authorizedUsers = getOptionalListInput(constants_1.Inputs.AuthorizedUsers); return { botToken, signingSecret, @@ -76870,6 +76892,7 @@ function getInputs() { channelId, mentionToUser, mentionToGroup, + authorizedUsers, }; } exports.getInputs = getInputs; @@ -76883,6 +76906,18 @@ function getOptionalInput(name) { } return (0, Option_1.some)(value); } +function getOptionalListInput(name) { + const value = core.getInput(name); + if (value === "") { + return Option_1.none; + } + const res = []; + const values = value.split(","); + for (const v of values) { + res.push(v.trim()); + } + return (0, Option_1.some)(res); +} /***/ }), @@ -77015,21 +77050,32 @@ function run(inputs, app) { }); }))(); app.action("slack-approval-approve", ({ ack, client, body, logger }) => __awaiter(this, void 0, void 0, function* () { - var _a, _b, _c; + var _a, _b; yield ack(); + const blockAction = body; + const userId = blockAction.user.id; + const ts = ((_a = blockAction.message) === null || _a === void 0 ? void 0 : _a.ts) || ""; + if (!isAuthorizedUser(userId, inputs.authorizedUsers)) { + yield client.chat.postMessage({ + channel: inputs.channelId, + thread_ts: ts, + text: `You are not authorized to approve this action: <@${userId}>`, + }); + return; + } try { - const response_blocks = (_a = body.message) === null || _a === void 0 ? void 0 : _a.blocks; + const response_blocks = (_b = blockAction.message) === null || _b === void 0 ? void 0 : _b.blocks; response_blocks.pop(); response_blocks.push({ type: "section", text: { type: "mrkdwn", - text: `Approved by <@${body.user.id}> `, + text: `Approved by <@${userId}> `, }, }); yield client.chat.update({ - channel: ((_b = body.channel) === null || _b === void 0 ? void 0 : _b.id) || "", - ts: ((_c = body.message) === null || _c === void 0 ? void 0 : _c.ts) || "", + channel: inputs.channelId, + ts: ts, blocks: response_blocks, }); } @@ -77039,21 +77085,32 @@ function run(inputs, app) { process.exit(0); })); app.action("slack-approval-reject", ({ ack, client, body, logger }) => __awaiter(this, void 0, void 0, function* () { - var _d, _e, _f; + var _c, _d; yield ack(); + const blockAction = body; + const userId = blockAction.user.id; + const ts = ((_c = blockAction.message) === null || _c === void 0 ? void 0 : _c.ts) || ""; + if (!isAuthorizedUser(userId, inputs.authorizedUsers)) { + yield client.chat.postMessage({ + channel: inputs.channelId, + thread_ts: ts, + text: `You are not authorized to reject this action: <@${userId}>`, + }); + return; + } try { - const response_blocks = (_d = body.message) === null || _d === void 0 ? void 0 : _d.blocks; + const response_blocks = (_d = blockAction.message) === null || _d === void 0 ? void 0 : _d.blocks; response_blocks.pop(); response_blocks.push({ type: "section", text: { type: "mrkdwn", - text: `Rejected by <@${body.user.id}>`, + text: `Rejected by <@${userId}>`, }, }); yield client.chat.update({ - channel: ((_e = body.channel) === null || _e === void 0 ? void 0 : _e.id) || "", - ts: ((_f = body.message) === null || _f === void 0 ? void 0 : _f.ts) || "", + channel: inputs.channelId, + ts: ts, blocks: response_blocks, }); } @@ -77073,6 +77130,12 @@ function run(inputs, app) { } }); } +function isAuthorizedUser(userId, authorizedUsers) { + if ((0, Option_1.isNone)(authorizedUsers)) { + return true; + } + return authorizedUsers.value.includes(userId); +} function main() { return __awaiter(this, void 0, void 0, function* () { const inputs = (0, input_helper_1.getInputs)(); diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 00000000..a40d829f --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,11 @@ +export const Inputs = { + BotToken: "bot-token", + SigningSecret: "signing-secret", + AppToken: "app-token", + ChannelId: "channel-id", + MentionToUser: "mention-to-user", + MentionToGroup: "mention-to-group", + AuthorizedUsers: "authorized-users", +} as const; + +export type Inputs = (typeof Inputs)[keyof typeof Inputs]; diff --git a/src/helper/input_helper.ts b/src/helper/input_helper.ts index 9466a68b..dcb10bee 100644 --- a/src/helper/input_helper.ts +++ b/src/helper/input_helper.ts @@ -1,5 +1,6 @@ import * as core from "@actions/core"; import { Option, none, some } from "fp-ts/lib/Option"; +import { Inputs } from "../constants"; export type SlackApprovalInputs = { botToken: string; @@ -8,15 +9,17 @@ export type SlackApprovalInputs = { channelId: string; mentionToUser: Option; mentionToGroup: Option; + authorizedUsers: Option; }; export function getInputs(): SlackApprovalInputs { - const botToken = getRequiredInput("bot-token"); - const signingSecret = getRequiredInput("signing-secret"); - const appToken = getRequiredInput("app-token"); - const channelId = getRequiredInput("channel-id"); - const mentionToUser = getOptionalInput("mention-to-user"); - const mentionToGroup = getOptionalInput("mention-to-group"); + const botToken = getRequiredInput(Inputs.BotToken); + const signingSecret = getRequiredInput(Inputs.SigningSecret); + const appToken = getRequiredInput(Inputs.AppToken); + const channelId = getRequiredInput(Inputs.ChannelId); + const mentionToUser = getOptionalInput(Inputs.MentionToUser); + const mentionToGroup = getOptionalInput(Inputs.MentionToGroup); + const authorizedUsers = getOptionalListInput(Inputs.AuthorizedUsers); return { botToken, @@ -25,14 +28,15 @@ export function getInputs(): SlackApprovalInputs { channelId, mentionToUser, mentionToGroup, + authorizedUsers, }; } -function getRequiredInput(name: string): string { +function getRequiredInput(name: Inputs): string { return core.getInput(name, { required: true }); } -function getOptionalInput(name: string): Option { +function getOptionalInput(name: Inputs): Option { const value = core.getInput(name); if (value === "") { return none; @@ -40,3 +44,16 @@ function getOptionalInput(name: string): Option { return some(value); } + +function getOptionalListInput(name: Inputs): Option { + const value = core.getInput(name); + if (value === "") { + return none; + } + const res: string[] = []; + const values = value.split(","); + for (const v of values) { + res.push(v.trim()); + } + return some(res); +} diff --git a/src/index.ts b/src/index.ts index e679bd5f..a4cf2110 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import * as core from "@actions/core"; import { App, BlockAction, LogLevel } from "@slack/bolt"; import { WebClient } from "@slack/web-api"; -import { isSome } from "fp-ts/lib/Option"; +import { Option, isNone, isSome } from "fp-ts/lib/Option"; import { getGitHubInfo } from "./helper/github_info_helper"; import { SlackApprovalInputs, getInputs } from "./helper/input_helper"; @@ -95,20 +95,34 @@ async function run(inputs: SlackApprovalInputs, app: App): Promise { "slack-approval-approve", async ({ ack, client, body, logger }) => { await ack(); + + const blockAction = body; + const userId = blockAction.user.id; + const ts = blockAction.message?.ts || ""; + + if (!isAuthorizedUser(userId, inputs.authorizedUsers)) { + await client.chat.postMessage({ + channel: inputs.channelId, + thread_ts: ts, + text: `You are not authorized to approve this action: <@${userId}>`, + }); + return; + } + try { - const response_blocks = (body).message?.blocks; + const response_blocks = blockAction.message?.blocks; response_blocks.pop(); response_blocks.push({ type: "section", text: { type: "mrkdwn", - text: `Approved by <@${body.user.id}> `, + text: `Approved by <@${userId}> `, }, }); await client.chat.update({ - channel: body.channel?.id || "", - ts: (body).message?.ts || "", + channel: inputs.channelId, + ts: ts, blocks: response_blocks, }); } catch (error) { @@ -123,20 +137,34 @@ async function run(inputs: SlackApprovalInputs, app: App): Promise { "slack-approval-reject", async ({ ack, client, body, logger }) => { await ack(); + + const blockAction = body; + const userId = blockAction.user.id; + const ts = blockAction.message?.ts || ""; + + if (!isAuthorizedUser(userId, inputs.authorizedUsers)) { + await client.chat.postMessage({ + channel: inputs.channelId, + thread_ts: ts, + text: `You are not authorized to reject this action: <@${userId}>`, + }); + return; + } + try { - const response_blocks = (body).message?.blocks; + const response_blocks = blockAction.message?.blocks; response_blocks.pop(); response_blocks.push({ type: "section", text: { type: "mrkdwn", - text: `Rejected by <@${body.user.id}>`, + text: `Rejected by <@${userId}>`, }, }); await client.chat.update({ - channel: body.channel?.id || "", - ts: (body).message?.ts || "", + channel: inputs.channelId, + ts: ts, blocks: response_blocks, }); } catch (error) { @@ -156,6 +184,17 @@ async function run(inputs: SlackApprovalInputs, app: App): Promise { } } +function isAuthorizedUser( + userId: string, + authorizedUsers: Option, +): boolean { + if (isNone(authorizedUsers)) { + return true; + } + + return authorizedUsers.value.includes(userId); +} + async function main() { const inputs = getInputs();