소스 검색

Merge pull request #4163 from weseek/feat/7017-restrict-request-from-slack-on-slack-proxy-side

Feat/7017 restrict request from slack on slack proxy side
Sizma yosimaz 4 년 전
부모
커밋
2a9a06d578

+ 5 - 3
packages/app/src/server/routes/apiv3/slack-integration-settings.js

@@ -57,6 +57,7 @@ module.exports = (crowi) => {
   const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
 
   const SlackAppIntegration = mongoose.model('SlackAppIntegration');
+  const SlackAppIntegrationMock = mongoose.model('SlackAppIntegrationMock');
 
   const validator = {
     botType: [
@@ -626,7 +627,8 @@ module.exports = (crowi) => {
     const { id } = req.params;
     let slackBotToken;
     try {
-      const slackAppIntegration = await SlackAppIntegration.findOne({ _id: id });
+      const slackAppIntegration = await SlackAppIntegrationMock.findOne({ _id: id });
+      // const slackAppIntegration = await SlackAppIntegration.findOne({ _id: id });
       if (slackAppIntegration == null) {
         const msg = 'Could not find SlackAppIntegration by id';
         return res.apiv3Err(new ErrorV3(msg, 'find-slackAppIntegration-failed'), 400);
@@ -637,8 +639,8 @@ module.exports = (crowi) => {
         'post',
         '/g2s/relation-test',
         {
-          supportedCommandsForBroadcastUse: slackAppIntegration.supportedCommandsForBroadcastUse,
-          supportedCommandsForSingleUse: slackAppIntegration.supportedCommandsForSingleUse,
+          permissionsForBroadcastUseCommands: slackAppIntegration.permissionsForBroadcastUseCommands,
+          permissionsForSingleUseCommands: slackAppIntegration.permissionsForSingleUseCommands,
         },
       );
 

+ 4 - 5
packages/app/src/server/routes/apiv3/slack-integration.js

@@ -9,6 +9,7 @@ const { verifySlackRequest, generateWebClient, getSupportedGrowiActionsRegExps }
 const logger = loggerFactory('growi:routes:apiv3:slack-integration');
 const router = express.Router();
 const SlackAppIntegration = mongoose.model('SlackAppIntegration');
+const SlackAppIntegrationMock = mongoose.model('SlackAppIntegrationMock');
 const { respondIfSlackbotError } = require('../../service/slack-command-handler/respond-if-slackbot-error');
 
 module.exports = (crowi) => {
@@ -26,7 +27,7 @@ module.exports = (crowi) => {
       return res.status(400).send({ message });
     }
 
-    const slackAppIntegrationCount = await SlackAppIntegration.countDocuments({ tokenPtoG });
+    const slackAppIntegrationCount = await SlackAppIntegrationMock.countDocuments({ tokenPtoG });
 
     logger.debug('verifyAccessTokenFromProxy', {
       tokenPtoG,
@@ -55,14 +56,14 @@ module.exports = (crowi) => {
 
     const tokenPtoG = req.headers['x-growi-ptog-tokens'];
 
-    const relation = await SlackAppIntegration.findOne({ tokenPtoG });
+    // const relation = await SlackAppIntegration.findOne({ tokenPtoG });
     // MOCK DATA DELETE THIS GW-6972 ---------------
     const SlackAppIntegrationMock = mongoose.model('SlackAppIntegrationMock');
     const slackAppIntegrationMock = await SlackAppIntegrationMock.findOne({ tokenPtoG });
     const permissionsForBroadcastUseCommands = slackAppIntegrationMock.permissionsForBroadcastUseCommands;
     const permissionsForSingleUseCommands = slackAppIntegrationMock.permissionsForSingleUseCommands;
     // MOCK DATA DELETE THIS GW-6972 ---------------
-    const { supportedCommandsForBroadcastUse, supportedCommandsForSingleUse } = relation;
+    // const { supportedCommandsForBroadcastUse, supportedCommandsForSingleUse } = relation;
 
     // get command name from req.body
     let command = '';
@@ -136,7 +137,6 @@ module.exports = (crowi) => {
       text: 'Processing your request ...',
     });
 
-
     const args = body.text.split(' ');
     const command = args[0];
 
@@ -156,7 +156,6 @@ module.exports = (crowi) => {
 
   router.post('/proxied/commands', verifyAccessTokenFromProxy, checkCommandPermission, 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') {

+ 1 - 1
packages/app/src/server/service/slack-command-handler/create.js

@@ -34,7 +34,7 @@ module.exports = (crowi) => {
           inputSectionBlock('path', 'Path', 'path_input', false, '/path'),
           inputSectionBlock('contents', 'Contents', 'contents_input', true, 'Input with Markdown...'),
         ],
-        private_metadata: JSON.stringify({ channelId: body.channel_id }),
+        private_metadata: JSON.stringify({ channelId: body.channel_id, channelName: body.channel_name }),
       },
     });
   };

+ 1 - 1
packages/app/src/server/service/slack-command-handler/search.js

@@ -258,7 +258,7 @@ module.exports = (crowi) => {
           },
           accessory: {
             type: 'button',
-            action_id: 'shareSingleSearchResult',
+            action_id: 'search:shareSinglePageResult',
             text: {
               type: 'plain_text',
               text: 'Share',

+ 3 - 2
packages/app/src/server/service/slack-integration.ts

@@ -128,9 +128,10 @@ export class SlackIntegrationService implements S2sMessageHandlable {
   async generateClientByTokenPtoG(tokenPtoG: string): Promise<WebClient> {
     this.isCheckTypeValid();
 
-    const SlackAppIntegration = mongoose.model('SlackAppIntegration');
+    // const SlackAppIntegration = mongoose.model('SlackAppIntegration');
+    const SlackAppIntegrationMock = mongoose.model('SlackAppIntegrationMock');
 
-    const slackAppIntegration = await SlackAppIntegration.findOne({ tokenPtoG });
+    const slackAppIntegration = await SlackAppIntegrationMock.findOne({ tokenPtoG });
 
     if (slackAppIntegration == null) {
       throw new Error('No SlackAppIntegration exists that corresponds to the tokenPtoG specified.');

+ 16 - 12
packages/slackbot-proxy/src/controllers/growi-to-slack.ts

@@ -15,7 +15,8 @@ import { WebclientRes, AddWebclientResponseToRes } from '~/middlewares/growi-to-
 
 import { GrowiReq } from '~/interfaces/growi-to-slack/growi-req';
 import { InstallationRepository } from '~/repositories/installation';
-import { RelationRepository } from '~/repositories/relation';
+// import { RelationRepository } from '~/repositories/relation';
+import { RelationMockRepository } from '~/repositories/relation-mock';
 import { OrderRepository } from '~/repositories/order';
 
 import { InstallerService } from '~/services/InstallerService';
@@ -36,8 +37,11 @@ export class GrowiToSlackCtrl {
   @Inject()
   installationRepository: InstallationRepository;
 
+  // @Inject()
+  // relationRepository: RelationRepository;
+
   @Inject()
-  relationRepository: RelationRepository;
+  relationMockRepository: RelationMockRepository;
 
   @Inject()
   orderRepository: OrderRepository;
@@ -72,7 +76,7 @@ export class GrowiToSlackCtrl {
     const { tokenGtoPs } = req;
 
     // retrieve Relation with Installation
-    const relations = await this.relationRepository.createQueryBuilder('relation')
+    const relations = await this.relationMockRepository.createQueryBuilder('relation')
       .where('relation.tokenGtoP IN (:...tokens)', { tokens: tokenGtoPs })
       .leftJoinAndSelect('relation.installation', 'installation')
       .getMany();
@@ -98,14 +102,14 @@ export class GrowiToSlackCtrl {
   async putSupportedCommands(@Req() req: GrowiReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
     // asserted (tokenGtoPs.length > 0) by verifyGrowiToSlackRequest
     const { tokenGtoPs } = req;
-    const { supportedCommandsForBroadcastUse, supportedCommandsForSingleUse } = req.body;
+    const { permissionsForBroadcastUseCommands, permissionsForSingleUseCommands } = req.body;
 
     if (tokenGtoPs.length !== 1) {
       throw createError(400, 'installation is invalid');
     }
 
     const tokenGtoP = tokenGtoPs[0];
-    const relation = await this.relationRepository.update({ tokenGtoP }, { supportedCommandsForBroadcastUse, supportedCommandsForSingleUse });
+    const relation = await this.relationMockRepository.update({ tokenGtoP }, { permissionsForBroadcastUseCommands, permissionsForSingleUseCommands });
 
     return res.send({ relation });
   }
@@ -122,7 +126,7 @@ export class GrowiToSlackCtrl {
     const tokenGtoP = tokenGtoPs[0];
 
     // retrieve relation with Installation
-    const relation = await this.relationRepository.createQueryBuilder('relation')
+    const relation = await this.relationMockRepository.createQueryBuilder('relation')
       .where('tokenGtoP = :token', { token: tokenGtoP })
       .leftJoinAndSelect('relation.installation', 'installation')
       .getOne();
@@ -190,26 +194,26 @@ export class GrowiToSlackCtrl {
     const expiredAtCommands = addHours(new Date(), 48);
 
     // Transaction is not considered because it is used infrequently,
-    const response = await this.relationRepository.createQueryBuilder('relation')
+    const response = await this.relationMockRepository.createQueryBuilder('relation')
       .insert()
       .values({
         installation: order.installation,
         tokenGtoP: order.tokenGtoP,
         tokenPtoG: order.tokenPtoG,
         growiUri: order.growiUrl,
-        supportedCommandsForBroadcastUse: req.body.supportedCommandsForBroadcastUse,
-        supportedCommandsForSingleUse: req.body.supportedCommandsForSingleUse,
+        permissionsForBroadcastUseCommands: req.body.permissionsForBroadcastUseCommands,
+        permissionsForSingleUseCommands: req.body.permissionsForSingleUseCommands,
         expiredAtCommands,
       })
       // https://github.com/typeorm/typeorm/issues/1090#issuecomment-634391487
       .orUpdate({
         conflict_target: ['installation', 'growiUri'],
-        overwrite: ['tokenGtoP', 'tokenPtoG', 'supportedCommandsForBroadcastUse', 'supportedCommandsForSingleUse'],
+        overwrite: ['tokenGtoP', 'tokenPtoG', 'permissionsForBroadcastUseCommands', 'permissionsForSingleUseCommands'],
       })
       .execute();
 
     // Find the generated relation
-    const generatedRelation = await this.relationRepository.findOne({ id: response.identifiers[0].id });
+    const generatedRelation = await this.relationMockRepository.findOne({ id: response.identifiers[0].id });
 
     return res.send({ relation: generatedRelation, slackBotToken: token });
   }
@@ -258,7 +262,7 @@ export class GrowiToSlackCtrl {
     const tokenGtoP = tokenGtoPs[0];
 
     // retrieve relation with Installation
-    const relation = await this.relationRepository.createQueryBuilder('relation')
+    const relation = await this.relationMockRepository.createQueryBuilder('relation')
       .where('tokenGtoP = :token', { token: tokenGtoP })
       .leftJoinAndSelect('relation.installation', 'installation')
       .getOne();

+ 110 - 69
packages/slackbot-proxy/src/controllers/slack.ts

@@ -4,7 +4,7 @@ import {
 
 import axios from 'axios';
 
-import { WebAPICallResult } from '@slack/web-api';
+import { WebAPICallResult, WebClient } from '@slack/web-api';
 import { Installation } from '@slack/oauth';
 
 
@@ -13,10 +13,12 @@ import {
   InvalidGrowiCommandError, requiredScopes, postWelcomeMessage, REQUEST_TIMEOUT_FOR_PTOG,
 } from '@growi/slack';
 
-import { Relation } from '~/entities/relation';
+// import { Relation } from '~/entities/relation';
+import { RelationMock } from '~/entities/relation-mock';
 import { SlackOauthReq } from '~/interfaces/slack-to-growi/slack-oauth-req';
 import { InstallationRepository } from '~/repositories/installation';
-import { RelationRepository } from '~/repositories/relation';
+// import { RelationRepository } from '~/repositories/relation';
+import { RelationMockRepository } from '~/repositories/relation-mock';
 import { OrderRepository } from '~/repositories/order';
 import { AddSigningSecretToReq } from '~/middlewares/slack-to-growi/add-signing-secret-to-req';
 import {
@@ -36,7 +38,33 @@ import { JoinToConversationMiddleware } from '~/middlewares/slack-to-growi/join-
 
 const logger = loggerFactory('slackbot-proxy:controllers:slack');
 
-
+const postNotAllowedMessage = async(client:WebClient, body:any, disallowedGrowiUrls:Set<string>, commandName:string):Promise<void> => {
+
+  const linkUrlList = Array.from(disallowedGrowiUrls).map((growiUrl) => {
+    return '\n'
+      + `• ${new URL('/admin/slack-integration', growiUrl).toString()}`;
+  });
+
+  const growiDocsLink = 'https://docs.growi.org/en/admin-guide/upgrading/43x.html';
+
+  await client.chat.postEphemeral({
+    text: 'Error occured.',
+    channel: body.channel_id,
+    user: body.user_id,
+    blocks: [
+      markdownSectionBlock('*None of GROWI permitted the command.*'),
+      markdownSectionBlock(`*'${commandName}'* command was not allowed.`),
+      markdownSectionBlock(
+        `To use this command, modify settings from following pages: ${linkUrlList}`,
+      ),
+      markdownSectionBlock(
+        `Or, if your GROWI version is 4.3.0 or below, upgrade GROWI to use commands and permission settings: ${growiDocsLink}`,
+      ),
+    ],
+  });
+
+  return;
+};
 @Controller('/slack')
 export class SlackCtrl {
 
@@ -46,8 +74,11 @@ export class SlackCtrl {
   @Inject()
   installationRepository: InstallationRepository;
 
+  // @Inject()
+  // relationRepository: RelationRepository;
+
   @Inject()
-  relationRepository: RelationRepository;
+  relationMockRepository: RelationMockRepository;
 
   @Inject()
   orderRepository: OrderRepository;
@@ -71,13 +102,12 @@ export class SlackCtrl {
    * @param body
    * @returns
    */
-  private async sendCommand(growiCommand: GrowiCommand, relations: Relation[], body: any) {
+  private async sendCommand(growiCommand: GrowiCommand, relations: RelationMock[], body: any) {
     if (relations.length === 0) {
       throw new Error('relations must be set');
     }
     const botToken = relations[0].installation?.data.bot?.token; // relations[0] should be exist
-
-    const promises = relations.map((relation: Relation) => {
+    const promises = relations.map((relation: RelationMock) => {
       // generate API URL
       const url = new URL('/_api/v3/slack-integration/proxied/commands', relation.growiUri);
       return axios.post(url.toString(), {
@@ -104,6 +134,7 @@ export class SlackCtrl {
     }
   }
 
+
   @Post('/commands')
   @UseBefore(AddSigningSecretToReq, verifySlackRequest, AuthorizeCommandMiddleware, JoinToConversationMiddleware)
   async handleCommand(@Req() req: SlackOauthReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
@@ -147,7 +178,7 @@ export class SlackCtrl {
     const installationId = authorizeResult.enterpriseId || authorizeResult.teamId;
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     const installation = await this.installationRepository.findByTeamIdOrEnterpriseId(installationId!);
-    const relations = await this.relationRepository.createQueryBuilder('relation')
+    const relations = await this.relationMockRepository.createQueryBuilder('relation')
       .where('relation.installationId = :id', { id: installation?.id })
       .leftJoinAndSelect('relation.installation', 'installation')
       .getMany();
@@ -180,20 +211,20 @@ export class SlackCtrl {
 
     const baseDate = new Date();
 
-    const allowedRelationsForSingleUse:Relation[] = [];
-    const allowedRelationsForBroadcastUse:Relation[] = [];
+    const allowedRelationsForSingleUse:RelationMock[] = [];
+    const allowedRelationsForBroadcastUse:RelationMock[] = [];
     const disallowedGrowiUrls: Set<string> = new Set();
 
     // check permission
     await Promise.all(relations.map(async(relation) => {
-      const isSupportedForSingleUse = await this.relationsService.isSupportedGrowiCommandForSingleUse(
-        relation, growiCommand.growiCommandType, baseDate,
+      const isSupportedForSingleUse = await this.relationsService.isPermissionsForSingleUseCommands(
+        relation, growiCommand.growiCommandType, body.channel_name, baseDate,
       );
 
       let isSupportedForBroadcastUse = false;
       if (!isSupportedForSingleUse) {
-        isSupportedForBroadcastUse = await this.relationsService.isSupportedGrowiCommandForBroadcastUse(
-          relation, growiCommand.growiCommandType, baseDate,
+        isSupportedForBroadcastUse = await this.relationsService.isPermissionsUseBroadcastCommands(
+          relation, growiCommand.growiCommandType, body.channel_name, baseDate,
         );
       }
 
@@ -212,29 +243,7 @@ export class SlackCtrl {
     if (relations.length === disallowedGrowiUrls.size) {
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       const client = generateWebClient(authorizeResult.botToken!);
-
-      const linkUrlList = Array.from(disallowedGrowiUrls).map((growiUrl) => {
-        return '\n'
-          + `• ${new URL('/admin/slack-integration', growiUrl).toString()}`;
-      });
-
-      const growiDocsLink = 'https://docs.growi.org/en/admin-guide/upgrading/43x.html';
-
-      return client.chat.postEphemeral({
-        text: 'Error occured.',
-        channel: body.channel_id,
-        user: body.user_id,
-        blocks: [
-          markdownSectionBlock('*None of GROWI permitted the command.*'),
-          markdownSectionBlock(`*'${growiCommand.growiCommandType}'* command was not allowed.`),
-          markdownSectionBlock(
-            `To use this command, modify settings from following pages: ${linkUrlList}`,
-          ),
-          markdownSectionBlock(
-            `Or, if your GROWI version is 4.3.0 or below, upgrade GROWI to use commands and permission settings: ${growiDocsLink}`,
-          ),
-        ],
-      });
+      return postNotAllowedMessage(client, body, disallowedGrowiUrls, growiCommand.growiCommandType);
     }
 
     // select GROWI
@@ -249,6 +258,7 @@ export class SlackCtrl {
     }
   }
 
+
   @Post('/interactions')
   @UseBefore(AuthorizeInteractionMiddleware, ExtractGrowiUriFromReq)
   async handleInteraction(@Req() req: SlackOauthReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
@@ -262,15 +272,14 @@ export class SlackCtrl {
       return;
     }
 
+    const payload:any = JSON.parse(body.payload);
+    const callbackId:string = payload?.view?.callback_id;
     const installationId = authorizeResult.enterpriseId || authorizeResult.teamId;
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     const installation = await this.installationRepository.findByTeamIdOrEnterpriseId(installationId!);
 
-    const payload = JSON.parse(body.payload);
-    const callBackId = payload?.view?.callback_id;
-
     // register
-    if (callBackId === 'register') {
+    if (callbackId === 'register') {
       try {
         await this.registerService.insertOrderRecord(installation, authorizeResult.botToken, payload);
       }
@@ -287,13 +296,21 @@ export class SlackCtrl {
     }
 
     // unregister
-    if (callBackId === 'unregister') {
+    if (callbackId === 'unregister') {
       await this.unregisterService.unregister(installation, authorizeResult, payload);
       return;
     }
 
+    let privateMeta:any;
+
+    if (payload.view != null) {
+      privateMeta = JSON.parse(payload?.view?.private_metadata);
+    }
+
+    const channelName = payload.channel?.name || privateMeta?.body?.channel_name || privateMeta?.channelName;
+
     // forward to GROWI server
-    if (callBackId === 'select_growi') {
+    if (callbackId === 'select_growi') {
       // 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();
@@ -302,35 +319,59 @@ export class SlackCtrl {
       return this.sendCommand(selectedGrowiInformation.growiCommand, [selectedGrowiInformation.relation], selectedGrowiInformation.sendCommandBody);
     }
 
-    // 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();
-
-    /*
-    * forward to GROWI server
-    */
-    const relation = await this.relationRepository.findOne({ installation, growiUri: req.growiUri });
-
-    if (relation == null) {
-      logger.error('*No relation found.*');
-      return;
-    }
+    // check permission
+    const relations = await this.relationMockRepository.createQueryBuilder('relation')
+      .where('relation.installationId = :id', { id: installation?.id })
+      .leftJoinAndSelect('relation.installation', 'installation')
+      .getMany();
 
-    try {
-      // generate API URL
-      const url = new URL('/_api/v3/slack-integration/proxied/interactions', req.growiUri);
-      await axios.post(url.toString(), {
-        ...body,
-      }, {
-        headers: {
-          'x-growi-ptog-tokens': relation.tokenPtoG,
-        },
-        timeout: REQUEST_TIMEOUT_FOR_PTOG,
+    if (relations.length === 0) {
+      return res.json({
+        blocks: [
+          markdownSectionBlock('*No relation found.*'),
+          markdownSectionBlock('Run `/growi register` first.'),
+        ],
       });
     }
-    catch (err) {
-      logger.error(err);
+
+
+    const actionId:string = payload?.actions?.[0].action_id;
+    await Promise.all(relations.map(async(relation) => {
+      await this.relationsService.checkPermissionForInteractions(relation, channelName, callbackId, actionId);
+    }));
+
+
+    const disallowedGrowiUrls = this.relationsService.getDisallowedGrowiUrls();
+    const commandName = this.relationsService.getCommandName();
+
+    if (relations.length === disallowedGrowiUrls.size) {
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      const client = generateWebClient(authorizeResult.botToken!);
+      return postNotAllowedMessage(client, body, disallowedGrowiUrls, commandName);
     }
+
+    /*
+     * forward to GROWI server
+     */
+
+    const allowedRelations = this.relationsService.getAllowedRelations();
+    allowedRelations.map(async(relation) => {
+      try {
+        // generate API URL
+        const url = new URL('/_api/v3/slack-integration/proxied/interactions', relation.growiUri);
+        await axios.post(url.toString(), {
+          ...body,
+        }, {
+          headers: {
+            'x-growi-ptog-tokens': relation.tokenPtoG,
+          },
+        });
+      }
+      catch (err) {
+        logger.error(err);
+      }
+    });
+
   }
 
   @Post('/events')

+ 10 - 0
packages/slackbot-proxy/src/repositories/relation-mock.ts

@@ -0,0 +1,10 @@
+import {
+  Repository, EntityRepository,
+} from 'typeorm';
+
+import { RelationMock } from '~/entities/relation-mock';
+
+@EntityRepository(RelationMock)
+export class RelationMockRepository extends Repository<RelationMock> {
+
+}

+ 110 - 15
packages/slackbot-proxy/src/services/RelationsService.ts

@@ -3,10 +3,11 @@ import { Inject, Service } from '@tsed/di';
 import axios from 'axios';
 import { addHours } from 'date-fns';
 
+// import { Relation } from '~/entities/relation';
 import { REQUEST_TIMEOUT_FOR_PTOG } from '@growi/slack';
-
-import { Relation } from '~/entities/relation';
-import { RelationRepository } from '~/repositories/relation';
+import { RelationMock } from '~/entities/relation-mock';
+// import { RelationRepository } from '~/repositories/relation';
+import { RelationMockRepository } from '~/repositories/relation-mock';
 
 import loggerFactory from '~/utils/logger';
 
@@ -16,9 +17,11 @@ const logger = loggerFactory('slackbot-proxy:services:RelationsService');
 export class RelationsService {
 
   @Inject()
-  relationRepository: RelationRepository;
+  // relationRepository: RelationRepository;
+
+  relationMockRepository: RelationMockRepository;
 
-  async getSupportedGrowiCommands(relation:Relation):Promise<any> {
+  async getSupportedGrowiCommands(relation:RelationMock):Promise<any> {
     // generate API URL
     const url = new URL('/_api/v3/slack-integration/supported-commands', relation.growiUri);
     return axios.get(url.toString(), {
@@ -29,17 +32,17 @@ export class RelationsService {
     });
   }
 
-  async syncSupportedGrowiCommands(relation:Relation): Promise<Relation> {
+  async syncSupportedGrowiCommands(relation:RelationMock): Promise<RelationMock> {
     const res = await this.getSupportedGrowiCommands(relation);
-    const { supportedCommandsForBroadcastUse, supportedCommandsForSingleUse } = res.data;
-    relation.supportedCommandsForBroadcastUse = supportedCommandsForBroadcastUse;
-    relation.supportedCommandsForSingleUse = supportedCommandsForSingleUse;
+    const { permissionsForBroadcastUseCommands, permissionsForSingleUseCommands } = res.data;
+    relation.permissionsForBroadcastUseCommands = permissionsForBroadcastUseCommands;
+    relation.permissionsForSingleUseCommands = permissionsForSingleUseCommands;
     relation.expiredAtCommands = addHours(new Date(), 48);
 
-    return this.relationRepository.save(relation);
+    return this.relationMockRepository.save(relation);
   }
 
-  async syncRelation(relation:Relation, baseDate:Date):Promise<Relation|null> {
+  async syncRelation(relation:RelationMock, baseDate:Date):Promise<RelationMock|null> {
     const distanceMillisecondsToExpiredAt = relation.getDistanceInMillisecondsToExpiredAt(baseDate);
 
     if (distanceMillisecondsToExpiredAt < 0) {
@@ -65,20 +68,112 @@ export class RelationsService {
     return relation;
   }
 
-  async isSupportedGrowiCommandForSingleUse(relation:Relation, growiCommandType:string, baseDate:Date):Promise<boolean> {
+  async isPermissionsForSingleUseCommands(relation:RelationMock, growiCommandType:string, channelName:string, baseDate:Date):Promise<boolean> {
     const syncedRelation = await this.syncRelation(relation, baseDate);
     if (syncedRelation == null) {
       return false;
     }
-    return relation.supportedCommandsForSingleUse.includes(growiCommandType);
+
+    const permission = relation.permissionsForSingleUseCommands[growiCommandType];
+
+    if (permission == null) {
+      return false;
+    }
+
+    if (Array.isArray(permission)) {
+      return permission.includes(channelName);
+    }
+
+    return permission;
   }
 
-  async isSupportedGrowiCommandForBroadcastUse(relation:Relation, growiCommandType:string, baseDate:Date):Promise<boolean> {
+  async isPermissionsUseBroadcastCommands(relation:RelationMock, growiCommandType:string, channelName:string, baseDate:Date):Promise<boolean> {
     const syncedRelation = await this.syncRelation(relation, baseDate);
     if (syncedRelation == null) {
       return false;
     }
-    return relation.supportedCommandsForBroadcastUse.includes(growiCommandType);
+
+    const permission = relation.permissionsForBroadcastUseCommands[growiCommandType];
+
+    if (permission == null) {
+      return false;
+    }
+
+    if (Array.isArray(permission)) {
+      return permission.includes(channelName);
+    }
+
+    return permission;
+  }
+
+
+  allowedRelations:RelationMock[] = [];
+
+  getAllowedRelations():RelationMock[] {
+    return this.allowedRelations;
+  }
+
+  disallowedGrowiUrls: Set<string> = new Set();
+
+  getDisallowedGrowiUrls():Set<string> {
+    return this.disallowedGrowiUrls;
+  }
+
+  commandName:string;
+
+  getCommandName():string {
+    return this.commandName;
+  }
+
+
+  async checkPermissionForInteractions(
+      // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+      relation:RelationMock, channelName:string, callbackId:string, actionId:string,
+  ):Promise<void>/* Promise<{isPermittedForInteractions:boolean, commandName:string, allowedRelations:RelationMock[], disallowedGrowiUrls:Set<string>}> */ {
+
+
+    const singleUse = Object.keys(relation.permissionsForSingleUseCommands);
+    const broadCastUse = Object.keys(relation.permissionsForBroadcastUseCommands);
+    let permissionForInteractions:boolean|string[];
+    let isPermittedForInteractions!:boolean;
+
+    [...singleUse, ...broadCastUse].forEach(async(tempCommandName) => {
+
+      // ex. search OR search:handlerName
+      const commandRegExp = new RegExp(`(^${tempCommandName}$)|(^${tempCommandName}:\\w+)`);
+
+      // skip this forEach loop if the requested command is not in permissionsForBroadcastUseCommands and permissionsForSingleUseCommands
+      if (!commandRegExp.test(actionId) && !commandRegExp.test(callbackId)) {
+        return;
+      }
+
+      this.commandName = tempCommandName;
+
+      // case: singleUse
+      permissionForInteractions = relation.permissionsForSingleUseCommands[tempCommandName];
+
+      // case: broadcastUse
+      if (permissionForInteractions == null) {
+        permissionForInteractions = relation.permissionsForBroadcastUseCommands[tempCommandName];
+      }
+
+      if (permissionForInteractions === true) {
+        isPermittedForInteractions = true;
+        return;
+      }
+
+      // check permission at channel level
+      if (Array.isArray(permissionForInteractions)) {
+        isPermittedForInteractions = true;
+        return;
+      }
+    });
+
+    if (!isPermittedForInteractions) {
+      this.disallowedGrowiUrls.add(relation.growiUri);
+    }
+
+    this.allowedRelations.push(relation);
   }
 
 }

+ 12 - 6
packages/slackbot-proxy/src/services/SelectGrowiService.ts

@@ -5,12 +5,14 @@ import { AuthorizeResult } from '@slack/oauth';
 
 import { GrowiCommandProcessor } from '~/interfaces/slack-to-growi/growi-command-processor';
 import { Installation } from '~/entities/installation';
-import { Relation } from '~/entities/relation';
-import { RelationRepository } from '~/repositories/relation';
+// import { Relation } from '~/entities/relation';
+import { RelationMock } from '~/entities/relation-mock';
+// import { RelationRepository } from '~/repositories/relation';
+import { RelationMockRepository } from '~/repositories/relation-mock';
 
 
 export type SelectedGrowiInformation = {
-  relation: Relation,
+  relation: RelationMock,
   growiCommand: GrowiCommand,
   sendCommandBody: any,
 }
@@ -18,10 +20,14 @@ export type SelectedGrowiInformation = {
 @Service()
 export class SelectGrowiService implements GrowiCommandProcessor {
 
+  // @Inject()
+  // relationRepository: RelationRepository;
+
   @Inject()
-  relationRepository: RelationRepository;
+  relationMockRepository: RelationMockRepository;
 
-  async process(growiCommand: GrowiCommand, authorizeResult: AuthorizeResult, body: {[key:string]:string } & {growiUrisForSingleUse:string[]}): Promise<void> {
+  // eslint-disable-next-line max-len
+  async process(growiCommand: GrowiCommand | string, authorizeResult: AuthorizeResult, body: {[key:string]:string } & {growiUrisForSingleUse:string[]}): Promise<void> {
     const { botToken } = authorizeResult;
 
     if (botToken == null) {
@@ -93,7 +99,7 @@ export class SelectGrowiService implements GrowiCommandProcessor {
     // ovverride trigger_id
     sendCommandBody.trigger_id = triggerId;
 
-    const relation = await this.relationRepository.createQueryBuilder('relation')
+    const relation = await this.relationMockRepository.createQueryBuilder('relation')
       .where('relation.growiUri =:growiUri', { growiUri })
       .andWhere('relation.installationId = :id', { id: installation?.id })
       .leftJoinAndSelect('relation.installation', 'installation')