Просмотр исходного кода

reorganize apiv3 endpoints for slack integration

Yuki Takei 5 лет назад
Родитель
Сommit
5fa5617253

+ 1 - 0
packages/slack/package.json

@@ -22,6 +22,7 @@
   },
   "devDependencies": {
     "@slack/bolt": "^3.3.0",
+    "@types/express": "^4.17.11",
     "@types/jest": "^26.0.22",
     "@typescript-eslint/eslint-plugin": "^4.18.0",
     "@typescript-eslint/parser": "^4.18.0",

+ 2 - 1
packages/slack/src/index.ts

@@ -8,8 +8,9 @@ export const supportedGrowiCommands: string[] = [
 ];
 
 export * from './interfaces/growi-command';
+export * from './interfaces/request-from-slack';
 export * from './models/errors';
-export * from './middlewares/verification-slack-request';
+export * from './middlewares/verify-slack-request';
 export * from './utils/block-creater';
 export * from './utils/slash-command-parser';
 export * from './utils/webclient-factory';

+ 9 - 0
packages/slack/src/interfaces/request-from-slack.ts

@@ -0,0 +1,9 @@
+import { Request } from 'express';
+
+export type RequestFromSlack = Request & {
+  // appended by slack
+  headers:{'x-slack-signature'?:string, 'x-slack-request-timestamp':number},
+
+  // appended by GROWI or slackbot-proxy
+  slackSigningSecret?:string,
+};

+ 13 - 15
packages/slack/src/middlewares/verification-slack-request.ts → packages/slack/src/middlewares/verify-slack-request.ts

@@ -1,19 +1,18 @@
 import { createHmac, timingSafeEqual } from 'crypto';
 import { stringify } from 'qs';
-import { Request, Response, NextFunction } from 'express';
+import { Response, NextFunction } from 'express';
+
+import { RequestFromSlack } from '../interfaces/request-from-slack';
+
 /**
-   * Verify if the request came from slack
-   * See: https://api.slack.com/authentication/verifying-requests-from-slack
-   */
-
-type signingSecretType = {
-  signingSecret?:string; headers:{'x-slack-signature'?:string, 'x-slack-request-timestamp':number}
-}
-
-// eslint-disable-next-line max-len
-export const verificationSlackRequest = (req : Request & signingSecretType, res:Response, next:NextFunction):Record<string, any>| void => {
-  if (req.signingSecret == null) {
-    return res.send('No signing secret.');
+ * Verify if the request came from slack
+ * See: https://api.slack.com/authentication/verifying-requests-from-slack
+ */
+export const verifySlackRequest = (req: RequestFromSlack, res: Response, next: NextFunction): Record<string, any> | void => {
+  const signingSecret = req.slackSigningSecret;
+
+  if (signingSecret == null) {
+    return res.status(400).send({ message: 'No signing secret.' });
   }
 
   // take out slackSignature and timestamp from header
@@ -32,7 +31,7 @@ export const verificationSlackRequest = (req : Request & signingSecretType, res:
 
   // generate growi signature
   const sigBaseString = `v0:${timestamp}:${stringify(req.body, { format: 'RFC1738' })}`;
-  const hasher = createHmac('sha256', req.signingSecret);
+  const hasher = createHmac('sha256', signingSecret);
   hasher.update(sigBaseString, 'utf8');
   const hashedSigningSecret = hasher.digest('hex');
   const growiSignature = `v0=${hashedSigningSecret}`;
@@ -40,7 +39,6 @@ export const verificationSlackRequest = (req : Request & signingSecretType, res:
   // compare growiSignature and slackSignature
   if (timingSafeEqual(Buffer.from(growiSignature, 'utf8'), Buffer.from(slackSignature, 'utf8'))) {
     return next();
-
   }
 
   return res.send('Verification failed.');

+ 8 - 2
packages/slackbot-proxy/src/controllers/slack.ts

@@ -120,7 +120,7 @@ export class SlackCtrl {
 
     const promises = relations.map((relation: Relation) => {
       // generate API URL
-      const url = new URL('/_api/v3/slack-integration/commands', relation.growiUri);
+      const url = new URL('/_api/v3/slack-integration/proxied/commands', relation.growiUri);
       return axios.post(url.toString(), {
         ...body,
         tokenPtoG: relation.tokenPtoG,
@@ -145,7 +145,13 @@ export class SlackCtrl {
           user: body.user_id,
           blocks: [
             generateMarkdownSectionBlock('*Error occured:*'),
-            ...rejectedResults.map(result => generateMarkdownSectionBlock(result.reason.toString())),
+            ...rejectedResults.map((rejectedResult) => {
+              const reason = rejectedResult.reason.toString();
+              const resData = rejectedResult.reason.response?.data;
+              const resDataMessage = resData?.message || resData.toString();
+              const errorMessage = `${reason} (${resDataMessage})`;
+              return generateMarkdownSectionBlock(errorMessage);
+            }),
           ],
         });
       }

+ 42 - 25
src/server/routes/apiv3/slack-integration.js

@@ -2,46 +2,39 @@ const express = require('express');
 
 const loggerFactory = require('@alias/logger');
 
-const logger = loggerFactory('growi:routes:apiv3:slack-integration');
+const { verifySlackRequest } = require('@growi/slack');
 
+const logger = loggerFactory('growi:routes:apiv3:slack-integration');
 const router = express.Router();
-const { verificationSlackRequest } = require('@growi/slack');
 
 module.exports = (crowi) => {
   this.app = crowi.express;
 
+  const { configManager } = crowi;
+
   // Check if the access token is correct
-  function verificationAccessToken(req, res, next) {
-    const botType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
-    if (botType === 'customBotWithoutProxy') {
-      return next();
-    }
-    const slackBotAccessToken = req.body.slack_bot_access_token || null;
+  function verifyAccessTokenFromProxy(req, res, next) {
+    const { body } = req;
+    const { tokenPtoG } = body;
 
-    if (slackBotAccessToken == null || slackBotAccessToken !== this.crowi.configManager.getConfig('crowi', 'slackbot:access-token')) {
-      logger.error('slack_bot_access_token is invalid.');
-      return res.send('*Access token is inValid*');
-    }
+    const correctToken = configManager.getConfig('crowi', 'slackbot:access-token');
 
-    return next();
-  }
+    logger.debug('verifyAccessTokenFromProxy', {
+      tokenPtoG,
+      correctToken,
+    });
 
-  function verificationRequestUrl(req, res, next) {
-    // for verification request URL on Event Subscriptions
-    if (req.body.type === 'url_verification') {
-      return res.send(req.body);
+    if (tokenPtoG == null || tokenPtoG !== correctToken) {
+      return res.status(403).send({ message: 'The access token that identifies the request source is slackbot-proxy is invalid.' });
     }
-
-    return next();
   }
 
-  const addSlackBotSigningSecret = (req, res, next) => {
-    req.signingSecret = crowi.configManager.getConfig('crowi', 'slackbot:signingSecret');
+  const addSlackBotSigningSecretToReq = (req, res, next) => {
+    req.slackSigningSecret = configManager.getConfig('crowi', 'slackbot:signingSecret');
     return next();
   };
 
-  router.post('/commands', verificationRequestUrl, addSlackBotSigningSecret, verificationSlackRequest, verificationAccessToken, async(req, res) => {
-
+  async function handleCommands(req, res) {
     // Send response immediately to avoid opelation_timeout error
     // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
     res.send();
@@ -67,8 +60,25 @@ module.exports = (crowi) => {
       logger.error(error);
       return res.send(error.message);
     }
+  }
+
+  router.post('/commands', addSlackBotSigningSecretToReq, verifySlackRequest, async(req, res) => {
+    return handleCommands(req, res);
   });
 
+  router.post('/proxied/commands', verifyAccessTokenFromProxy, async(req, res) => {
+    const { body } = req;
+
+    // eslint-disable-next-line max-len
+    // see: https://api.slack.com/apis/connections/events-api#the-events-api__subscribing-to-event-types__events-api-request-urls__request-url-configuration--verification
+    if (body.type === 'url_verification') {
+      return body.challenge;
+    }
+
+    return handleCommands(req, res);
+  });
+
+
   const handleBlockActions = async(payload) => {
     const { action_id: actionId } = payload.actions[0];
 
@@ -102,7 +112,7 @@ module.exports = (crowi) => {
     }
   };
 
-  router.post('/interactions', verificationRequestUrl, addSlackBotSigningSecret, verificationSlackRequest, async(req, res) => {
+  async function handleInteractions(req, res) {
 
     // Send response immediately to avoid opelation_timeout error
     // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
@@ -128,8 +138,15 @@ module.exports = (crowi) => {
       return res.send(error.message);
     }
 
+  }
+
+  router.post('/interactions', addSlackBotSigningSecretToReq, verifySlackRequest, async(req, res) => {
+    return handleInteractions(req, res);
   });
 
+  router.post('/proxied/interactions', verifyAccessTokenFromProxy, async(req, res) => {
+    return handleInteractions(req, res);
+  });
 
   return router;
 };