mirror of
https://github.com/dadosfera/maestro.git
synced 2026-09-07 18:04:47 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e92ca2c36 | ||
|
|
12af510d36 | ||
|
|
61871958aa | ||
|
|
64d84f9148 | ||
|
|
2c099ba491 | ||
|
|
65b2578f15 |
@@ -35,9 +35,7 @@ jobs:
|
||||
semantic_release:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
#new_release_published: ${{ steps.semantic.outputs.last_release_version != steps.semantic.outputs.new_release_version }}
|
||||
new_release_published: ${{ steps.semantic.outputs.new_release_published }}
|
||||
#new_release_version: ${{ steps.semantic.outputs.new_release_version }}
|
||||
new_release_version: ${{ (steps.semantic.outputs.new_release_published == 'true' && steps.semantic.outputs.new_release_version) || (github.event_name == 'workflow_dispatch' && '0.0.0') }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -57,7 +55,6 @@ jobs:
|
||||
]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
deploy:
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || needs.semantic_release.outputs.new_release_published == 'true' }}
|
||||
@@ -107,9 +104,8 @@ jobs:
|
||||
ENV: ${{ needs.extract_environment.outputs.environment }}
|
||||
IMAGE_TAG: ${{ needs.semantic_release.outputs.new_release_version }}
|
||||
ACCOUNT_ID: ${{ steps.aws.outputs.aws-account-id }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
docker-compose -f build.docker-compose.yml build --build-arg NPM_TOKEN=${NPM_TOKEN}
|
||||
docker-compose -f build.docker-compose.yml build
|
||||
docker-compose -f build.docker-compose.yml push
|
||||
|
||||
- name: Create ZIP file to Deploy AWS Beanstalk
|
||||
|
||||
@@ -2,8 +2,6 @@ name: Test
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- alpha
|
||||
- beta
|
||||
- main
|
||||
|
||||
jobs:
|
||||
@@ -21,23 +19,22 @@ jobs:
|
||||
env:
|
||||
ENV: test
|
||||
IMAGE_TAG: test
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
export ENV=test
|
||||
export IMAGE_TAG=test
|
||||
export ACCOUNT_ID=test
|
||||
docker-compose -f build.docker-compose.yml build --build-arg NPM_TOKEN=${NPM_TOKEN}
|
||||
docker-compose -f build.docker-compose.yml build
|
||||
|
||||
- name: Run Test
|
||||
env:
|
||||
ENV: test
|
||||
IMAGE_TAG: test
|
||||
ACCOUNT_ID: test
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
APP_NAME: ${{ github.event.repository.name }}
|
||||
run: |
|
||||
export ENV=test
|
||||
export IMAGE_TAG=test
|
||||
docker-compose -f build.docker-compose.yml run -e NPM_TOKEN=${NPM_TOKEN} --rm --entrypoint="npm run test" maestro
|
||||
docker-compose -f build.docker-compose.yml run --rm --entrypoint="npm run test" $APP_NAME
|
||||
|
||||
- name: Remove Docker's Trash
|
||||
if: always()
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"preset": "eslint"
|
||||
}
|
||||
],
|
||||
"@semantic-release/npm",
|
||||
"@semantic-release/github"
|
||||
]
|
||||
}
|
||||
|
||||
+6
-13
@@ -1,23 +1,16 @@
|
||||
FROM node:18.3.0-alpine3.15 as packages
|
||||
WORKDIR /packages
|
||||
ARG NPM_TOKEN
|
||||
COPY package.json .
|
||||
COPY package-lock.json .
|
||||
COPY .npmrc .
|
||||
RUN apk update \
|
||||
&& apk add --no-cache aws-cli \
|
||||
&& aws codeartifact login --tool npm --repository dadosfera-npm --domain dadosfera --domain-owner 611330257153 --region us-east-1 \
|
||||
&& npm install
|
||||
|
||||
RUN apk update && apk add curl unzip
|
||||
RUN apk add --no-cache aws-cli
|
||||
|
||||
RUN aws codeartifact login --tool npm --repository dadosfera-npm --domain dadosfera --domain-owner 611330257153 --region us-east-1
|
||||
|
||||
RUN npm install
|
||||
RUN rm -f ./.npmrc
|
||||
|
||||
FROM node:14.15.4-alpine3.12
|
||||
FROM node:18.3.0-alpine3.15
|
||||
WORKDIR /app
|
||||
COPY . /app/
|
||||
ARG NPM_TOKEN
|
||||
COPY --from=packages /packages/node_modules /app/node_modules
|
||||
RUN npm run build
|
||||
EXPOSE 3333
|
||||
ENTRYPOINT npm run start
|
||||
ENTRYPOINT npm run start
|
||||
|
||||
@@ -1,50 +1,301 @@
|
||||
# Maestro
|
||||
<p align="center">
|
||||
<image src="./assets/maestro.svg" style="width:10rem">
|
||||
<h1 align="center">Maestro</h1>
|
||||
</p>
|
||||
This is the Dadosfera´s gateway repository, it´s responsable for the communication between frontend application and Dadosfera´s mirosservices.
|
||||
|
||||
## 💻 Requirements
|
||||
# Maestro
|
||||
Maestro is the Dadosfera's gateway, it's responsible for the communication between the frontend application and Dadosfera's microservices.
|
||||
|
||||
Before you start, make sure you have done the following steps:
|
||||
## 🚀 Starting
|
||||
These instructions will allow you to get a working copy of the project on your local machine for development and testing purposes.
|
||||
|
||||
* Installed Nodejs version 16.14.2
|
||||
* Installed latest NPM version
|
||||
### 📋 Requirements
|
||||
- [NodeJS v18.3.0 LTS / NPM v8.11](https://nodejs.org/pt-br/download/) (you can opt to use [NVM](https://github.com/nvm-sh/nvm) to easily manage node versions)
|
||||
- Request access to AWS Console dev account for **all services** (avoid gradually asking for each needed service. it will slow down your development cycle)
|
||||
- Create your Access Key on the "Security credentials" menu
|
||||
- Set the Access Key on your local development machine
|
||||
- Request access to the dev, stg and prd VPNs
|
||||
|
||||
### 🔧 Installation<a id="installation"></a>
|
||||
- Clone the repository
|
||||
- SSH
|
||||
```
|
||||
git clone git@github.com:dadosfera/maestro.git
|
||||
```
|
||||
or
|
||||
|
||||
- HTTPS
|
||||
```
|
||||
git clone https://github.com/dadosfera/maestro.git
|
||||
```
|
||||
|
||||
## 🚀 Installing Maestro
|
||||
- Select the correct node version (optional, only if using [NVM](https://github.com/nvm-sh/nvm)):
|
||||
```sh
|
||||
nvm use
|
||||
```
|
||||
|
||||
First of all clone the repository:
|
||||
* SSH:
|
||||
```
|
||||
git clone git@github.com:dadosfera/maestro.git
|
||||
```
|
||||
* HTTPS:
|
||||
```
|
||||
git clone git@github.com:dadosfera/maestro.git
|
||||
- Install the project dependencies:
|
||||
```sh
|
||||
npm i
|
||||
```
|
||||
|
||||
- Setup the following enviroment variables:
|
||||
```
|
||||
ENV=
|
||||
DUC_URL=
|
||||
INFACTORY_URL=
|
||||
OTFACTORY_URL=
|
||||
PIFACTORY_URL=
|
||||
SM_OAUTH_PATH=
|
||||
```
|
||||
|
||||
- Start the server:
|
||||
```sh
|
||||
# dev mode
|
||||
npm run start:dev
|
||||
|
||||
# or in debug mode
|
||||
npm run start:debug
|
||||
```
|
||||
The service should start successfully.
|
||||
|
||||
## Authentication decorators
|
||||
Maestro have utilities to ease the user authentication on every controller and route. The following decorators are available:
|
||||
|
||||
### `@Authenticated`
|
||||
If the user must be authenticated to make request, we can use the `@Authenticated` decorator in the controller or route, as needed.
|
||||
|
||||
```ts
|
||||
import { Authenticated } from '../../authentication/authentication.decorator';
|
||||
|
||||
@Controller('foo')
|
||||
@Authenticated()
|
||||
class FooController {
|
||||
/* ... */
|
||||
|
||||
@Get('bar')
|
||||
async getBar() {
|
||||
this.logger.info('user is authenticated!');
|
||||
|
||||
return { authenticated: true };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Enviroment variables
|
||||
```ts
|
||||
import { Authenticated } from '../../authentication/authentication.decorator';
|
||||
|
||||
Here is a list of enviroment variables needed in order to run the application correctly.
|
||||
@Controller('foo')
|
||||
class FooController {
|
||||
/* ... */
|
||||
|
||||
```
|
||||
ENV=
|
||||
DUC_URL=
|
||||
INFACTORY_URL=
|
||||
TRFACTORY_URL=
|
||||
OTFACTORY_URL=
|
||||
PIFACTORY_URL=
|
||||
JWT_PRIVATE_KEY=
|
||||
AWS_IDENTITY_POOL_ID=
|
||||
@Get('bar')
|
||||
@Authenticated()
|
||||
async getBar() {
|
||||
this.logger.info('user is authenticated!');
|
||||
|
||||
return { authenticated: true };
|
||||
}
|
||||
|
||||
@Post('bar')
|
||||
async postBar() {
|
||||
this.logger.info('user is NOT authenticated!');
|
||||
|
||||
return { authenticated: false };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Running Maestro
|
||||
### `@RequireAllPermissions`
|
||||
This decorator requires that **all permissions** listed are granted to the requesting user.
|
||||
|
||||
In order to run Maestro just run the following command:
|
||||
```ts
|
||||
import { RequireAllPermissions } from '../../authentication/authentication.decorator';
|
||||
import { Permissions } from '../../authentication/permissions.enum';
|
||||
|
||||
@Controller('foo')
|
||||
class FooController {
|
||||
/* ... */
|
||||
|
||||
@Get('bar')
|
||||
@RequireAllPermissions(Permissions.BAR.MANAGE, Permissions.BAR.CREATE)
|
||||
async getBar() {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
```
|
||||
npm run start:dev
|
||||
|
||||
### `@RequireSomePermission`
|
||||
In the following case, the user is required to have **at least one** listed permission.
|
||||
|
||||
```ts
|
||||
import { RequireSomePermission } from '../../authentication/authentication.decorator';
|
||||
import { Permissions } from '../../authentication/permissions.enum';
|
||||
|
||||
@Controller('foo')
|
||||
@RequireSomePermission(Permissions.FOO.MANAGE, Permissions.FOO.CREATE)
|
||||
class FooController {
|
||||
/* ... */
|
||||
|
||||
@Get('bar')
|
||||
async getBar() {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
```
|
||||
If everything is fine the Maestro will start and be ready to receive HTTP requests
|
||||
|
||||
### `@AuthenticateCondition`
|
||||
If a more complicated authentication check needs to be done, we can use the `@AuthenticateCondition` decorator to define it. The custom function must return `true` to authenticate the request.
|
||||
|
||||
In the following example:
|
||||
- all routes on the `FooController` controller can only be requested from localhost
|
||||
- `POST /foo/bar` can only be requested from localhost **and** by users from customer id `111...eef`
|
||||
|
||||
```ts
|
||||
import { AuthenticateCondition } from '../../authentication/authentication.decorator';
|
||||
|
||||
@Controller('foo')
|
||||
// allow requests only from localhost
|
||||
@AuthenticateCondition((request: Request) => request.ip === '::ffff:127.0.0.1')
|
||||
class FooController {
|
||||
/* ... */
|
||||
|
||||
@Get('bar')
|
||||
async getBar() {
|
||||
/* ... */
|
||||
}
|
||||
|
||||
@Post('bar')
|
||||
// allow requests only from a specific customer
|
||||
@AuthenticateCondition((request: Request, user: RequestUser) => user.customer_id === '1113e943-2187-4fdd-9c2c-54338fedaeef')
|
||||
async postBar() {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Note on authentication decorators
|
||||
- The old authentication method placed the user data in the `request.body.info` field, this imposes certain issues regarding the request body because this data should be from the frontend without any modification by Maestro. Now this usage is ⚠️ **DEPRECATED** ⚠️. We are working to migrate to the `@User` parameter decorator. The old method is working while the migration is in progress.
|
||||
|
||||
- Authentication decorators can be used together and all of them **must** pass to the request be authenticated, but in the general case you don't need to (*and wouldn't like to...*) use all of them together, as you can code all of the authentication logic in the `@AuthenticateCondition` decorator.
|
||||
|
||||
```ts
|
||||
import {
|
||||
RequireAllPermissions,
|
||||
RequireSomePermission,
|
||||
AuthenticateCondition,
|
||||
} from '../../authentication/authentication.decorator';
|
||||
import { Permissions } from '../../authentication/permissions.enum';
|
||||
import { Waa, Baz } from './authenticationFunctions'
|
||||
|
||||
@Controller('foo')
|
||||
@RequireAllPermissions(Permissions.FOO.USE, Permissions.FOO.REQUEST)
|
||||
@RequireSomePermission(Permissions.FOO.MANAGE, Permissions.FOO.ADMIN)
|
||||
@AuthenticateCondition(Waa)
|
||||
@AuthenticateCondition(Baz)
|
||||
class FooController {
|
||||
/* ... */
|
||||
|
||||
@Get('bar')
|
||||
async getBar() {
|
||||
/* ... */
|
||||
}
|
||||
|
||||
@Post('bar')
|
||||
@RequireAllPermissions(Permissions.BAR.MANAGE, Permissions.BAR.USE, Permissions.BAR.REQUEST)
|
||||
async postBar() {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- If `@RequireAllPermissions` and `@RequireSomePermission` are used with **only a single permission**, they present the **exactly same behavior**.
|
||||
|
||||
```ts
|
||||
// same behavior
|
||||
@RequireAllPermissions(Permissions.BAR.MANAGE)
|
||||
@RequireSomePermission(Permissions.BAR.MANAGE)
|
||||
```
|
||||
|
||||
|
||||
|
||||
## `@User` parameter decorator
|
||||
The requesting user data can be obtained using the @User parameter decorator, like in the following snippet:
|
||||
|
||||
```ts
|
||||
import { User, RequestUser } from '../../authentication/user.decorator';
|
||||
|
||||
@Controller('foo')
|
||||
class FooController {
|
||||
/* ... */
|
||||
|
||||
@Get('bar')
|
||||
async getBar(@User() user: RequestUser) {
|
||||
this.logger.info(user);
|
||||
|
||||
return { user };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If the user is required to be logged in, set `required` to `true`, as you would want in the `change-password` operation:
|
||||
|
||||
```ts
|
||||
import { User, RequestUser } from '../../authentication/user.decorator';
|
||||
|
||||
@Controller('user')
|
||||
class UserController {
|
||||
/* ... */
|
||||
|
||||
@Post('change-password')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async changePassword(
|
||||
@User({ required: true }) user: RequestUser,
|
||||
@Body() body: AuthChangePasswordRequest,
|
||||
) {
|
||||
const { oldPassword, newPassword } = body;
|
||||
|
||||
return this.authClient.changePassword({
|
||||
userId: user.user_id,
|
||||
oldPassword,
|
||||
newPassword,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📦 Development
|
||||
### ⌨️ Coding Style
|
||||
By default, we use [ESLint](https://eslint.org/) + [Prettier](https://prettier.io/) with default settings.
|
||||
|
||||
**We recommend using Visual Studio Code and installing the recommended extensions to ease the development process.**
|
||||
|
||||
### Commits pattern
|
||||
Our workflow pipeline follows the conventional commits specs ([cheat sheets](https://cheatography.com/albelop/cheat-sheets/conventional-commits/)) to release versions accordingly.
|
||||
|
||||
Format: `<type>[optional scope]: <description>`
|
||||
|
||||
Example: `FIX: ensure Range headers adhere more closely to RFC 2616`
|
||||
|
||||
### Branching naming convention
|
||||
- **Feature**: Any code changes for a new module or use case should be done on a feature branch. This branch is created based on the `main` branch. When all changes are done, a Pull Request/Merge Request is needed to put all of these changes back to the `main` branch. Examples: `feature/integrate-swagger`, `feature/JIRA-1234`, `feature/JIRA-1234_support-dark-theme`.
|
||||
|
||||
**It is recommended to use all lower caps letters and hyphen (-) to separate words unless it is a specific item name or ID. Underscore (_) could be used to separate the ID and description.**
|
||||
|
||||
- **Bug Fix**: If the code changes made from the feature branch were rejected after a release, sprint or demo, any necessary fixes after that should be done on the bugfix branch. Examples: `bugfix/more-gray-shades`, `bugfix/JIRA-1444_gray-on-blur-fix`.
|
||||
|
||||
- **Hot Fix**: If there is a need to fix a blocker, do a temporary patch, apply a critical framework or configuration change that should be handled immediately, it should be created as a Hotfix. Examples: `hotfix/disable-endpoint-zero-day-exploit`, `hotfix/increase-scaling-threshold`.
|
||||
|
||||
- **Experimental**: A branch for playing around. Any new feature or idea that is not part of a release or a sprint. Example: `experimental/dark-theme-support`.
|
||||
|
||||
### Making a Pull Request
|
||||
1. Commit your changes
|
||||
2. Open the Pull Request on GitHub
|
||||
3. Send Pull Request link in microsfera Google Chat Group for review and possible approval
|
||||
|
||||
## 🛠️ Built with
|
||||
Some technologies used in this project:
|
||||
|
||||
- [NestJS](https://docs.nestjs.com) - Framework for building efficient and scalable NodeJS server-side applications
|
||||
|
||||
## ⚙️ Back-end Architecture
|
||||
The architecture can be found at [this link](https://sites.google.com/dadosfera.ai/wikidoproduto/time/back-end).
|
||||
|
||||
Generated
+4
-1
@@ -9868,8 +9868,9 @@
|
||||
},
|
||||
"node_modules/supertest": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://dadosfera-611330257153.d.codeartifact.us-east-1.amazonaws.com:443/npm/dadosfera-npm/supertest/-/supertest-6.2.3.tgz",
|
||||
"integrity": "sha512-3GSdMYTMItzsSYjnIcljxMVZKPW1J9kYHZY+7yLfD0wpPwww97GeImZC1oOk0S5+wYl2niJwuFusBJqwLqYM3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"methods": "^1.1.2",
|
||||
"superagent": "^7.1.3"
|
||||
@@ -17790,6 +17791,8 @@
|
||||
},
|
||||
"supertest": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://dadosfera-611330257153.d.codeartifact.us-east-1.amazonaws.com:443/npm/dadosfera-npm/supertest/-/supertest-6.2.3.tgz",
|
||||
"integrity": "sha512-3GSdMYTMItzsSYjnIcljxMVZKPW1J9kYHZY+7yLfD0wpPwww97GeImZC1oOk0S5+wYl2niJwuFusBJqwLqYM3g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"methods": "^1.1.2",
|
||||
|
||||
+4
-4
@@ -36,13 +36,11 @@
|
||||
"@nestjs/platform-express": "^8.4.7",
|
||||
"@nestjs/schedule": "^1.1.0",
|
||||
"@nestjs/swagger": "^5.2.1",
|
||||
"protospack": "2.5.1",
|
||||
"protospack-v2": "1.1.1",
|
||||
"axios": "^0.25.0",
|
||||
"cron-parser": "^4.4.0",
|
||||
"dadosfera-logs": "^1.0.0-alpha.10",
|
||||
"elastic-apm-node": "^3.36.0",
|
||||
"dotenv": "^14.3.2",
|
||||
"elastic-apm-node": "^3.36.0",
|
||||
"helmet": "^5.1.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwk-to-pem": "^2.0.5",
|
||||
@@ -52,6 +50,8 @@
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-hubspot-oauth2": "^1.0.3",
|
||||
"passport-mailchimp": "^1.1.0",
|
||||
"protospack": "2.5.1",
|
||||
"protospack-v2": "1.1.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.5.5",
|
||||
@@ -107,4 +107,4 @@
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,52 @@
|
||||
import { SetMetadata, applyDecorators } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { CustomDecorator } from '@nestjs/common';
|
||||
import { Permission } from './permissions.enum';
|
||||
import { RequestUser } from './user.decorator';
|
||||
|
||||
export const PERMISSIONS_KEY = '__PERMISSIONS__';
|
||||
export const MUST_BE_AUTHENTICATED_KEY = '__MUST_BE_AUTHENTICATED__';
|
||||
export const CUSTOM_AUTHENTICATION_FUNCTION_KEY =
|
||||
'__CUSTOM_AUTHENTICATION_FUNCTION__';
|
||||
export const AUTH_FUNCTION_KEY = '__AUTH_FUNCTION__';
|
||||
|
||||
export type AuthenticationFunction = (req: Request, user: any) => boolean;
|
||||
|
||||
export function RequirePermissions(...permissions: Permission[]) {
|
||||
return applyDecorators(
|
||||
SetMetadata(PERMISSIONS_KEY, permissions),
|
||||
SetMetadata(MUST_BE_AUTHENTICATED_KEY, true),
|
||||
// implementation copied from SetMetadata, but tweaked to get existing values
|
||||
// of the metadataKey and accumulate it with the new metadataValue
|
||||
function SetMultipleMetadata(metadataKey, metadataValue): CustomDecorator {
|
||||
const decoratorFactory = (target, key, descriptor) => {
|
||||
// .start: tweak
|
||||
// descriptor?.value = function decorator; target = class decorator
|
||||
const accumulatedVal =
|
||||
Reflect.getMetadata(metadataKey, descriptor?.value ?? target) ?? [];
|
||||
accumulatedVal.push(metadataValue);
|
||||
// .end: tweak
|
||||
|
||||
if (descriptor) {
|
||||
Reflect.defineMetadata(metadataKey, accumulatedVal, descriptor.value);
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
Reflect.defineMetadata(metadataKey, accumulatedVal, target);
|
||||
return target;
|
||||
};
|
||||
|
||||
decoratorFactory.KEY = metadataKey;
|
||||
return decoratorFactory as any;
|
||||
}
|
||||
|
||||
export function RequireAllPermissions(...permissions: Permission[]) {
|
||||
return SetMultipleMetadata(AUTH_FUNCTION_KEY, (req, user: RequestUser) =>
|
||||
permissions.every(({ seqid }) => user.permissions.includes(seqid)),
|
||||
);
|
||||
}
|
||||
|
||||
export function RequireSomePermission(...permissions: Permission[]) {
|
||||
return SetMultipleMetadata(AUTH_FUNCTION_KEY, (req, user: RequestUser) =>
|
||||
permissions.some(({ seqid }) => user.permissions.includes(seqid)),
|
||||
);
|
||||
}
|
||||
|
||||
export function AuthenticateCondition(func: AuthenticationFunction) {
|
||||
return applyDecorators(
|
||||
SetMetadata(CUSTOM_AUTHENTICATION_FUNCTION_KEY, func),
|
||||
SetMetadata(MUST_BE_AUTHENTICATED_KEY, true),
|
||||
);
|
||||
return SetMultipleMetadata(AUTH_FUNCTION_KEY, func);
|
||||
}
|
||||
|
||||
export function Authenticated() {
|
||||
return SetMetadata(MUST_BE_AUTHENTICATED_KEY, true);
|
||||
return SetMultipleMetadata(AUTH_FUNCTION_KEY, () => true);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
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-logs';
|
||||
import {
|
||||
AuthenticateCondition,
|
||||
Authenticated,
|
||||
RequireAllPermissions,
|
||||
RequireSomePermission,
|
||||
} from './authentication.decorator';
|
||||
import { AuthenticationGuard } from './authentication.guard';
|
||||
import { Permissions } from './permissions.enum';
|
||||
import { AuthClientService } from '../clients/auth/client.service';
|
||||
|
||||
import ErrorCodes from '../utils/errorCodes';
|
||||
|
||||
@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.ZENDESK.OPEN)
|
||||
async requiredPermission(@Body() body) {
|
||||
return { body };
|
||||
}
|
||||
|
||||
@Get('has-all-permissions')
|
||||
@RequireAllPermissions(Permissions.ZENDESK.OPEN, Permissions.METABASE.OPEN)
|
||||
async hasAllPermissions(@Body() body) {
|
||||
return { body };
|
||||
}
|
||||
|
||||
@Get('has-some-permission')
|
||||
@RequireSomePermission(Permissions.ZENDESK.OPEN, Permissions.METABASE.OPEN)
|
||||
async hasSomePermission(@Body() body) {
|
||||
return { body };
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('class-auth-condition')
|
||||
@AuthenticateCondition((req) => req.get('x-on-class') === 'ok')
|
||||
@RequireSomePermission(Permissions.METABASE.OPEN)
|
||||
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.ZENDESK.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',
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: AuthClientService,
|
||||
useValue: {
|
||||
getPublicKeys: async () => ({
|
||||
keys: [jwtSecretA, jwtSecretB],
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: DadosferaLogger,
|
||||
useValue: { logger: console },
|
||||
},
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: AuthenticationGuard,
|
||||
},
|
||||
],
|
||||
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.ZENDESK.OPEN]);
|
||||
NoClassAuthTest(tokenZ, ['zendesk']);
|
||||
ClassAuthConditionTest(tokenZ, ['zendesk']);
|
||||
|
||||
const tokenM = CreateToken([Permissions.METABASE.OPEN]);
|
||||
NoClassAuthTest(tokenM, ['metabase']);
|
||||
ClassAuthConditionTest(tokenM, ['metabase']);
|
||||
|
||||
const tokenZM = CreateToken([
|
||||
Permissions.ZENDESK.OPEN,
|
||||
Permissions.METABASE.OPEN,
|
||||
]);
|
||||
NoClassAuthTest(tokenZM, ['zendesk', 'metabase']);
|
||||
ClassAuthConditionTest(tokenZM, ['zendesk', 'metabase']);
|
||||
});
|
||||
@@ -11,13 +11,11 @@ import assert from 'assert';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { DadosferaLogger } from 'dadosfera-logs';
|
||||
import { AuthClientService } from '../clients/auth/client.service';
|
||||
import { Permission } from './permissions.enum';
|
||||
import {
|
||||
AuthenticationFunction,
|
||||
PERMISSIONS_KEY,
|
||||
CUSTOM_AUTHENTICATION_FUNCTION_KEY,
|
||||
MUST_BE_AUTHENTICATED_KEY,
|
||||
AUTH_FUNCTION_KEY,
|
||||
} from './authentication.decorator';
|
||||
import { RequestUser } from './user.decorator';
|
||||
import ErrorBuilder from '../utils/ErrorBuilder';
|
||||
import ErrorCodes from '../utils/errorCodes';
|
||||
|
||||
@@ -43,7 +41,7 @@ export class AuthenticationGuard
|
||||
return this.loadDucJWKS();
|
||||
}
|
||||
|
||||
async loadDucJWKS() {
|
||||
private async loadDucJWKS() {
|
||||
const { keys } = await this.authClient.getPublicKeys();
|
||||
|
||||
keys.forEach((key) => {
|
||||
@@ -52,57 +50,38 @@ export class AuthenticationGuard
|
||||
}
|
||||
|
||||
canActivate(ctx: ExecutionContext): boolean {
|
||||
const requiredPermissions = this.reflector.getAllAndOverride<Permission[]>(
|
||||
PERMISSIONS_KEY,
|
||||
[ctx.getHandler(), ctx.getClass()],
|
||||
);
|
||||
const customAuthenticationFunction =
|
||||
this.reflector.getAllAndOverride<AuthenticationFunction>(
|
||||
CUSTOM_AUTHENTICATION_FUNCTION_KEY,
|
||||
[ctx.getHandler(), ctx.getClass()],
|
||||
);
|
||||
const mustBeAuthenticated = this.reflector.getAllAndOverride<boolean>(
|
||||
MUST_BE_AUTHENTICATED_KEY,
|
||||
[ctx.getHandler(), ctx.getClass()],
|
||||
);
|
||||
const authFunctions = this.reflector.getAllAndMerge<
|
||||
AuthenticationFunction[]
|
||||
>(AUTH_FUNCTION_KEY, [ctx.getClass(), ctx.getHandler()]);
|
||||
const mustBeAuthenticated = authFunctions.length > 0;
|
||||
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const accessToken = this.verifyToken(request, mustBeAuthenticated);
|
||||
const accessToken = this.validateToken(request, mustBeAuthenticated);
|
||||
|
||||
if (!mustBeAuthenticated) {
|
||||
// no need to be authenticated
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
// couldn't load valid token
|
||||
|
||||
if (!mustBeAuthenticated) {
|
||||
// no need to be authenticated
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new ErrorBuilder(ErrorCodes.AUTH.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof customAuthenticationFunction === 'function' &&
|
||||
!customAuthenticationFunction(request, request.user)
|
||||
) {
|
||||
// custom authentication function forbidden this request
|
||||
throw new ErrorBuilder(ErrorCodes.AUTH.FORBIDDEN);
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(requiredPermissions) &&
|
||||
requiredPermissions.length > 0 &&
|
||||
!this.matchPermissions(requiredPermissions, accessToken.permissions)
|
||||
) {
|
||||
// couldn't match permissions
|
||||
// every authentication function must return true to authenticate
|
||||
if (!authFunctions.every((func) => func(request, accessToken))) {
|
||||
throw new ErrorBuilder(ErrorCodes.AUTH.FORBIDDEN);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private verifyToken(request, mustBeAuthenticated: boolean) {
|
||||
private validateToken(
|
||||
request,
|
||||
mustBeAuthenticated: boolean,
|
||||
): RequestUser | false {
|
||||
const accessToken = request.get('Authorization');
|
||||
let accessTokenPayload;
|
||||
let accessTokenPayload: RequestUser;
|
||||
|
||||
// If the user isn't authenticated, an error will occurr anywhere here.
|
||||
// Fancy error avoidance isn't performed by purpose, such as avoiding to access null values.
|
||||
@@ -154,13 +133,4 @@ export class AuthenticationGuard
|
||||
|
||||
return accessTokenPayload;
|
||||
}
|
||||
|
||||
private matchPermissions(
|
||||
requiredPermissions: Permission[],
|
||||
userPermissions: number[],
|
||||
) {
|
||||
return requiredPermissions.some((permission) =>
|
||||
userPermissions.includes(permission.seqid),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
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 { Controller, Get } from '@nestjs/common';
|
||||
import { DadosferaLogger } from 'dadosfera-logs';
|
||||
import { AuthenticationGuard } from './authentication.guard';
|
||||
import { AuthClientService } from '../clients/auth/client.service';
|
||||
|
||||
import ErrorCodes from '../utils/errorCodes';
|
||||
import { User } from './user.decorator';
|
||||
import { Permissions } from './permissions.enum';
|
||||
|
||||
@Controller('user')
|
||||
class UserController {
|
||||
@Get()
|
||||
public getUser(@User() user) {
|
||||
return { user };
|
||||
}
|
||||
|
||||
@Get('options')
|
||||
public getUserWithOptions(@User({}) user) {
|
||||
return { user };
|
||||
}
|
||||
|
||||
@Get('not-required')
|
||||
public getUserNotRequired(@User({ required: false }) user) {
|
||||
return { user };
|
||||
}
|
||||
|
||||
@Get('required')
|
||||
public getUserRequired(@User({ required: true }) user) {
|
||||
return { user };
|
||||
}
|
||||
}
|
||||
|
||||
describe('user.decorator', () => {
|
||||
let app: INestApplication;
|
||||
const jwt_secret = {
|
||||
kid: 'token-testing-shared-key',
|
||||
pem: '$tr0ng-SH4Red-secr3t!!!~gl0ba1~]',
|
||||
};
|
||||
const fakeUserPayload = {
|
||||
user_id: 'd50d33c7-6c2b-463c-861f-e21667e7c125',
|
||||
username: 'super.admin',
|
||||
permissions: [Permissions.METABASE.OPEN].map(({ seqid }) => seqid),
|
||||
customer_id: '9d18e8ae-24b9-41a3-9e8f-a25ce57555b11',
|
||||
customer_name: 'dadosfera',
|
||||
customer_tier: 'BASIC',
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: AuthClientService,
|
||||
useValue: {
|
||||
getPublicKeys: async () => ({
|
||||
keys: [jwt_secret],
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: DadosferaLogger,
|
||||
useValue: { logger: console },
|
||||
},
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: AuthenticationGuard,
|
||||
},
|
||||
],
|
||||
controllers: [UserController],
|
||||
}).compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
function AssertNoAuth(res: request.Response) {
|
||||
expect(res.statusCode).toBe(HttpStatus.OK);
|
||||
expect(res.body).toStrictEqual({});
|
||||
}
|
||||
|
||||
function AssertWithAuth(res: request.Response) {
|
||||
expect(res.statusCode).toBe(HttpStatus.OK);
|
||||
expect(res.body).toStrictEqual({ user: fakeUserPayload });
|
||||
}
|
||||
|
||||
function AssertRequiredNoAuth(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 UserTest(accessToken: string) {
|
||||
describe(`with${accessToken ? '' : 'out'} token`, () => {
|
||||
it('should GET /user', async () => {
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/user')
|
||||
.set({ Authorization: accessToken });
|
||||
|
||||
if (accessToken) {
|
||||
AssertWithAuth(res);
|
||||
} else {
|
||||
AssertNoAuth(res);
|
||||
}
|
||||
});
|
||||
|
||||
it('should GET /user/options', async () => {
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/user/options')
|
||||
.set({ Authorization: accessToken });
|
||||
|
||||
if (accessToken) {
|
||||
AssertWithAuth(res);
|
||||
} else {
|
||||
AssertNoAuth(res);
|
||||
}
|
||||
});
|
||||
|
||||
it('should GET /user/not-required', async () => {
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/user/not-required')
|
||||
.set({ Authorization: accessToken });
|
||||
|
||||
if (accessToken) {
|
||||
AssertWithAuth(res);
|
||||
} else {
|
||||
AssertNoAuth(res);
|
||||
}
|
||||
});
|
||||
|
||||
it('should GET /user/required if logged', async () => {
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/user/required')
|
||||
.set({ Authorization: accessToken });
|
||||
|
||||
if (accessToken) {
|
||||
AssertWithAuth(res);
|
||||
} else {
|
||||
AssertRequiredNoAuth(res);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function CreateToken(overrides?) {
|
||||
const tokenPayload = {
|
||||
...fakeUserPayload,
|
||||
token_use: 'access',
|
||||
...overrides?.user,
|
||||
};
|
||||
|
||||
return jwt.sign(tokenPayload, jwt_secret.pem, {
|
||||
keyid: jwt_secret.kid,
|
||||
...overrides?.jwt,
|
||||
});
|
||||
}
|
||||
|
||||
UserTest(null);
|
||||
|
||||
const token = CreateToken();
|
||||
UserTest(token);
|
||||
});
|
||||
@@ -6,18 +6,19 @@ import ErrorCodes from '../utils/errorCodes';
|
||||
export interface RequestUser {
|
||||
user_id: string;
|
||||
username: string;
|
||||
permissions: string;
|
||||
permissions: number[];
|
||||
customer_id: string;
|
||||
customer_name: string;
|
||||
customer_tier: string;
|
||||
}
|
||||
|
||||
export const User = createParamDecorator((data: any, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
export const User: (options?: { required?: boolean }) => ParameterDecorator =
|
||||
createParamDecorator((options: any, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
|
||||
if (!request.user && data?.required) {
|
||||
throw new ErrorBuilder(ErrorCodes.AUTH.UNAUTHORIZED);
|
||||
}
|
||||
if (!request.user && options?.required) {
|
||||
throw new ErrorBuilder(ErrorCodes.AUTH.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
return request.user;
|
||||
});
|
||||
return request.user;
|
||||
});
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { HttpExceptionFilter } from './http-exception.filter';
|
||||
|
||||
describe('HttpExceptionFilter', () => {
|
||||
it('should be defined', () => {
|
||||
expect(new HttpExceptionFilter()).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
describe('PipelinesGrpcServerService', () => {
|
||||
it('should be defined', () => {
|
||||
expect(2 + 2).toBe(4);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
describe('PipelinesGrpcServerService', () => {
|
||||
it('should be defined', () => {
|
||||
expect(2 + 2).toBe(4);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
describe('PipelinesGrpcServerService', () => {
|
||||
it('should be defined', () => {
|
||||
expect(2 + 2).toBe(4);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
describe('PipelinesGrpcServerService', () => {
|
||||
it('should be defined', () => {
|
||||
expect(2 + 2).toBe(4);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
describe('PipelinesGrpcServerService', () => {
|
||||
it('should be defined', () => {
|
||||
expect(2 + 2).toBe(4);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user