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