mirror of
https://github.com/dadosfera/maestro.git
synced 2026-09-24 21:04:47 +00:00
466 lines
13 KiB
TypeScript
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']);
|
|
});
|