slack-integration.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import loggerFactory from '~/utils/logger';
  2. const express = require('express');
  3. const mongoose = require('mongoose');
  4. const urljoin = require('url-join');
  5. const { verifySlackRequest, generateWebClient, getSupportedGrowiActionsRegExps } = require('@growi/slack');
  6. const logger = loggerFactory('growi:routes:apiv3:slack-integration');
  7. const router = express.Router();
  8. const SlackAppIntegration = mongoose.model('SlackAppIntegration');
  9. const { respondIfSlackbotError } = require('../../service/slack-command-handler/respond-if-slackbot-error');
  10. module.exports = (crowi) => {
  11. this.app = crowi.express;
  12. const { configManager } = crowi;
  13. // Check if the access token is correct
  14. async function verifyAccessTokenFromProxy(req, res, next) {
  15. const tokenPtoG = req.headers['x-growi-ptog-tokens'];
  16. if (tokenPtoG == null) {
  17. const message = 'The value of header \'x-growi-ptog-tokens\' must not be empty.';
  18. logger.warn(message, { body: req.body });
  19. return res.status(400).send({ message });
  20. }
  21. const slackAppIntegrationCount = await SlackAppIntegration.countDocuments({ tokenPtoG });
  22. logger.debug('verifyAccessTokenFromProxy', {
  23. tokenPtoG,
  24. slackAppIntegrationCount,
  25. });
  26. if (slackAppIntegrationCount === 0) {
  27. return res.status(403).send({
  28. message: 'The access token that identifies the request source is slackbot-proxy is invalid. Did you setup with `/growi register`.\n'
  29. + 'Or did you delete registration for GROWI ? if so, the link with GROWI has been disconnected. '
  30. + 'Please unregister the information registered in the proxy and setup `/growi register` again.',
  31. });
  32. }
  33. next();
  34. }
  35. async function checkCommandPermission(req, res, next) {
  36. let payload;
  37. if (req.body.payload) {
  38. payload = JSON.parse(req.body.payload);
  39. }
  40. if (req.body.text == null && !payload) { // when /relation-test
  41. return next();
  42. }
  43. const tokenPtoG = req.headers['x-growi-ptog-tokens'];
  44. const relation = await SlackAppIntegration.findOne({ tokenPtoG });
  45. // MOCK DATA DELETE THIS GW-6972 ---------------
  46. const SlackAppIntegrationMock = mongoose.model('SlackAppIntegrationMock');
  47. const slackAppIntegrationMock = await SlackAppIntegrationMock.findOne({ tokenPtoG });
  48. const channelsObject = slackAppIntegrationMock.permittedChannelsForEachCommand._doc.channelsObject;
  49. // MOCK DATA DELETE THIS GW-6972 ---------------
  50. const { supportedCommandsForBroadcastUse, supportedCommandsForSingleUse } = relation;
  51. const supportedCommands = supportedCommandsForBroadcastUse.concat(supportedCommandsForSingleUse);
  52. const supportedGrowiActionsRegExps = getSupportedGrowiActionsRegExps(supportedCommands);
  53. // get command name from req.body
  54. let command = '';
  55. let actionId = '';
  56. let callbackId = '';
  57. if (!payload) { // when request is to /commands
  58. command = req.body.text.split(' ')[0];
  59. }
  60. else if (payload.actions) { // when request is to /interactions && block_actions
  61. actionId = payload.actions[0].action_id;
  62. }
  63. else { // when request is to /interactions && view_submission
  64. callbackId = payload.view.callback_id;
  65. }
  66. // code below checks permission at channel level
  67. const fromChannel = req.body.channel_name || payload.channel.name;
  68. [...channelsObject.keys()].forEach((commandName) => {
  69. const permittedChannels = channelsObject.get(commandName);
  70. // ex. search OR search:hogehoge
  71. const commandRegExp = new RegExp(`(^${commandName}$)|(^${commandName}:\\w+)`);
  72. // RegExp check
  73. if (commandRegExp.test(commandName) || commandRegExp.test(actionId) || commandRegExp.test(callbackId)) {
  74. // check if the channel is permitted
  75. if (permittedChannels.includes(fromChannel)) return next();
  76. }
  77. });
  78. // code below checks permission at command level
  79. let isActionSupported = false;
  80. supportedGrowiActionsRegExps.forEach((regexp) => {
  81. if (regexp.test(actionId) || regexp.test(callbackId)) {
  82. isActionSupported = true;
  83. }
  84. });
  85. // validate
  86. if (command && !supportedCommands.includes(command)) {
  87. return res.status(403).send(`It is not allowed to run '${command}' command to this GROWI.`);
  88. }
  89. if ((actionId || callbackId) && !isActionSupported) {
  90. return res.status(403).send(`It is not allowed to run '${command}' command to this GROWI.`);
  91. }
  92. next();
  93. }
  94. const addSigningSecretToReq = (req, res, next) => {
  95. req.slackSigningSecret = configManager.getConfig('crowi', 'slackbot:signingSecret');
  96. return next();
  97. };
  98. const generateClientForResponse = (tokenGtoP) => {
  99. const currentBotType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
  100. if (currentBotType == null) {
  101. throw new Error('The config \'SLACK_BOT_TYPE\'(ns: \'crowi\', key: \'slackbot:currentBotType\') must be set.');
  102. }
  103. let token;
  104. // connect directly
  105. if (tokenGtoP == null) {
  106. token = crowi.configManager.getConfig('crowi', 'slackbot:token');
  107. return generateWebClient(token);
  108. }
  109. // connect to proxy
  110. const proxyServerUri = crowi.configManager.getConfig('crowi', 'slackbot:proxyServerUri');
  111. const serverUri = urljoin(proxyServerUri, '/g2s');
  112. const headers = {
  113. 'x-growi-gtop-tokens': tokenGtoP,
  114. };
  115. return generateWebClient(token, serverUri, headers);
  116. };
  117. async function handleCommands(req, res) {
  118. const { body } = req;
  119. if (body.text == null) {
  120. return 'No text.';
  121. }
  122. /*
  123. * TODO: use parseSlashCommand
  124. */
  125. // Send response immediately to avoid opelation_timeout error
  126. // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
  127. res.send();
  128. const tokenPtoG = req.headers['x-growi-ptog-tokens'];
  129. // generate client
  130. let client;
  131. if (tokenPtoG == null) {
  132. client = generateClientForResponse();
  133. }
  134. else {
  135. const slackAppIntegration = await SlackAppIntegration.findOne({ tokenPtoG });
  136. client = generateClientForResponse(slackAppIntegration.tokenGtoP);
  137. }
  138. const args = body.text.split(' ');
  139. const command = args[0];
  140. try {
  141. await crowi.slackBotService.handleCommandRequest(command, client, body, args);
  142. }
  143. catch (err) {
  144. await respondIfSlackbotError(client, body, err);
  145. }
  146. }
  147. router.post('/commands', addSigningSecretToReq, verifySlackRequest, async(req, res) => {
  148. return handleCommands(req, res);
  149. });
  150. router.post('/proxied/commands', verifyAccessTokenFromProxy, checkCommandPermission, async(req, res) => {
  151. const { body } = req;
  152. // eslint-disable-next-line max-len
  153. // see: https://api.slack.com/apis/connections/events-api#the-events-api__subscribing-to-event-types__events-api-request-urls__request-url-configuration--verification
  154. if (body.type === 'url_verification') {
  155. return res.send({ challenge: body.challenge });
  156. }
  157. return handleCommands(req, res);
  158. });
  159. async function handleInteractions(req, res) {
  160. // Send response immediately to avoid opelation_timeout error
  161. // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
  162. res.send();
  163. const tokenPtoG = req.headers['x-growi-ptog-tokens'];
  164. // generate client
  165. let client;
  166. if (tokenPtoG == null) {
  167. client = generateClientForResponse();
  168. }
  169. else {
  170. const slackAppIntegration = await SlackAppIntegration.findOne({ tokenPtoG });
  171. client = generateClientForResponse(slackAppIntegration.tokenGtoP);
  172. }
  173. const payload = JSON.parse(req.body.payload);
  174. const { type } = payload;
  175. try {
  176. switch (type) {
  177. case 'block_actions':
  178. try {
  179. await crowi.slackBotService.handleBlockActionsRequest(client, payload);
  180. }
  181. catch (err) {
  182. await respondIfSlackbotError(client, req.body, err);
  183. }
  184. break;
  185. case 'view_submission':
  186. try {
  187. await crowi.slackBotService.handleViewSubmissionRequest(client, payload);
  188. }
  189. catch (err) {
  190. await respondIfSlackbotError(client, req.body, err);
  191. }
  192. break;
  193. default:
  194. break;
  195. }
  196. }
  197. catch (error) {
  198. logger.error(error);
  199. }
  200. }
  201. router.post('/interactions', addSigningSecretToReq, verifySlackRequest, async(req, res) => {
  202. return handleInteractions(req, res);
  203. });
  204. router.post('/proxied/interactions', verifyAccessTokenFromProxy, checkCommandPermission, async(req, res) => {
  205. return handleInteractions(req, res);
  206. });
  207. router.get('/supported-commands', verifyAccessTokenFromProxy, async(req, res) => {
  208. const tokenPtoG = req.headers['x-growi-ptog-tokens'];
  209. const slackAppIntegration = await SlackAppIntegration.findOne({ tokenPtoG });
  210. return res.send(slackAppIntegration);
  211. });
  212. return router;
  213. };