Skip to content
Snippets Groups Projects
Commit fc672f39 authored by FORESTIER Fabien's avatar FORESTIER Fabien
Browse files

Use guards instead of middleware in order to restrict access by group

parent a3b971c1
No related branches found
No related tags found
No related merge requests found
Pipeline #
import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { EmailModule } from './email/email.module'; import { EmailModule } from './email/email.module';
import { ConfigModule } from './configuration/config.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({ @Module({
imports: [ConfigModule, EmailModule], imports: [ConfigModule, EmailModule],
controllers: [], controllers: [],
providers: [], providers: [
{
provide: APP_GUARD,
useClass: GroupsGuard,
},
],
}) })
export class AppModule { 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 },
);
}
}
...@@ -21,7 +21,7 @@ export class ConfigService { ...@@ -21,7 +21,7 @@ export class ConfigService {
this._config.rabbitMQ.user = process.env.RABBITMQ_USER; this._config.rabbitMQ.user = process.env.RABBITMQ_USER;
this._config.rabbitMQ.password = process.env.RABBITMQ_PASSWORD; this._config.rabbitMQ.password = process.env.RABBITMQ_PASSWORD;
this._config.plateformDataEmail = process.env.PLATEFORM_DATA_EMAIL; 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; this._config.groupHeader = process.env.GROUP_HEADER;
} }
......
...@@ -8,6 +8,8 @@ export const config = { ...@@ -8,6 +8,8 @@ export const config = {
mailerQueue: 'portail-data-send-email', mailerQueue: 'portail-data-send-email',
plateformDataEmail: '', plateformDataEmail: '',
imageHost: 'https://highway-to-data.alpha.grandlyon.com/email-template-assets', imageHost: 'https://highway-to-data.alpha.grandlyon.com/email-template-assets',
emailWriterGroupName: '', groupNames: {
emailWriter: '',
},
groupHeader: '', groupHeader: '',
}; };
\ No newline at end of file
import { ReflectMetadata } from '@nestjs/common';
export const Groups = (...roles: string[]) => ReflectMetadata('groups', roles);
\ No newline at end of file
...@@ -2,6 +2,7 @@ import { Controller, Post, Body, InternalServerErrorException, Logger, HttpCode ...@@ -2,6 +2,7 @@ import { Controller, Post, Body, InternalServerErrorException, Logger, HttpCode
import { ContactForm, EmailWithoutFrom } from './email'; import { ContactForm, EmailWithoutFrom } from './email';
import { EmailService } from './email.service'; import { EmailService } from './email.service';
import { ApiBadRequestResponse, ApiOkResponse, ApiUseTags, ApiOperation, ApiInternalServerErrorResponse } from '@nestjs/swagger'; import { ApiBadRequestResponse, ApiOkResponse, ApiUseTags, ApiOperation, ApiInternalServerErrorResponse } from '@nestjs/swagger';
import { Groups } from '../decorators/groups.decorators';
@ApiUseTags('email') @ApiUseTags('email')
@Controller('email') @Controller('email')
...@@ -28,6 +29,7 @@ export class EmailController { ...@@ -28,6 +29,7 @@ export class EmailController {
} }
@Post('send') @Post('send')
@Groups('emailWriter')
@ApiOperation({ title: 'Send email.' }) @ApiOperation({ title: 'Send email.' })
@ApiOkResponse({ description: 'OK' }) @ApiOkResponse({ description: 'OK' })
@ApiBadRequestResponse({ description: 'Missing fields' }) @ApiBadRequestResponse({ description: 'Missing fields' })
......
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
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
{"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"]}}} {"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 \ No newline at end of file
...@@ -8,6 +8,11 @@ ...@@ -8,6 +8,11 @@
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"lib": [
"es6",
"es2017",
"dom"
],
"target": "es6", "target": "es6",
"sourceMap": true, "sourceMap": true,
"outDir": "./dist", "outDir": "./dist",
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment