Files
maestro/src/authentication/authentication.guard.spec.ts
T

466 lines
13 KiB
TypeScript

import request from 'supertest';
import { Test } from '@nestjs/testing';
import { HttpStatus, INestApplication } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import jwt from 'jsonwebtoken';
import { Body, Controller, Get } from '@nestjs/common';
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import {
AuthenticateCondition,
Authenticated,
RequireAllPermissions,
RequireSomePermission,
} from '../decorators/authentication.decorator';
import { AuthenticationGuard } from './authentication.guard';
import { PERMISSIONS_GROUPS } from './permissions.enum';
import { AuthClientService } from '../modules/auth/auth.service';
import ErrorCodes from '../utils/errorCodes';
import { ApiKeyService } from 'src/modules/api-key/api-key.service';
const logger = {
info: (...args) => args,
error: (...args) => args,
};
@Controller('no-class-auth')
class NoClassAuthController {
@Get('body')
async getBody(@Body() body) {
return { body };
}
@Get('authenticated')
@Authenticated()
async mustBeAuthenticated(@Body() body) {
return { body };
}
@Get('required-permission')
@RequireSomePermission(PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN)
async requiredPermission(@Body() body) {
return { body };
}
@Get('has-all-permissions')
@RequireAllPermissions(
PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN,
PERMISSIONS_GROUPS.DATAVIZ.permissions.METABASE,
)
async hasAllPermissions(@Body() body) {
return { body };
}
@Get('has-some-permission')
@RequireSomePermission(
PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN,
PERMISSIONS_GROUPS.DATAVIZ.permissions.METABASE,
)
async hasSomePermission(@Body() body) {
return { body };
}
}
@Controller('class-auth-condition')
@AuthenticateCondition((req) => req.get('x-on-class') === 'ok')
@RequireSomePermission(PERMISSIONS_GROUPS.DATAVIZ.permissions.METABASE)
class ClassAuthConditionController {
@Get('body')
async getBody(@Body() body) {
return { body };
}
@Get('authenticated')
@Authenticated()
async mustBeAuthenticated(@Body() body) {
return { body };
}
@Get('required-permission')
@AuthenticateCondition((req) => req.get('x-on-route') === 'ok')
@RequireSomePermission(PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN)
async requiredPermission(@Body() body) {
return { body };
}
}
describe('authentication.guard', () => {
let app: INestApplication;
const jwtSecretA = {
kid: 'token-a-testing-shared-key',
pem: '$tr0ng-SH4Red-secr3t!!!~gl0ba1~]',
};
const jwtSecretB = {
kid: 'token-b',
pem: 'another-shared-token',
};
const fakeUserPayload = {
user_id: 'd50d33c7-6c2b-463c-861f-e21667e7c125',
username: 'super.admin',
customer_id: '9d18e8ae-24b9-41a3-9e8f-a25ce57555b11',
customer_name: 'dadosfera',
customer_tier: 'BASIC',
customer_modules: []
};
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [
{
provide: AuthClientService,
useValue: {
getPublicKeys: async () => ({
keys: [jwtSecretA, jwtSecretB],
}),
},
},
{
provide: DadosferaLogger,
useValue: { logger },
},
{
provide: APP_GUARD,
useClass: AuthenticationGuard,
},
{
provide: ApiKeyService,
useValue: {
get: () => Promise.resolve(null)
}
}
],
controllers: [NoClassAuthController, ClassAuthConditionController],
}).compile();
app = moduleRef.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
function AssertBodyNoAuth(res: request.Response) {
expect(res.statusCode).toBe(HttpStatus.OK);
expect(res.body).toStrictEqual({ body: {} });
}
function AssertBodyWithAuth(res: request.Response) {
expect(res.statusCode).toBe(HttpStatus.OK);
expect(res.body).toStrictEqual({
body: {
info: {
customer: 'dadosfera',
customer_id: '9d18e8ae-24b9-41a3-9e8f-a25ce57555b11',
customer_tier: 'BASIC',
user_id: 'd50d33c7-6c2b-463c-861f-e21667e7c125',
},
},
});
}
function AssertUnauthorized(res: request.Response) {
expect(res.statusCode).toBe(HttpStatus.UNAUTHORIZED);
expect(res.body).toStrictEqual({
code: ErrorCodes.AUTH.UNAUTHORIZED,
error: 'Não autenticado',
message: 'É necessário estar logado para realizar essa operação',
statusCode: HttpStatus.UNAUTHORIZED,
});
}
function AssertForbidden(res: request.Response) {
expect(res.statusCode).toBe(HttpStatus.FORBIDDEN);
expect(res.body).toStrictEqual({
code: ErrorCodes.AUTH.FORBIDDEN,
error: 'Não autorizado',
message:
'Você não tem permissões suficientes para realizar essa operação',
statusCode: HttpStatus.FORBIDDEN,
});
}
function NoClassAuthTest(accessToken: string, tokenName: string[]) {
const route = '/no-class-auth';
describe(`${route}, ${tokenName?.length ? tokenName : 'none'} auth`, () => {
it('should GET /body with user data (if available)', async () => {
const res = await request(app.getHttpServer())
.get(`${route}/body`)
.set({ Authorization: accessToken });
if (tokenName?.length) {
AssertBodyWithAuth(res);
} else {
AssertBodyNoAuth(res);
}
});
it('should GET /authenticated if authenticated', async () => {
const res = await request(app.getHttpServer())
.get(`${route}/authenticated`)
.set({ Authorization: accessToken });
if (tokenName?.length) {
AssertBodyWithAuth(res);
} else {
AssertUnauthorized(res);
}
});
it('should GET /required-permission if authenticated', async () => {
const res = await request(app.getHttpServer())
.get(`${route}/required-permission`)
.set({ Authorization: accessToken });
// zendesk required
if (tokenName?.includes('zendesk')) {
AssertBodyWithAuth(res);
} else if (tokenName?.length) {
AssertForbidden(res);
} else {
AssertUnauthorized(res);
}
});
it('should GET /has-all-permissions if authenticated', async () => {
const res = await request(app.getHttpServer())
.get(`${route}/has-all-permissions`)
.set({ Authorization: accessToken });
// zendesk and metabase
if (tokenName?.includes('zendesk') && tokenName?.includes('metabase')) {
AssertBodyWithAuth(res);
} else if (tokenName?.length) {
AssertForbidden(res);
} else {
AssertUnauthorized(res);
}
});
it('should GET /has-some-permission if authenticated', async () => {
const res = await request(app.getHttpServer())
.get(`${route}/has-some-permission`)
.set({ Authorization: accessToken });
// zendesk or metabase
if (tokenName?.includes('zendesk') || tokenName?.includes('metabase')) {
AssertBodyWithAuth(res);
} else if (tokenName?.length) {
AssertForbidden(res);
} else {
AssertUnauthorized(res);
}
});
});
}
function ClassAuthConditionTest(accessToken: string, tokenName: string[]) {
const route = '/class-auth-condition';
describe(`${route}, ${tokenName?.length ? tokenName : 'none'} auth`, () => {
describe('GET /body', () => {
it('should GET /body if authenticated and with correct headers', async () => {
const res = await request(app.getHttpServer())
.get(`${route}/body`)
.set({
'x-on-class': 'ok',
Authorization: accessToken,
});
// metabase required
if (tokenName?.includes('metabase')) {
AssertBodyWithAuth(res);
} else if (tokenName?.length) {
AssertForbidden(res);
} else {
AssertUnauthorized(res);
}
});
it('should not GET /body without correct headers', async () => {
const res = await request(app.getHttpServer())
.get(`${route}/body`)
.set({ Authorization: accessToken });
if (tokenName?.length) {
AssertForbidden(res);
} else {
AssertUnauthorized(res);
}
});
});
describe('GET /authenticated', () => {
it('should GET /authenticated if authenticated and with correct headers', async () => {
const res = await request(app.getHttpServer())
.get(`${route}/authenticated`)
.set({
'x-on-class': 'ok',
Authorization: accessToken,
});
// metabase required
if (tokenName?.includes('metabase')) {
AssertBodyWithAuth(res);
} else if (tokenName?.length) {
AssertForbidden(res);
} else {
AssertUnauthorized(res);
}
});
it('should not GET /authenticated without correct class headers', async () => {
const res = await request(app.getHttpServer())
.get(`${route}/authenticated`)
.set({ Authorization: accessToken });
if (tokenName?.length) {
AssertForbidden(res);
} else {
AssertUnauthorized(res);
}
});
});
describe('GET /required-permission', () => {
it('should GET /required-permission if authenticated and with correct headers', async () => {
const res = await request(app.getHttpServer())
.get(`${route}/required-permission`)
.set({
'x-on-class': 'ok',
'x-on-route': 'ok',
Authorization: accessToken,
});
// zendesk and metabase
if (
tokenName?.includes('zendesk') &&
tokenName?.includes('metabase')
) {
AssertBodyWithAuth(res);
} else if (tokenName?.length) {
AssertForbidden(res);
} else {
AssertUnauthorized(res);
}
});
it('should not GET /required-permission without correct route headers', async () => {
const res = await request(app.getHttpServer())
.get(`${route}/required-permission`)
.set({
'x-on-class': 'ok',
Authorization: accessToken,
});
if (tokenName?.length) {
AssertForbidden(res);
} else {
AssertUnauthorized(res);
}
});
it('should not GET /required-permission without correct class headers', async () => {
const res = await request(app.getHttpServer())
.get(`${route}/required-permission`)
.set({
'x-on-route': 'ok',
Authorization: accessToken,
});
if (tokenName?.length) {
AssertForbidden(res);
} else {
AssertUnauthorized(res);
}
});
});
});
}
describe('token validation', () => {
it('should accept token kid A', async () => {
const Authorization = CreateToken([], {
jwt: {
kid: jwtSecretA.kid,
pem: jwtSecretA.pem,
},
});
const res = await request(app.getHttpServer())
.get('/no-class-auth/authenticated')
.set({
Authorization,
});
AssertBodyWithAuth(res);
});
it('should accept token kid B', async () => {
const Authorization = CreateToken([], {
jwt: {
kid: jwtSecretB.kid,
pem: jwtSecretB.pem,
},
});
const res = await request(app.getHttpServer())
.get('/no-class-auth/authenticated')
.set({
Authorization,
});
AssertBodyWithAuth(res);
});
it('should not accept unknown kid', async () => {
const Authorization = CreateToken([], {
jwt: {
kid: 'unknown-key-id',
pem: jwtSecretA.pem,
},
});
const res = await request(app.getHttpServer())
.get('/no-class-auth/authenticated')
.set({
Authorization,
});
AssertUnauthorized(res);
});
});
function CreateToken(permissions, overrides?: { user?: any; jwt?: any }) {
const tokenPayload = {
...fakeUserPayload,
permissions: permissions.map(({ seqid }) => seqid),
token_use: 'access',
...overrides?.user,
};
return jwt.sign(tokenPayload, overrides?.jwt?.pem ?? jwtSecretA.pem, {
keyid: overrides?.jwt?.kid ?? jwtSecretA.kid,
});
}
NoClassAuthTest(null, null);
ClassAuthConditionTest(null, null);
// const tokenZ = CreateToken([PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN]);
// NoClassAuthTest(tokenZ, ['zendesk']);
// ClassAuthConditionTest(tokenZ, ['zendesk']);
// const tokenM = CreateToken([PERMISSIONS_GROUPS.DATAVIZ.permissions.METABASE]);
// NoClassAuthTest(tokenM, ['metabase']);
// ClassAuthConditionTest(tokenM, ['metabase']);
// const tokenZM = CreateToken([
// PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN,
// PERMISSIONS_GROUPS.DATAVIZ.permissions.METABASE,
// ]);
// NoClassAuthTest(tokenZM, ['zendesk', 'metabase']);
// ClassAuthConditionTest(tokenZM, ['zendesk', 'metabase']);
});