From fc672f39d44b94e58eae9f7165d479a355b2fbe0 Mon Sep 17 00:00:00 2001
From: FORESTIER Fabien <fabien.forestier@soprasteria.com>
Date: Wed, 27 Feb 2019 14:34:12 +0100
Subject: [PATCH] Use guards instead of middleware in order to restrict access
 by group

---
 src/app.module.ts                          | 23 ++++++++---------
 src/configuration/config.service.ts        |  2 +-
 src/configuration/config.ts                |  4 ++-
 src/decorators/groups.decorators.ts        |  3 +++
 src/email/email.controller.ts              |  2 ++
 src/guards/groups.guards.ts                | 27 ++++++++++++++++++++
 src/middlewares/email-writer.middleware.ts | 29 ----------------------
 swagger-spec.json                          |  2 +-
 tsconfig.json                              |  5 ++++
 9 files changed, 52 insertions(+), 45 deletions(-)
 create mode 100644 src/decorators/groups.decorators.ts
 create mode 100644 src/guards/groups.guards.ts
 delete mode 100644 src/middlewares/email-writer.middleware.ts

diff --git a/src/app.module.ts b/src/app.module.ts
index a3f2fd3..ec3e6b1 100644
--- a/src/app.module.ts
+++ b/src/app.module.ts
@@ -1,20 +1,17 @@
-import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
+import { Module } from '@nestjs/common';
 import { EmailModule } from './email/email.module';
 import { ConfigModule } from './configuration/config.module';
-import { EmailWriterMiddleware } from './middlewares/email-writer.middleware';
+import { APP_GUARD } from '@nestjs/core';
+import { GroupsGuard } from './guards/groups.guards';
 
 @Module({
   imports: [ConfigModule, EmailModule],
   controllers: [],
-  providers: [],
+  providers: [
+    {
+      provide: APP_GUARD,
+      useClass: GroupsGuard,
+    },
+  ],
 })
-export class AppModule {
-  configure(consumer: MiddlewareConsumer) {
-    // Applying the middleware that verify if there is the right group in header
-    // and takes the Authorization header jwt payload and put it in the request headers
-    consumer
-      .apply(EmailWriterMiddleware).forRoutes(
-        { path: 'email/send', method: RequestMethod.POST },
-      );
-  }
-}
+export class AppModule {}
diff --git a/src/configuration/config.service.ts b/src/configuration/config.service.ts
index 529e8bd..a1ac45a 100644
--- a/src/configuration/config.service.ts
+++ b/src/configuration/config.service.ts
@@ -21,7 +21,7 @@ export class ConfigService {
     this._config.rabbitMQ.user = process.env.RABBITMQ_USER;
     this._config.rabbitMQ.password = process.env.RABBITMQ_PASSWORD;
     this._config.plateformDataEmail = process.env.PLATEFORM_DATA_EMAIL;
-    this._config.emailWriterGroupName = process.env.EMAIL_WRITER_GROUP_NAME;
+    this._config.groupNames.emailWriter = process.env.EMAIL_WRITER_GROUP_NAME;
     this._config.groupHeader = process.env.GROUP_HEADER;
   }
 
diff --git a/src/configuration/config.ts b/src/configuration/config.ts
index 3cd92a1..9aa4d6e 100644
--- a/src/configuration/config.ts
+++ b/src/configuration/config.ts
@@ -8,6 +8,8 @@ export const config = {
   mailerQueue: 'portail-data-send-email',
   plateformDataEmail: '',
   imageHost: 'https://highway-to-data.alpha.grandlyon.com/email-template-assets',
-  emailWriterGroupName: '',
+  groupNames: {
+    emailWriter: '',
+  },
   groupHeader: '',
 };
\ No newline at end of file
diff --git a/src/decorators/groups.decorators.ts b/src/decorators/groups.decorators.ts
new file mode 100644
index 0000000..d0531b5
--- /dev/null
+++ b/src/decorators/groups.decorators.ts
@@ -0,0 +1,3 @@
+import { ReflectMetadata } from '@nestjs/common';
+
+export const Groups = (...roles: string[]) => ReflectMetadata('groups', roles);
\ No newline at end of file
diff --git a/src/email/email.controller.ts b/src/email/email.controller.ts
index a8aedee..405ea0f 100644
--- a/src/email/email.controller.ts
+++ b/src/email/email.controller.ts
@@ -2,6 +2,7 @@ import { Controller, Post, Body, InternalServerErrorException, Logger, HttpCode
 import { ContactForm, EmailWithoutFrom } from './email';
 import { EmailService } from './email.service';
 import { ApiBadRequestResponse, ApiOkResponse, ApiUseTags, ApiOperation, ApiInternalServerErrorResponse } from '@nestjs/swagger';
+import { Groups } from '../decorators/groups.decorators';
 
 @ApiUseTags('email')
 @Controller('email')
@@ -28,6 +29,7 @@ export class EmailController {
   }
 
   @Post('send')
+  @Groups('emailWriter')
   @ApiOperation({ title: 'Send email.' })
   @ApiOkResponse({ description: 'OK' })
   @ApiBadRequestResponse({ description: 'Missing fields' })
diff --git a/src/guards/groups.guards.ts b/src/guards/groups.guards.ts
new file mode 100644
index 0000000..9907486
--- /dev/null
+++ b/src/guards/groups.guards.ts
@@ -0,0 +1,27 @@
+import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
+import { Reflector } from '@nestjs/core';
+import { ConfigService } from '../configuration/config.service';
+
+@Injectable()
+export class GroupsGuard implements CanActivate {
+  constructor(private readonly reflector: Reflector, private configService: ConfigService) {}
+
+  canActivate(context: ExecutionContext): boolean {
+    // We get the groups specified on the endpoint with the @Groups annotation
+    // We need then to get the real group names associated to those keys from the config file
+    const groups = this.reflector.get<string[]>('groups', context.getHandler()).map((e) => {
+      return this.configService.config.groupNames[e];
+    });
+
+    // if no groups specified then access is always ok
+    if (!groups) {
+      return true;
+    }
+
+    // Get the group from the header
+    const request = context.switchToHttp().getRequest();
+    const consumerGroups = request.headers[this.configService.config.groupHeader].split(',').map(e => e.trim());
+    const hasGroup = () => consumerGroups.some((group) => groups.includes(group));
+    return consumerGroups && hasGroup();
+  }
+}
\ No newline at end of file
diff --git a/src/middlewares/email-writer.middleware.ts b/src/middlewares/email-writer.middleware.ts
deleted file mode 100644
index 7838f44..0000000
--- a/src/middlewares/email-writer.middleware.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Logger, ForbiddenException, NestMiddleware, Injectable, UnauthorizedException } from '@nestjs/common';
-import { ConfigService } from '../configuration/config.service';
-
-@Injectable()
-export class EmailWriterMiddleware implements NestMiddleware {
-
-  constructor(private configService: ConfigService) {
-  }
-
-  resolve() {
-    return (req, res, next) => {
-      Logger.log('[-] Untokenise middleware called');
-
-      const groupHeaderName = this.configService.config.groupHeader;
-      if (req.headers[groupHeaderName]) {
-        let arr = req.headers[groupHeaderName].split(',');
-        arr = arr.map(e => e.trim());
-        const group = arr.find(e => e === this.configService.config.emailWriterGroupName);
-        if (group === undefined) {
-          throw new ForbiddenException('You can\'t access this ressource.');
-        } else {
-          next();
-        }
-      } else {
-        throw new UnauthorizedException('You can\'t access this ressource.');
-      }
-    };
-  }
-}
\ No newline at end of file
diff --git a/swagger-spec.json b/swagger-spec.json
index 86ef943..76c450a 100644
--- a/swagger-spec.json
+++ b/swagger-spec.json
@@ -1 +1 @@
-{"swagger":"2.0","info":{"description":"Service providing the method to send emails.","version":"0.1","title":"Email service API"},"basePath":"/","tags":[{"name":"email","description":""}],"schemes":["http"],"paths":{"/email/contact":{"post":{"summary":"Send email to admin (emails defined as var env of the project, see docker-compose.yml file).","parameters":[{"name":"ContactForm","required":true,"in":"body","schema":{"$ref":"#/definitions/ContactForm"}}],"responses":{"200":{"description":"OK"},"400":{"description":"Missing fields"},"500":{"description":"Internal error, this is probably a rabbitMQ related error (unreachable service...)"}},"tags":["email"],"produces":["application/json"],"consumes":["application/json"]}}},"definitions":{"ContactForm":{"type":"object","properties":{"from":{"type":"string"},"subject":{"type":"string"},"firstname":{"type":"string"},"lastname":{"type":"string"},"text":{"type":"string"}},"required":["from","subject","firstname","lastname","text"]}}}
\ No newline at end of file
+{"swagger":"2.0","info":{"description":"Service providing the method to send emails.","version":"0.1","title":"Email service API"},"basePath":"/","tags":[{"name":"email","description":""}],"schemes":["http"],"paths":{"/email/contact":{"post":{"summary":"Send email to admin (emails defined as var env of the project, see docker-compose.yml file) and recap email to user email.","parameters":[{"name":"ContactForm","required":true,"in":"body","schema":{"$ref":"#/definitions/ContactForm"}}],"responses":{"200":{"description":"OK"},"400":{"description":"Missing fields"},"500":{"description":"Internal error, this is probably a rabbitMQ related error (unreachable service...)"}},"tags":["email"],"produces":["application/json"],"consumes":["application/json"]}},"/email/send":{"post":{"summary":"Send email.","parameters":[{"name":"EmailWithoutFrom","required":true,"in":"body","schema":{"$ref":"#/definitions/EmailWithoutFrom"}}],"responses":{"200":{"description":"OK"},"400":{"description":"Missing fields"},"500":{"description":"Internal error, this is probably a rabbitMQ related error (unreachable service...)"}},"tags":["email"],"produces":["application/json"],"consumes":["application/json"]}}},"definitions":{"ContactForm":{"type":"object","properties":{"email":{"type":"string"},"subject":{"type":"string"},"firstname":{"type":"string"},"lastname":{"type":"string"},"text":{"type":"string"}},"required":["email","subject","firstname","lastname","text"]},"EmailWithoutFrom":{"type":"object","properties":{"to":{"type":"array","items":{"type":"string"}},"replyTo":{"type":"string"},"cc":{"type":"array","items":{"type":"string"}},"bcc":{"type":"array","items":{"type":"string"}},"subject":{"type":"string"},"html":{"type":"string"}},"required":["to","subject","html"]}}}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index d6e1820..774aae5 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -8,6 +8,11 @@
     "allowSyntheticDefaultImports": true,
     "emitDecoratorMetadata": true,
     "experimentalDecorators": true,
+    "lib": [
+      "es6",
+      "es2017",
+      "dom"
+    ],
     "target": "es6",
     "sourceMap": true,
     "outDir": "./dist",
-- 
GitLab