diff --git a/.github/workflows/authz_ci.yml b/.github/workflows/authz_ci.yml index e9452a3bb..e8ead7ad5 100644 --- a/.github/workflows/authz_ci.yml +++ b/.github/workflows/authz_ci.yml @@ -40,7 +40,6 @@ jobs: shell: bash run: | make authz/copy-default-env - make authz/test/db/setup make authz/db/generate-types - name: Test types @@ -56,7 +55,6 @@ jobs: - name: Test integration shell: bash run: | - make authz/test/db/setup make authz/test/integration # - name: Test E2E diff --git a/apps/authz/Makefile b/apps/authz/Makefile index 1b4ff9585..9bb9849e0 100644 --- a/apps/authz/Makefile +++ b/apps/authz/Makefile @@ -52,7 +52,6 @@ authz/db/setup: prisma migrate reset \ --schema ${AUTHZ_DATABASE_SCHEMA} \ --force - make authz/db/seed @echo "" @echo "${TERM_GREEN}🛠️ Setting up Authz test database${TERM_NO_COLOR}" @@ -65,23 +64,8 @@ authz/db/create-migration: --schema ${AUTHZ_DATABASE_SCHEMA} \ --name ${NAME} -# To maintain seed data within their respective modules and then import them -# into the main seed.ts file for execution, it's necessary to compile the -# project and resolve its path aliases before running the vanilla JavaScript -# seed entry point. -authz/db/seed: - npx dotenv -e ${AUTHZ_PROJECT_DIR}/.env -- \ - ts-node -r tsconfig-paths/register --project ${AUTHZ_PROJECT_DIR}/tsconfig.app.json ${AUTHZ_PROJECT_DIR}/src/shared/module/persistence/seed.ts - # === Testing === -authz/test/db/setup: - npx dotenv -e ${AUTHZ_PROJECT_DIR}/.env.test --override -- \ - prisma migrate reset \ - --schema ${AUTHZ_DATABASE_SCHEMA} \ - --skip-seed \ - --force - authz/test/type: make authz/db/generate-types npx tsc \ @@ -106,6 +90,11 @@ authz/test/e2e: authz/test/e2e/watch: make authz/test/e2e ARGS=--watch +authz/test: + make authz/test/unit + make authz/test/integration + make authz/test/e2e + # === Open Policy Agent & Rego === authz/rego/build: diff --git a/apps/authz/src/app/__test__/e2e/admin.spec.ts b/apps/authz/src/app/__test__/e2e/admin.spec.ts index 0859d891d..1d737e7ba 100644 --- a/apps/authz/src/app/__test__/e2e/admin.spec.ts +++ b/apps/authz/src/app/__test__/e2e/admin.spec.ts @@ -1,26 +1,18 @@ -import { - AccountClassification, - AccountType, - Action, - Alg, - EntityType, - Signature, - UserRole, - ValueOperators -} from '@narval/authz-shared' +import { Action, Alg, EntityType, FIXTURE, Signature, UserRole, ValueOperators } from '@narval/authz-shared' import { Intents } from '@narval/transaction-request-intent' import { HttpStatus, INestApplication } from '@nestjs/common' import { ConfigModule } from '@nestjs/config' import { Test, TestingModule } from '@nestjs/testing' import { readFileSync, unlinkSync } from 'fs' +import { mock } from 'jest-mock-extended' import request from 'supertest' import { AppModule } from '../../../app/app.module' -import { AAUser, AAUser_Credential_1 } from '../../../app/persistence/repository/mock_data' import { PersistenceModule } from '../../../shared/module/persistence/persistence.module' import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' import { Organization } from '../../../shared/types/entities.types' import { Criterion, Then, TimeWindow } from '../../../shared/types/policy.type' import { load } from '../../app.config' +import { EntityRepository } from '../../persistence/repository/entity.repository' const REQUEST_HEADER_ORG_ID = 'x-org-id' describe('Admin Endpoints', () => { @@ -53,6 +45,9 @@ describe('Admin Endpoints', () => { } beforeAll(async () => { + const entityRepositoryMock = mock() + entityRepositoryMock.fetch.mockResolvedValue(FIXTURE.ENTITIES) + module = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ @@ -62,9 +57,13 @@ describe('Admin Endpoints', () => { PersistenceModule, AppModule ] - }).compile() + }) + .overrideProvider(EntityRepository) + .useValue(entityRepositoryMock) + .compile() testPrismaService = module.get(TestPrismaService) + app = module.createNestApplication() await app.init() @@ -84,371 +83,6 @@ describe('Admin Endpoints', () => { await testPrismaService.truncateAll() }) - describe('POST /admin/organizations', () => { - it('creates a new organization', async () => { - // Clear the db since we create an org in beforeEach - await testPrismaService.truncateAll() - - const payload = { - authentication, - approvals, - request: { - action: Action.CREATE_ORGANIZATION, - nonce: 'random-nonce-111', - organization: { - uid: org.uid, - credential: AAUser_Credential_1 - } - } - } - - const { status, body } = await request(app.getHttpServer()) - .post('/admin/organizations') - .set(REQUEST_HEADER_ORG_ID, org.uid) - .send(payload) - - expect(status).toEqual(HttpStatus.CREATED) - expect(body).toMatchObject({ organization: org }) - }) - }) - - describe('POST /admin/users', () => { - it('creates a new user & credential', async () => { - const payload = { - authentication, - approvals, - request: { - action: Action.CREATE_USER, - nonce: 'random-nonce-111', - user: { - ...AAUser, - credential: AAUser_Credential_1 - } - } - } - - const { status, body } = await request(app.getHttpServer()) - .post('/admin/users') - .set(REQUEST_HEADER_ORG_ID, org.uid) - .send(payload) - - expect(body).toMatchObject({ - user: AAUser - }) - expect(status).toEqual(HttpStatus.CREATED) - }) - - it('creates a new user without credential', async () => { - const payload = { - authentication, - approvals, - request: { - action: Action.CREATE_USER, - nonce: 'random-nonce-111', - user: AAUser - } - } - - const { status, body } = await request(app.getHttpServer()) - .post('/admin/users') - .set(REQUEST_HEADER_ORG_ID, org.uid) - .send(payload) - - expect(body).toMatchObject({ - user: AAUser - }) - expect(status).toEqual(HttpStatus.CREATED) - }) - - it('errors on duplicate', async () => { - expect.assertions(3) - - const payload = { - authentication, - approvals, - request: { - action: Action.CREATE_USER, - nonce: 'random-nonce-111', - user: AAUser - } - } - - const { status, body } = await request(app.getHttpServer()) - .post('/admin/users') - .set(REQUEST_HEADER_ORG_ID, org.uid) - .send(payload) - - // Repeat it - const { status: duplicateStatus } = await request(app.getHttpServer()) - .post('/admin/users') - .set(REQUEST_HEADER_ORG_ID, org.uid) - .send(payload) - - expect(body).toMatchObject({ user: AAUser }) - expect(status).toEqual(HttpStatus.CREATED) - expect(duplicateStatus).toEqual(HttpStatus.INTERNAL_SERVER_ERROR) - }) - }) - - describe('PATCH /admin/users/:uid', () => { - it('updates a user', async () => { - // First, insert the user who is an ADMIN - await testPrismaService.getClient().user.create({ - data: { - ...AAUser, - role: UserRole.ADMIN - } - }) - const payload = { - authentication, - approvals, - request: { - action: Action.UPDATE_USER, - nonce: 'random-nonce-111', - user: { - ...AAUser, - role: UserRole.MEMBER - } - } - } - - const { status, body } = await request(app.getHttpServer()) - .patch(`/admin/users/${AAUser.uid}`) - .set(REQUEST_HEADER_ORG_ID, org.uid) - .send(payload) - - expect(body).toMatchObject({ - user: { - ...AAUser, - role: UserRole.MEMBER - } - }) - expect(status).toEqual(HttpStatus.OK) - }) - }) - - describe('POST /admin/credentials', () => { - it(`creates a new credential`, async () => { - const payload = { - authentication, - approvals, - request: { - nonce: 'random-nonce-111', - action: Action.CREATE_CREDENTIAL, - credential: AAUser_Credential_1 - } - } - - const { status, body } = await request(app.getHttpServer()) - .post('/admin/credentials') - .set(REQUEST_HEADER_ORG_ID, org.uid) - .send(payload) - - expect(body).toMatchObject({ - credential: AAUser_Credential_1 - }) - expect(status).toEqual(HttpStatus.CREATED) - }) - }) - - describe('POST /user-groups', () => { - it('creates a new user group', async () => { - const payload = { - authentication, - approvals, - request: { - nonce: 'random-nonce-111', - action: Action.ASSIGN_USER_GROUP, - data: { - userId: AAUser.uid, - groupId: 'test-user-group-uid' - } - } - } - - const { status, body } = await request(app.getHttpServer()) - .post('/admin/user-groups') - .set(REQUEST_HEADER_ORG_ID, org.uid) - .send(payload) - - expect(body).toMatchObject({ - data: { - userId: AAUser.uid, - groupId: 'test-user-group-uid' - } - }) - expect(status).toEqual(HttpStatus.CREATED) - }) - }) - - describe('POST /wallets', () => { - it('creates a new wallet', async () => { - // TODO: This data _should_ fail a test later once we add validations. - const payload = { - authentication, - approvals, - request: { - nonce: 'random-nonce-111', - action: Action.REGISTER_WALLET, - wallet: { - uid: 'test-wallet-uid', - address: '0x1234', - accountType: AccountType.EOA, - chainId: 1 - } - } - } - - const { status, body } = await request(app.getHttpServer()) - .post('/admin/wallets') - .set(REQUEST_HEADER_ORG_ID, org.uid) - .send(payload) - - expect(body).toMatchObject({ - wallet: { - uid: 'test-wallet-uid', - address: '0x1234', - accountType: AccountType.EOA, - chainId: 1 - } - }) - expect(status).toEqual(HttpStatus.CREATED) - }) - }) - - describe('POST /wallet-groups', () => { - it('creates a new wallet group', async () => { - const payload = { - authentication, - approvals, - request: { - nonce: 'random-nonce-111', - action: Action.ASSIGN_WALLET_GROUP, - data: { - walletId: 'test-wallet-uid', - groupId: 'test-wallet-group-uid' - } - } - } - - const { status, body } = await request(app.getHttpServer()) - .post('/admin/wallet-groups') - .set(REQUEST_HEADER_ORG_ID, org.uid) - .send(payload) - - expect(body).toMatchObject({ - data: { - walletId: 'test-wallet-uid', - groupId: 'test-wallet-group-uid' - } - }) - expect(status).toEqual(HttpStatus.CREATED) - }) - }) - - describe('POST /user-wallets', () => { - it('creates a new user wallet', async () => { - const payload = { - authentication, - approvals, - request: { - nonce: 'random-nonce-111', - action: Action.ASSIGN_USER_WALLET, - data: { - userId: AAUser.uid, - walletId: 'test-wallet-uid' - } - } - } - - const { status, body } = await request(app.getHttpServer()) - .post('/admin/user-wallets') - .set(REQUEST_HEADER_ORG_ID, org.uid) - .send(payload) - - expect(body).toMatchObject({ - data: { - userId: AAUser.uid, - walletId: 'test-wallet-uid' - } - }) - expect(status).toEqual(HttpStatus.CREATED) - }) - }) - - describe('POST /address-book', () => { - it('creates a new address book entry', async () => { - const payload = { - authentication, - approvals, - request: { - nonce: 'random-nonce-111', - action: Action.CREATE_ADDRESS_BOOK_ACCOUNT, - account: { - uid: 'test-address-book-uid', - address: '0x1234', - chainId: 1, - classification: AccountClassification.INTERNAL - } - } - } - - const { status, body } = await request(app.getHttpServer()) - .post('/admin/address-book') - .set(REQUEST_HEADER_ORG_ID, org.uid) - .send(payload) - - expect(body).toMatchObject({ - account: { - uid: 'test-address-book-uid', - address: '0x1234', - chainId: 1, - classification: AccountClassification.INTERNAL - } - }) - expect(status).toEqual(HttpStatus.CREATED) - }) - }) - - describe('POST /tokens', () => { - it('registers new tokens', async () => { - const payload = { - authentication, - approvals, - request: { - nonce: 'random-nonce', - action: Action.REGISTER_TOKENS, - tokens: [ - { - uid: 'test-token-uid', - address: '0x1234', - chainId: 1, - symbol: 'TT', - decimals: 18 - }, - { - uid: 'test-token-uid-2', - address: '0x1234', - chainId: 137, - symbol: 'TT2', - decimals: 6 - } - ] - } - } - - const { status, body } = await request(app.getHttpServer()) - .post('/admin/tokens') - .set(REQUEST_HEADER_ORG_ID, org.uid) - .send(payload) - - expect(body).toMatchObject({ - tokens: payload.request.tokens - }) - expect(status).toEqual(HttpStatus.CREATED) - }) - }) - describe('POST /policies', () => { it('sets the organization policies', async () => { const payload = { diff --git a/apps/authz/src/app/__test__/integration/example.spec.ts b/apps/authz/src/app/__test__/integration/example.spec.ts deleted file mode 100644 index 2f673e194..000000000 --- a/apps/authz/src/app/__test__/integration/example.spec.ts +++ /dev/null @@ -1,5 +0,0 @@ -describe('Example integration test', () => { - it('foo', () => { - expect(1).toEqual(1) - }) -}) diff --git a/apps/authz/src/app/app.module.ts b/apps/authz/src/app/app.module.ts index e16e91047..18d5c76af 100644 --- a/apps/authz/src/app/app.module.ts +++ b/apps/authz/src/app/app.module.ts @@ -1,3 +1,4 @@ +import { HttpModule } from '@nestjs/axios' import { Module, ValidationPipe } from '@nestjs/common' import { ConfigModule } from '@nestjs/config' import { APP_PIPE } from '@nestjs/core' @@ -8,7 +9,7 @@ import { AppService } from './app.service' import { AdminService } from './core/admin.service' import { AdminController } from './http/rest/controller/admin.controller' import { OpaService } from './opa/opa.service' -import { AdminRepository } from './persistence/repository/admin.repository' +import { EntityRepository } from './persistence/repository/entity.repository' @Module({ imports: [ @@ -16,14 +17,15 @@ import { AdminRepository } from './persistence/repository/admin.repository' load: [load], isGlobal: true }), + HttpModule, PersistenceModule ], controllers: [AppController, AdminController], providers: [ AppService, AdminService, - AdminRepository, OpaService, + EntityRepository, { provide: APP_PIPE, useClass: ValidationPipe diff --git a/apps/authz/src/app/app.service.ts b/apps/authz/src/app/app.service.ts index 62e3123d1..2fda5661b 100644 --- a/apps/authz/src/app/app.service.ts +++ b/apps/authz/src/app/app.service.ts @@ -11,14 +11,20 @@ import { hashRequest } from '@narval/authz-shared' import { safeDecode } from '@narval/transaction-request-intent' -import { Injectable } from '@nestjs/common' +import { + BadRequestException, + Injectable, + InternalServerErrorException, + NotFoundException, + UnprocessableEntityException +} from '@nestjs/common' import { InputType } from 'packages/transaction-request-intent/src/lib/domain' import { Intent } from 'packages/transaction-request-intent/src/lib/intent.types' import { Hex, verifyMessage } from 'viem' import { privateKeyToAccount } from 'viem/accounts' -import { AdminRepository } from '../app/persistence/repository/admin.repository' import { OpaResult, RegoInput } from '../shared/types/domain.type' import { OpaService } from './opa/opa.service' +import { EntityRepository } from './persistence/repository/entity.repository' const ENGINE_PRIVATE_KEY = '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5' @@ -62,11 +68,16 @@ export const finalizeDecision = (response: OpaResult[]) => { @Injectable() export class AppService { - constructor(private adminRepository: AdminRepository, private opaService: OpaService) {} + constructor(private opaService: OpaService, private entityRepository: EntityRepository) {} async #verifySignature(requestSignature: Signature, verificationMessage: string): Promise { const { pubKey, alg, sig } = requestSignature - const credential = await this.adminRepository.getCredentialForPubKey(pubKey) + const credential = this.entityRepository.getCredentialForPubKey(pubKey) + + if (!credential) { + throw new NotFoundException('Credential not found') + } + if (alg === Alg.ES256K) { // TODO: ensure sig & pubkey begins with 0x const signature = sig.startsWith('0x') ? sig : `0x${sig}` @@ -82,10 +93,9 @@ export class AppService { sig }) - throw new Error('Invalid Signature') + throw new BadRequestException('Invalid signature') } } - // TODO: verify other alg types return credential } @@ -133,7 +143,7 @@ export class AppService { } } - throw new Error(`Unsupported action ${request.action}`) + throw new InternalServerErrorException(`Unsupported action ${request.action}`) } /** @@ -165,7 +175,7 @@ export class AppService { : undefined if (intentResult?.success === false) { - throw new Error(`Could not decode intent: ${intentResult.error.message}`) + throw new UnprocessableEntityException(`Could not decode intent: ${intentResult.error.message}`) } const intent = intentResult?.intent diff --git a/apps/authz/src/app/core/admin.service.ts b/apps/authz/src/app/core/admin.service.ts index a798fe5d9..7d637749d 100644 --- a/apps/authz/src/app/core/admin.service.ts +++ b/apps/authz/src/app/core/admin.service.ts @@ -1,105 +1,10 @@ -import { - AssignUserGroupRequest, - AssignUserWalletRequest, - AssignWalletGroupRequest, - AuthCredential, - CreateAddressBookAccountRequest, - CreateCredentialRequest, - CreateOrganizationRequest, - CreateUserRequest, - RegisterTokensRequest, - RegisterWalletRequest, - UpdateUserRequest, - UserGroupMembership, - UserWallet, - WalletGroupMembership -} from '@narval/authz-shared' import { Injectable } from '@nestjs/common' -import { AddressBookAccount, Organization, Token, User, Wallet } from '../../shared/types/entities.types' import { Policy, SetPolicyRulesRequest } from '../../shared/types/policy.type' import { OpaService } from '../opa/opa.service' -import { AdminRepository } from '../persistence/repository/admin.repository' @Injectable() export class AdminService { - constructor(private adminRepository: AdminRepository, private opaService: OpaService) {} - - async createOrganization(payload: CreateOrganizationRequest): Promise<{ - organization: Organization - rootUser: User - rootCredential: AuthCredential - }> { - // TODO: Verify authentication (user) & approval sig (engine) - - const { uid: organizationId, credential: rootCredential } = payload.request.organization - - const data = await this.adminRepository.createOrganization(organizationId, rootCredential) - - // TODO: return api key? - return { - organization: data.organization, - rootUser: data.rootUser, - rootCredential: data.rootCredential - } - } - - async createUser(payload: CreateUserRequest): Promise { - const { uid, role, credential } = payload.request.user - const user = await this.adminRepository.createUser(uid, role, credential) - - return user - } - - async updateUser(payload: UpdateUserRequest): Promise { - const { uid, role } = payload.request.user - await this.adminRepository.assignUserRole(uid, role) - - return payload.request.user - } - - async createCredential(payload: CreateCredentialRequest): Promise { - // TODO: Should we generate the credential uid here to enforce that it's a hash of the pubKey? - await this.adminRepository.createAuthCredential(payload.request.credential) - - return payload.request.credential - } - - async assignUserGroup(payload: AssignUserGroupRequest): Promise { - await this.adminRepository.assignUserGroup(payload.request.data.userId, payload.request.data.groupId) - - return payload.request.data - } - - async registerWallet(payload: RegisterWalletRequest): Promise { - const { uid, address, accountType, chainId } = payload.request.wallet - await this.adminRepository.registerWallet(uid, address, accountType, chainId) - - return payload.request.wallet - } - - async assignWalletGroup(payload: AssignWalletGroupRequest): Promise { - await this.adminRepository.assignWalletGroup(payload.request.data.walletId, payload.request.data.groupId) - - return payload.request.data - } - - async assignUserWallet(payload: AssignUserWalletRequest): Promise { - await this.adminRepository.assignUserWallet(payload.request.data.userId, payload.request.data.walletId) - - return payload.request.data - } - - async createAddressBookAccount(payload: CreateAddressBookAccountRequest): Promise { - await this.adminRepository.createAddressBookAccount(payload.request.account) - - return payload.request.account - } - - async registerTokens(payload: RegisterTokensRequest): Promise { - await this.adminRepository.registerTokens(payload.request.tokens) - - return payload.request.tokens - } + constructor(private opaService: OpaService) {} async setPolicyRules(payload: SetPolicyRulesRequest): Promise<{ fileId: string; policies: Policy[] }> { const { fileId, policies } = this.opaService.buildPoliciesWasm(payload.request.data) diff --git a/apps/authz/src/app/evaluation-request.dto.ts b/apps/authz/src/app/evaluation-request.dto.ts index 171442e28..950c9d351 100644 --- a/apps/authz/src/app/evaluation-request.dto.ts +++ b/apps/authz/src/app/evaluation-request.dto.ts @@ -1,9 +1,16 @@ -import { AccessList, AccountId, Action, Address, FiatCurrency, Hex } from '@narval/authz-shared' +import { + AccessList, + AccountId, + Action, + Address, + BaseActionDto, + FiatCurrency, + Hex, + SignatureDto +} from '@narval/authz-shared' import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger' import { Transform, Type } from 'class-transformer' import { IsDefined, IsEthereumAddress, IsIn, IsOptional, IsString, ValidateNested } from 'class-validator' -import { BaseActionDto } from './http/rest/dto/base-action.dto' -import { RequestSignatureDto } from './http/rest/dto/request-signature.dto' export class TransactionRequestDto { @IsString() @@ -126,15 +133,15 @@ export class EvaluationRequestDto { @IsDefined() @ValidateNested() @ApiProperty() - authentication: RequestSignatureDto + authentication: SignatureDto @IsOptional() @ValidateNested() @ApiProperty({ - type: () => RequestSignatureDto, + type: () => SignatureDto, isArray: true }) - approvals?: RequestSignatureDto[] + approvals?: SignatureDto[] @ValidateNested() @Type((opts) => { diff --git a/apps/authz/src/app/http/rest/controller/admin.controller.ts b/apps/authz/src/app/http/rest/controller/admin.controller.ts index 18d20d7e6..1ed074cd7 100644 --- a/apps/authz/src/app/http/rest/controller/admin.controller.ts +++ b/apps/authz/src/app/http/rest/controller/admin.controller.ts @@ -1,146 +1,13 @@ -import { - AssignUserGroupRequest, - AssignUserWalletRequest, - AssignWalletGroupRequest, - CreateAddressBookAccountRequest, - CreateCredentialRequest, - CreateOrganizationRequest, - CreateUserRequest, - RegisterTokensRequest, - RegisterWalletRequest, - UpdateUserRequest -} from '@narval/authz-shared' -import { Body, Controller, Logger, Patch, Post } from '@nestjs/common' +import { Body, Controller, Post } from '@nestjs/common' import { SetPolicyRulesRequest } from '../../../../shared/types/policy.type' import { AdminService } from '../../../core/admin.service' -import { AssignUserGroupRequestDto } from '../dto/assign-user-group-request.dto' -import { AssignUserGroupResponseDto } from '../dto/assign-user-group-response.dto' -import { AssignUserWalletRequestDto } from '../dto/assign-user-wallet-request.dto' -import { AssignUserWalletResponseDto } from '../dto/assign-user-wallet-response.dto' -import { AssignWalletGroupRequestDto } from '../dto/assign-wallet-group-request.dto' -import { AssignWalletGroupResponseDto } from '../dto/assign-wallet-group-response.dto' -import { CreateAddressBookAccountRequestDto } from '../dto/create-address-book-request.dto' -import { CreateAddressBookAccountResponseDto } from '../dto/create-address-book-response.dto' -import { CreateCredentialRequestDto } from '../dto/create-credential-request.dto' -import { CreateCredentialResponseDto } from '../dto/create-credential-response.dto' -import { CreateOrganizationRequestDto } from '../dto/create-organization-request.dto' -import { CreateOrganizationResponseDto } from '../dto/create-organization-response.dto' -import { CreateUserRequestDto } from '../dto/create-user-request.dto' -import { CreateUserResponseDto } from '../dto/create-user-response.dto' import { SetPolicyRulesRequestDto } from '../dto/policy-rules/set-policy-rules-request.dto' import { SetPolicyRulesResponseDto } from '../dto/policy-rules/set-policy-rules-response.dto' -import { RegisterTokensRequestDto } from '../dto/register-tokens-request.dto' -import { RegisterTokensResponseDto } from '../dto/register-tokens-response.dto' -import { RegisterWalletRequestDto } from '../dto/register-wallet-request.dto' -import { RegisterWalletResponseDto } from '../dto/register-wallet-response.dto' -import { UpdateUserRequestDto } from '../dto/update-user-request.dto' -import { UpdateUserResponseDto } from '../dto/update-user-response.dto' @Controller('/admin') export class AdminController { - private logger = new Logger(AdminController.name) - constructor(private readonly adminService: AdminService) {} - @Post('/organizations') - async createOrganization(@Body() body: CreateOrganizationRequestDto) { - const payload: CreateOrganizationRequest = body - - const result = await this.adminService.createOrganization(payload) - - const response = new CreateOrganizationResponseDto(result.organization, result.rootCredential, result.rootUser) - return response - } - - @Post('/users') - async createUser(@Body() body: CreateUserRequestDto) { - const payload: CreateUserRequest = body - - const user = await this.adminService.createUser(payload) - - const response = new CreateUserResponseDto(user) - return response - } - - @Patch('/users/:uid') - async updateUser(@Body() body: UpdateUserRequestDto) { - const payload: UpdateUserRequest = body - - const user = await this.adminService.updateUser(payload) - - const response = new UpdateUserResponseDto(user) - return response - } - - @Post('/credentials') - async createCredential(@Body() body: CreateCredentialRequestDto) { - const payload: CreateCredentialRequest = body - - const authCredential = await this.adminService.createCredential(payload) - - const response = new CreateCredentialResponseDto(authCredential) - return response - } - - @Post('/user-groups') - async assignUserGroup(@Body() body: AssignUserGroupRequestDto) { - const payload: AssignUserGroupRequest = body - - const userGroup = await this.adminService.assignUserGroup(payload) - - const response = new AssignUserGroupResponseDto(userGroup) - return response - } - - @Post('/wallets') - async registerWallet(@Body() body: RegisterWalletRequestDto) { - const payload: RegisterWalletRequest = body - - const wallet = await this.adminService.registerWallet(payload) - - const response = new RegisterWalletResponseDto(wallet) - return response - } - - @Post('/wallet-groups') - async assignWalletGroup(@Body() body: AssignWalletGroupRequestDto) { - const payload: AssignWalletGroupRequest = body - - const wallet = await this.adminService.assignWalletGroup(payload) - - const response = new AssignWalletGroupResponseDto(wallet) - return response - } - - @Post('/user-wallets') - async assignUserWallet(@Body() body: AssignUserWalletRequestDto) { - const payload: AssignUserWalletRequest = body - - const userWallet = await this.adminService.assignUserWallet(payload) - - const response = new AssignUserWalletResponseDto(userWallet) - return response - } - - @Post('/address-book') - async createAddressBookEntry(@Body() body: CreateAddressBookAccountRequestDto) { - const payload: CreateAddressBookAccountRequest = body - - const addressBookAccount = await this.adminService.createAddressBookAccount(payload) - - const response = new CreateAddressBookAccountResponseDto(addressBookAccount) - return response - } - - @Post('/tokens') - async registerTokens(@Body() body: RegisterTokensRequestDto) { - const payload: RegisterTokensRequest = body - const tokens = await this.adminService.registerTokens(payload) - - const response = new RegisterTokensResponseDto(tokens) - return response - } - @Post('/policies') async setPolicyRules(@Body() body: SetPolicyRulesRequestDto) { const payload: SetPolicyRulesRequest = body diff --git a/apps/authz/src/app/http/rest/dto/address-book-account-dto.ts b/apps/authz/src/app/http/rest/dto/address-book-account-dto.ts deleted file mode 100644 index 446c4038d..000000000 --- a/apps/authz/src/app/http/rest/dto/address-book-account-dto.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { AccountClassification, Address } from '@narval/authz-shared' -import { ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsIn, IsNumber, IsString } from 'class-validator' -import { AddressBookAccount } from '../../../../shared/types/entities.types' - -export class AddressBookAccountDataDto { - constructor(addressBookAccount: AddressBookAccount) { - this.uid = addressBookAccount.uid - this.classification = addressBookAccount.classification - this.address = addressBookAccount.address - this.chainId = addressBookAccount.chainId - } - - @IsString() - @IsDefined() - @ApiProperty() - uid: string - - @IsIn(Object.values(AccountClassification)) - @IsDefined() - @ApiProperty({ - enum: Object.values(AccountClassification) - }) - classification: AccountClassification - - @IsString() - @IsDefined() - @ApiProperty() - address: Address - - @IsNumber() - @IsDefined() - @ApiProperty() - chainId: number -} diff --git a/apps/authz/src/app/http/rest/dto/assign-user-group-request.dto.ts b/apps/authz/src/app/http/rest/dto/assign-user-group-request.dto.ts deleted file mode 100644 index 61e6ec9d5..000000000 --- a/apps/authz/src/app/http/rest/dto/assign-user-group-request.dto.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Action, UserGroupMembership } from '@narval/authz-shared' -import { ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsIn, ValidateNested } from 'class-validator' -import { BaseActionDto } from './base-action.dto' -import { BaseAdminRequestPayloadDto } from './base-admin-request-payload.dto' - -class AssignUserGroupActionDto extends BaseActionDto { - @IsIn(Object.values(Action)) - @IsDefined() - @ApiProperty({ - enum: Object.values(Action), - default: Action.ASSIGN_USER_GROUP - }) - action: typeof Action.ASSIGN_USER_GROUP - - @IsDefined() - @ValidateNested() - @ApiProperty() - data: UserGroupMembership -} - -export class AssignUserGroupRequestDto extends BaseAdminRequestPayloadDto { - @IsDefined() - @ValidateNested() - @ApiProperty() - request: AssignUserGroupActionDto -} diff --git a/apps/authz/src/app/http/rest/dto/assign-user-wallet-request.dto.ts b/apps/authz/src/app/http/rest/dto/assign-user-wallet-request.dto.ts deleted file mode 100644 index 6394b3d78..000000000 --- a/apps/authz/src/app/http/rest/dto/assign-user-wallet-request.dto.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Action, UserWallet } from '@narval/authz-shared' -import { ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsIn, ValidateNested } from 'class-validator' -import { BaseActionDto } from './base-action.dto' -import { BaseAdminRequestPayloadDto } from './base-admin-request-payload.dto' - -class AssignUserWalletActionDto extends BaseActionDto { - @IsIn(Object.values(Action)) - @IsDefined() - @ApiProperty({ - enum: Object.values(Action), - default: Action.ASSIGN_USER_WALLET - }) - action: typeof Action.ASSIGN_USER_WALLET - - @IsDefined() - @ValidateNested() - @ApiProperty() - data: UserWallet -} - -export class AssignUserWalletRequestDto extends BaseAdminRequestPayloadDto { - @IsDefined() - @ValidateNested() - @ApiProperty() - request: AssignUserWalletActionDto -} diff --git a/apps/authz/src/app/http/rest/dto/assign-wallet-group-request.dto.ts b/apps/authz/src/app/http/rest/dto/assign-wallet-group-request.dto.ts deleted file mode 100644 index 3697a133d..000000000 --- a/apps/authz/src/app/http/rest/dto/assign-wallet-group-request.dto.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Action, WalletGroupMembership } from '@narval/authz-shared' -import { ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsIn, ValidateNested } from 'class-validator' -import { BaseActionDto } from './base-action.dto' -import { BaseAdminRequestPayloadDto } from './base-admin-request-payload.dto' - -class AssignWalletGroupActionDto extends BaseActionDto { - @IsIn(Object.values(Action)) - @IsDefined() - @ApiProperty({ - enum: Object.values(Action), - default: Action.ASSIGN_WALLET_GROUP - }) - action: typeof Action.ASSIGN_WALLET_GROUP - - @IsDefined() - @ValidateNested() - @ApiProperty() - data: WalletGroupMembership -} - -export class AssignWalletGroupRequestDto extends BaseAdminRequestPayloadDto { - @IsDefined() - @ValidateNested() - @ApiProperty() - request: AssignWalletGroupActionDto -} diff --git a/apps/authz/src/app/http/rest/dto/auth-credential.dto.ts b/apps/authz/src/app/http/rest/dto/auth-credential.dto.ts deleted file mode 100644 index eb010bf0a..000000000 --- a/apps/authz/src/app/http/rest/dto/auth-credential.dto.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Alg } from '@narval/authz-shared' -import { ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsIn, IsString } from 'class-validator' - -export class AuthCredentialDto { - constructor(data: AuthCredentialDto) { - this.uid = data.uid - this.pubKey = data.pubKey - this.alg = data.alg - this.userId = data.userId - } - - @IsString() - @IsDefined() - @ApiProperty() - uid: string - - @IsString() - @IsDefined() - @ApiProperty() - pubKey: string - - @IsIn(Object.values(Alg)) - @IsDefined() - @ApiProperty({ enum: Object.values(Alg) }) - alg: Alg - - @IsString() - @IsDefined() - @ApiProperty() - userId: string -} diff --git a/apps/authz/src/app/http/rest/dto/base-admin-request-payload.dto.ts b/apps/authz/src/app/http/rest/dto/base-admin-request-payload.dto.ts deleted file mode 100644 index bd91342d6..000000000 --- a/apps/authz/src/app/http/rest/dto/base-admin-request-payload.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ApiExtraModels, ApiProperty } from '@nestjs/swagger' -import { ArrayNotEmpty, IsDefined, ValidateNested } from 'class-validator' -import { RequestSignatureDto } from './request-signature.dto' - -@ApiExtraModels(RequestSignatureDto) -export class BaseAdminRequestPayloadDto { - @IsDefined() - @ApiProperty() - authentication: RequestSignatureDto - - @IsDefined() - @ArrayNotEmpty() - @ValidateNested() - @ApiProperty({ - type: () => RequestSignatureDto, - isArray: true - }) - approvals: RequestSignatureDto[] -} diff --git a/apps/authz/src/app/http/rest/dto/create-address-book-request.dto.ts b/apps/authz/src/app/http/rest/dto/create-address-book-request.dto.ts deleted file mode 100644 index daf4ee6a5..000000000 --- a/apps/authz/src/app/http/rest/dto/create-address-book-request.dto.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Action } from '@narval/authz-shared' -import { ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsIn, ValidateNested } from 'class-validator' -import { AddressBookAccountDataDto } from './address-book-account-dto' -import { BaseActionDto } from './base-action.dto' -import { BaseAdminRequestPayloadDto } from './base-admin-request-payload.dto' - -class CreateAddressBookAccountActionDto extends BaseActionDto { - @IsIn(Object.values(Action)) - @IsDefined() - @ApiProperty({ - enum: Object.values(Action), - default: Action.CREATE_ADDRESS_BOOK_ACCOUNT - }) - action: typeof Action.CREATE_ADDRESS_BOOK_ACCOUNT - - @IsDefined() - @ValidateNested() - @ApiProperty() - account: AddressBookAccountDataDto -} - -export class CreateAddressBookAccountRequestDto extends BaseAdminRequestPayloadDto { - @IsDefined() - @ValidateNested() - @ApiProperty() - request: CreateAddressBookAccountActionDto -} diff --git a/apps/authz/src/app/http/rest/dto/create-address-book-response.dto.ts b/apps/authz/src/app/http/rest/dto/create-address-book-response.dto.ts deleted file mode 100644 index a72cbb2f6..000000000 --- a/apps/authz/src/app/http/rest/dto/create-address-book-response.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger' -import { IsDefined, ValidateNested } from 'class-validator' -import { AddressBookAccount } from '../../../../shared/types/entities.types' -import { AddressBookAccountDataDto } from './address-book-account-dto' - -export class CreateAddressBookAccountResponseDto { - constructor(account: AddressBookAccount) { - this.account = new AddressBookAccountDataDto(account) - } - - @IsDefined() - @ValidateNested() - @ApiProperty() - account: AddressBookAccountDataDto -} diff --git a/apps/authz/src/app/http/rest/dto/create-organization-response.dto.ts b/apps/authz/src/app/http/rest/dto/create-organization-response.dto.ts deleted file mode 100644 index cfc6b8f79..000000000 --- a/apps/authz/src/app/http/rest/dto/create-organization-response.dto.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { AuthCredential } from '@narval/authz-shared' -import { ApiExtraModels, ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsString, ValidateNested } from 'class-validator' -import { Organization, User } from '../../../../shared/types/entities.types' -import { AuthCredentialDto } from './auth-credential.dto' -import { UserDto } from './user-dto' - -class OrganizationDataDto { - @IsString() - @IsDefined() - @ApiProperty() - uid: string -} - -@ApiExtraModels(OrganizationDataDto, AuthCredentialDto) -export class CreateOrganizationResponseDto { - constructor(organization: Organization, rootCredential: AuthCredential, rootUser: User) { - this.organization = organization - this.rootCredential = rootCredential - this.rootUser = rootUser - } - - @IsDefined() - @ValidateNested() - @ApiProperty() - organization: OrganizationDataDto - rootCredential: AuthCredentialDto - rootUser: UserDto -} diff --git a/apps/authz/src/app/http/rest/dto/create-user-request.dto.ts b/apps/authz/src/app/http/rest/dto/create-user-request.dto.ts deleted file mode 100644 index a6230b8b3..000000000 --- a/apps/authz/src/app/http/rest/dto/create-user-request.dto.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Action, UserRole } from '@narval/authz-shared' -import { ApiExtraModels, ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsIn, IsOptional, IsString, ValidateNested } from 'class-validator' -import { AuthCredentialDto } from './auth-credential.dto' -import { BaseActionDto } from './base-action.dto' -import { BaseAdminRequestPayloadDto } from './base-admin-request-payload.dto' - -class CreateUserDataDto { - @IsString() - @IsDefined() - @ApiProperty() - uid: string - - @IsIn(Object.values(UserRole)) - @IsDefined() - @ApiProperty({ - enum: Object.values(UserRole) - }) - role: UserRole - - @IsString() - @IsOptional() - @ApiProperty() - @ValidateNested() - credential?: AuthCredentialDto -} - -class CreateUserActionDto extends BaseActionDto { - @IsIn(Object.values(Action)) - @IsDefined() - @ApiProperty({ - enum: Object.values(Action), - default: Action.CREATE_USER - }) - action: typeof Action.CREATE_USER - - @IsDefined() - @ValidateNested() - @ApiProperty() - user: CreateUserDataDto -} - -@ApiExtraModels(CreateUserActionDto, AuthCredentialDto) -export class CreateUserRequestDto extends BaseAdminRequestPayloadDto { - @IsDefined() - @ValidateNested() - @ApiProperty() - request: CreateUserActionDto -} diff --git a/apps/authz/src/app/http/rest/dto/create-user-response.dto.ts b/apps/authz/src/app/http/rest/dto/create-user-response.dto.ts deleted file mode 100644 index 960b1b9a3..000000000 --- a/apps/authz/src/app/http/rest/dto/create-user-response.dto.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ApiExtraModels, ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsString, ValidateNested } from 'class-validator' -import { User } from '../../../../shared/types/entities.types' -import { AuthCredentialDto } from './auth-credential.dto' -import { UserDto } from './user-dto' - -class OrganizationDataDto { - @IsString() - @IsDefined() - @ApiProperty() - uid: string -} - -@ApiExtraModels(OrganizationDataDto, AuthCredentialDto) -export class CreateUserResponseDto { - constructor(user: User) { - this.user = new UserDto(user) - } - - @IsDefined() - @ValidateNested() - @ApiProperty() - user: UserDto -} diff --git a/apps/authz/src/app/http/rest/dto/policy-rules/set-policy-rules-request.dto.ts b/apps/authz/src/app/http/rest/dto/policy-rules/set-policy-rules-request.dto.ts index 425555f02..6fe56d882 100644 --- a/apps/authz/src/app/http/rest/dto/policy-rules/set-policy-rules-request.dto.ts +++ b/apps/authz/src/app/http/rest/dto/policy-rules/set-policy-rules-request.dto.ts @@ -1,27 +1,25 @@ -import { Action } from '@narval/authz-shared' +import { Action, BaseActionDto, BaseActionRequestDto } from '@narval/authz-shared' import { ApiProperty } from '@nestjs/swagger' import { Type } from 'class-transformer' -import { IsArray, IsDefined, IsString, Matches, ValidateNested } from 'class-validator' +import { ArrayNotEmpty, IsDefined, Matches, ValidateNested } from 'class-validator' import { Policy } from '../../../../../shared/types/policy.type' -import { BaseActionDto } from '../base-action.dto' -import { BaseAdminRequestPayloadDto } from '../base-admin-request-payload.dto' export class SetPolicyRulesDto extends BaseActionDto { - @IsDefined() - @IsString() @Matches(Action.SET_POLICY_RULES) - @ApiProperty() + @ApiProperty({ + enum: [Action.SET_POLICY_RULES], + default: Action.SET_POLICY_RULES + }) action: typeof Action.SET_POLICY_RULES - @IsDefined() - @IsArray() + @ArrayNotEmpty() @Type(() => Policy) @ValidateNested({ each: true }) - @ApiProperty() + @ApiProperty({ type: [Policy] }) data: Policy[] } -export class SetPolicyRulesRequestDto extends BaseAdminRequestPayloadDto { +export class SetPolicyRulesRequestDto extends BaseActionRequestDto { @IsDefined() @ValidateNested() @Type(() => SetPolicyRulesDto) diff --git a/apps/authz/src/app/http/rest/dto/policy-rules/set-policy-rules-response.dto.ts b/apps/authz/src/app/http/rest/dto/policy-rules/set-policy-rules-response.dto.ts index 26cd19ab5..462b41ec5 100644 --- a/apps/authz/src/app/http/rest/dto/policy-rules/set-policy-rules-response.dto.ts +++ b/apps/authz/src/app/http/rest/dto/policy-rules/set-policy-rules-response.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger' import { Type } from 'class-transformer' -import { IsDefined, IsString, ValidateNested } from 'class-validator' +import { ArrayNotEmpty, IsDefined, IsString, ValidateNested } from 'class-validator' import { Policy } from '../../../../../shared/types/policy.type' export class SetPolicyRulesResponseDto { @@ -8,10 +8,10 @@ export class SetPolicyRulesResponseDto { @IsString() fileId: string - @IsDefined() + @ArrayNotEmpty() @Type(() => Policy) - @ValidateNested() - @ApiProperty({ type: () => Policy, isArray: true }) + @ValidateNested({ each: true }) + @ApiProperty({ type: [Policy] }) policies: Policy[] constructor(partial: Partial) { diff --git a/apps/authz/src/app/http/rest/dto/register-token-dto.ts b/apps/authz/src/app/http/rest/dto/register-token-dto.ts deleted file mode 100644 index c8b42e418..000000000 --- a/apps/authz/src/app/http/rest/dto/register-token-dto.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Address } from '@narval/authz-shared' -import { ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsNumber, IsString } from 'class-validator' -import { Token } from '../../../../shared/types/entities.types' - -export class TokenDataDto { - constructor(token: Token) { - this.uid = token.uid - this.address = token.address - this.chainId = token.chainId - this.symbol = token.symbol - this.decimals = token.decimals - } - - @IsString() - @IsDefined() - @ApiProperty() - uid: string - - @IsString() - @IsDefined() - @ApiProperty() - address: Address - - @IsNumber() - @IsDefined() - @ApiProperty() - chainId: number - - @IsString() - @IsDefined() - @ApiProperty() - symbol: string - - @IsNumber() - @IsDefined() - @ApiProperty() - decimals: number -} diff --git a/apps/authz/src/app/http/rest/dto/register-tokens-request.dto.ts b/apps/authz/src/app/http/rest/dto/register-tokens-request.dto.ts deleted file mode 100644 index 0f4c999c6..000000000 --- a/apps/authz/src/app/http/rest/dto/register-tokens-request.dto.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Action } from '@narval/authz-shared' -import { ApiProperty } from '@nestjs/swagger' -import { IsArray, IsDefined, IsIn, ValidateNested } from 'class-validator' -import { BaseActionDto } from './base-action.dto' -import { BaseAdminRequestPayloadDto } from './base-admin-request-payload.dto' -import { TokenDataDto } from './register-token-dto' - -class RegisterTokensActionDto extends BaseActionDto { - @IsIn(Object.values(Action)) - @IsDefined() - @ApiProperty({ - enum: Object.values(Action), - default: Action.REGISTER_TOKENS - }) - action: typeof Action.REGISTER_TOKENS - - @IsDefined() - @ValidateNested() - @IsArray() - @ApiProperty() - tokens: TokenDataDto[] -} - -export class RegisterTokensRequestDto extends BaseAdminRequestPayloadDto { - @IsDefined() - @ValidateNested() - @ApiProperty() - request: RegisterTokensActionDto -} diff --git a/apps/authz/src/app/http/rest/dto/register-tokens-response.dto.ts b/apps/authz/src/app/http/rest/dto/register-tokens-response.dto.ts deleted file mode 100644 index 016863d69..000000000 --- a/apps/authz/src/app/http/rest/dto/register-tokens-response.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger' -import { IsArray, IsDefined, ValidateNested } from 'class-validator' -import { Token } from '../../../../shared/types/entities.types' -import { TokenDataDto } from './register-token-dto' - -export class RegisterTokensResponseDto { - constructor(tokens: Token[]) { - this.tokens = tokens.map((token) => new TokenDataDto(token)) - } - - @IsDefined() - @ValidateNested() - @IsArray() - @ApiProperty() - tokens: TokenDataDto[] -} diff --git a/apps/authz/src/app/http/rest/dto/register-wallet-request.dto.ts b/apps/authz/src/app/http/rest/dto/register-wallet-request.dto.ts deleted file mode 100644 index 255d66ba7..000000000 --- a/apps/authz/src/app/http/rest/dto/register-wallet-request.dto.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Action } from '@narval/authz-shared' -import { ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsIn, ValidateNested } from 'class-validator' -import { BaseActionDto } from './base-action.dto' -import { BaseAdminRequestPayloadDto } from './base-admin-request-payload.dto' -import { WalletDataDto } from './wallet-dto' - -class RegisterWalletActionDto extends BaseActionDto { - @IsIn(Object.values(Action)) - @IsDefined() - @ApiProperty({ - enum: Object.values(Action), - default: Action.REGISTER_WALLET - }) - action: typeof Action.REGISTER_WALLET - - @IsDefined() - @ValidateNested() - @ApiProperty() - wallet: WalletDataDto -} - -export class RegisterWalletRequestDto extends BaseAdminRequestPayloadDto { - @IsDefined() - @ValidateNested() - @ApiProperty() - request: RegisterWalletActionDto -} diff --git a/apps/authz/src/app/http/rest/dto/register-wallet-response.dto.ts b/apps/authz/src/app/http/rest/dto/register-wallet-response.dto.ts deleted file mode 100644 index fba910197..000000000 --- a/apps/authz/src/app/http/rest/dto/register-wallet-response.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger' -import { IsDefined, ValidateNested } from 'class-validator' -import { Wallet } from '../../../../shared/types/entities.types' -import { WalletDataDto } from './wallet-dto' - -export class RegisterWalletResponseDto { - constructor(wallet: Wallet) { - this.wallet = new WalletDataDto(wallet) - } - - @IsDefined() - @ValidateNested() - @ApiProperty() - wallet: WalletDataDto -} diff --git a/apps/authz/src/app/http/rest/dto/update-user-request.dto.ts b/apps/authz/src/app/http/rest/dto/update-user-request.dto.ts deleted file mode 100644 index 7b17d9809..000000000 --- a/apps/authz/src/app/http/rest/dto/update-user-request.dto.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Action, UserRole } from '@narval/authz-shared' -import { ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsIn, IsString, ValidateNested } from 'class-validator' -import { BaseActionDto } from './base-action.dto' -import { BaseAdminRequestPayloadDto } from './base-admin-request-payload.dto' - -class UpdateUserDataDto { - @IsString() - @IsDefined() - @ApiProperty() - uid: string - - @IsIn(Object.values(UserRole)) - @IsDefined() - @ApiProperty({ - enum: Object.values(UserRole) - }) - role: UserRole -} - -class UpdateUserActionDto extends BaseActionDto { - @IsIn(Object.values(Action)) - @IsDefined() - @ApiProperty({ - enum: Object.values(Action), - default: Action.UPDATE_USER - }) - action: typeof Action.UPDATE_USER - - @IsDefined() - @ValidateNested() - @ApiProperty() - user: UpdateUserDataDto -} - -export class UpdateUserRequestDto extends BaseAdminRequestPayloadDto { - @IsDefined() - @ValidateNested() - @ApiProperty() - request: UpdateUserActionDto -} diff --git a/apps/authz/src/app/http/rest/dto/user-dto.ts b/apps/authz/src/app/http/rest/dto/user-dto.ts deleted file mode 100644 index 6f9fdf497..000000000 --- a/apps/authz/src/app/http/rest/dto/user-dto.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { UserRole } from '@narval/authz-shared' -import { ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsIn, IsString } from 'class-validator' -import { User } from '../../../../shared/types/entities.types' - -export class UserDto { - constructor(user: User) { - this.uid = user.uid - this.role = user.role - } - - @IsString() - @IsDefined() - @ApiProperty() - uid: string - - @IsIn(Object.values(UserRole)) - @IsDefined() - @ApiProperty({ - enum: Object.values(UserRole) - }) - role: UserRole -} diff --git a/apps/authz/src/app/http/rest/dto/user-group-membership.dto.ts b/apps/authz/src/app/http/rest/dto/user-group-membership.dto.ts deleted file mode 100644 index f408276e1..000000000 --- a/apps/authz/src/app/http/rest/dto/user-group-membership.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { UserGroupMembership } from '@narval/authz-shared' -import { ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsString } from 'class-validator' - -export class UserGroupMembershipDto { - constructor(data: UserGroupMembership) { - this.userId = data.userId - this.groupId = data.groupId - } - - @IsString() - @IsDefined() - @ApiProperty() - userId: string - - @IsString() - @IsDefined() - @ApiProperty() - groupId: string -} diff --git a/apps/authz/src/app/http/rest/dto/user-wallet.dto.ts b/apps/authz/src/app/http/rest/dto/user-wallet.dto.ts deleted file mode 100644 index 77dd5ca55..000000000 --- a/apps/authz/src/app/http/rest/dto/user-wallet.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { UserWallet } from '@narval/authz-shared' -import { ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsString } from 'class-validator' - -export class UserWalletDto { - constructor(data: UserWallet) { - this.userId = data.userId - this.walletId = data.walletId - } - - @IsString() - @IsDefined() - @ApiProperty() - walletId: string - - @IsString() - @IsDefined() - @ApiProperty() - userId: string -} diff --git a/apps/authz/src/app/http/rest/dto/wallet-dto.ts b/apps/authz/src/app/http/rest/dto/wallet-dto.ts deleted file mode 100644 index 32bfb6cb2..000000000 --- a/apps/authz/src/app/http/rest/dto/wallet-dto.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { AccountType, Address } from '@narval/authz-shared' -import { ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsIn, IsNumber, IsOptional, IsString } from 'class-validator' -import { Wallet } from '../../../../shared/types/entities.types' - -export class WalletDataDto { - constructor(wallet: Wallet) { - this.uid = wallet.uid - this.accountType = wallet.accountType - this.address = wallet.address - this.chainId = wallet.chainId - } - - @IsString() - @IsDefined() - @ApiProperty() - uid: string - - @IsIn(Object.values(AccountType)) - @IsDefined() - @ApiProperty({ - enum: Object.values(AccountType) - }) - accountType: AccountType - - @IsString() - @ApiProperty() - address: Address - - @IsNumber() - @IsOptional() - @ApiProperty() - chainId?: number -} diff --git a/apps/authz/src/app/http/rest/dto/wallet-group-membership.dto.ts b/apps/authz/src/app/http/rest/dto/wallet-group-membership.dto.ts deleted file mode 100644 index e247cedc8..000000000 --- a/apps/authz/src/app/http/rest/dto/wallet-group-membership.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { WalletGroupMembership } from '@narval/authz-shared' -import { ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsString } from 'class-validator' - -export class WalletGroupMembershipDto { - constructor(data: WalletGroupMembership) { - this.walletId = data.walletId - this.groupId = data.groupId - } - - @IsString() - @IsDefined() - @ApiProperty() - walletId: string - - @IsString() - @IsDefined() - @ApiProperty() - groupId: string -} diff --git a/apps/authz/src/app/opa/opa.service.ts b/apps/authz/src/app/opa/opa.service.ts index 4c22443ce..cfe11d1ca 100644 --- a/apps/authz/src/app/opa/opa.service.ts +++ b/apps/authz/src/app/opa/opa.service.ts @@ -3,14 +3,15 @@ import { loadPolicy } from '@open-policy-agent/opa-wasm' import { execSync } from 'child_process' import { readFileSync, writeFileSync } from 'fs' import Handlebars from 'handlebars' +import { indexBy } from 'lodash/fp' +import { ORGANIZATION } from 'packages/authz-shared/src/lib/dev.fixture' import path from 'path' -import * as R from 'remeda' -import { v4 as uuidv4 } from 'uuid' -import { RegoData, User, UserGroup, WalletGroup } from '../../shared/types/entities.types' +import { v4 as uuid } from 'uuid' +import { RegoData } from '../../shared/types/entities.types' import { Policy } from '../../shared/types/policy.type' import { OpaResult, RegoInput } from '../../shared/types/rego' import { criterionToString, reasonToString } from '../../shared/utils/opa.utils' -import { AdminRepository } from '../persistence/repository/admin.repository' +import { EntityRepository } from '../persistence/repository/entity.repository' type PromiseType> = T extends Promise ? U : never type OpaEngine = PromiseType> @@ -22,7 +23,7 @@ export class OpaService implements OnApplicationBootstrap { private logger = new Logger(OpaService.name) private opaEngine: OpaEngine | undefined - constructor(private adminRepository: AdminRepository) {} + constructor(private entityRepository: EntityRepository) {} async onApplicationBootstrap(): Promise { this.logger.log('OPA Service boot') @@ -58,11 +59,11 @@ export class OpaService implements OnApplicationBootstrap { const template = Handlebars.compile(templateSource) - const policies = payload.map((p) => ({ ...p, id: uuidv4() })) + const policies = payload.map((p) => ({ ...p, id: uuid() })) const regoContent = template({ policies }) - const fileId = uuidv4() + const fileId = uuid() writeFileSync(`./apps/authz/src/opa/rego/generated/${fileId}.rego`, regoContent, 'utf-8') @@ -76,68 +77,22 @@ export class OpaService implements OnApplicationBootstrap { } private async fetchEntityData(): Promise { - const users = await this.adminRepository.getAllUsers() - const wallets = await this.adminRepository.getAllWallets() - const walletGroups = await this.adminRepository.getAllWalletGroups() - const userWallets = await this.adminRepository.getAllUserWallets() - const userGroups = await this.adminRepository.getAllUserGroups() - const addressBook = await this.adminRepository.getAllAddressBook() - const tokens = await this.adminRepository.getAllTokens() - - const regoUsers: Record = R.indexBy(users, (u) => u.uid) - const regoWallets = R.indexBy(wallets, (w) => w.uid) - const regoAddressBook = R.indexBy(addressBook, (a) => a.uid) - const regoTokens = R.indexBy(tokens, (t) => t.uid) - - // Add the assignees into the regoWallets - userWallets.forEach((uw) => { - if (regoWallets[uw.walletId]) { - if (!regoWallets[uw.walletId].assignees) regoWallets[uw.walletId].assignees = [] - regoWallets[uw.walletId].assignees?.push(uw.userId) - } - }) - - const regoUserGroups = userGroups.reduce((acc, ug) => { - if (!acc[ug.userGroupId]) { - acc[ug.userGroupId] = { - uid: ug.userGroupId, - users: [] - } - } - - acc[ug.userGroupId].users.push(ug.userId) - - return acc - }, {} as Record) - - const regoWalletGroups = walletGroups.reduce((acc, ug) => { - if (!acc[ug.walletGroupId]) { - acc[ug.walletGroupId] = { - uid: ug.walletGroupId, - wallets: [] - } - } - - acc[ug.walletGroupId].wallets.push(ug.walletId) - - return acc - }, {} as Record) + const entities = await this.entityRepository.fetch(ORGANIZATION.uid) - const mockData = await this.adminRepository.getEntityData() - - const regoData: RegoData = { + const data: RegoData = { entities: { - users: regoUsers, - wallets: regoWallets, - userGroups: regoUserGroups, - walletGroups: regoWalletGroups, - addressBook: regoAddressBook, - tokens: regoTokens + addressBook: indexBy('uid', entities.addressBook), + users: indexBy('uid', entities.users), + userGroups: indexBy('uid', entities.userGroups), + wallets: indexBy('uid', entities.wallets), + walletGroups: indexBy('uid', entities.walletGroups), + tokens: indexBy('uid', entities.tokens) } } - this.logger.log('Fetched OPA Engine data', regoData) - return mockData + this.logger.log('Fetched OPA Engine data', data) + + return data } async reloadEntityData() { diff --git a/apps/authz/src/app/persistence/repository/__test__/integration/admin.repository.spec.ts b/apps/authz/src/app/persistence/repository/__test__/integration/admin.repository.spec.ts deleted file mode 100644 index c39405a6b..000000000 --- a/apps/authz/src/app/persistence/repository/__test__/integration/admin.repository.spec.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { Alg, UserRole } from '@narval/authz-shared' -import { ConfigModule, ConfigService } from '@nestjs/config' -import { Test, TestingModule } from '@nestjs/testing' -import { load } from '../../../../../app/app.config' -import { AdminRepository } from '../../../../../app/persistence/repository/admin.repository' -import { PersistenceModule } from '../../../../../shared/module/persistence/persistence.module' -import { TestPrismaService } from '../../../../../shared/module/persistence/service/test-prisma.service' - -describe(AdminRepository.name, () => { - let module: TestingModule - let repository: AdminRepository - let testPrismaService: TestPrismaService - let transactionSpy: jest.SpyInstance - - beforeEach(async () => { - module = await Test.createTestingModule({ - imports: [ - ConfigModule.forRoot({ - load: [load], - isGlobal: true - }), - PersistenceModule - ], - providers: [AdminRepository] - }).compile() - - testPrismaService = module.get(TestPrismaService) - repository = module.get(AdminRepository) - transactionSpy = jest.spyOn(testPrismaService.getClient(), '$transaction') - }) - - afterEach(async () => { - await testPrismaService.truncateAll() - await module.close() - await transactionSpy.mockRestore() - }) - - describe('setup', () => { - it('should have test db url', () => { - // Just leaving this to ensure the jest.setup.ts file is configured correctly to set the env variable. - const configService = module.get(ConfigService) - expect(configService.get('database.url', { infer: true })).toBe('file:./engine-core-test.sqlite') - }) - }) - - describe('createOrganization', () => { - it('creates a new organization', async () => { - await repository.createOrganization('test-org-uid', { - uid: 'test-kid', - alg: Alg.ES256K, - pubKey: 'test-public-key', - userId: 'test-user-id' - }) - - const org = await testPrismaService.getClient().organization.findFirst({ - where: { - uid: 'test-org-uid' - } - }) - expect(org).toEqual({ uid: 'test-org-uid' }) - expect(transactionSpy).toHaveBeenCalledTimes(1) - }) - - it('rolls back org & user creation if rootCredential fails', async () => { - expect.assertions(2) - - await repository.createOrganization('test-org-uid', { - uid: 'test-kid', - alg: Alg.ES256K, - pubKey: 'test-public-key', - userId: 'test-user-id' - }) - - // Insert another new Org & new user but same cred. - const insertAgain = async () => - repository.createOrganization('test-org-uid-2', { - uid: 'test-kid', - alg: Alg.ES256K, - pubKey: 'test-public-key', - userId: 'test-user-id-2' - }) - - expect(insertAgain()).rejects.toThrow() - - const org = await testPrismaService.getClient().organization.findFirst({ - where: { - uid: 'test-org-uid-2' - } - }) - expect(org).toBeNull() - }) - }) - - describe('createUser', () => { - it('creates a new user without credential', async () => { - const uid = 'test-uid' - const role = UserRole.ADMIN - await repository.createUser(uid, role) - - const user = await testPrismaService.getClient().user.findFirst({ - where: { - uid - } - }) - - expect(user).toEqual({ uid, role }) - expect(transactionSpy).toHaveBeenCalledTimes(1) - }) - - it('creates a new user with credential', async () => { - const uid = 'test-uid' - const role = UserRole.ADMIN - const credential = { - uid: 'test-credential-uid', - pubKey: 'test-public-key', - alg: Alg.ES256K, - userId: uid - } - - await repository.createUser(uid, role, credential) - - const user = await testPrismaService.getClient().user.findFirst({ - where: { - uid - } - }) - - const savedCredential = await testPrismaService.getClient().authCredential.findFirst({ - where: { - uid: credential.uid - } - }) - - expect(user).toEqual({ uid, role }) - expect(savedCredential).toEqual(credential) - expect(transactionSpy).toHaveBeenCalledTimes(1) - }) - }) - - describe('assignUserGroup', () => { - it('should assign user to group', async () => { - const userId = 'test-user-id' - const groupId = 'test-group-id' - - await repository.assignUserGroup(userId, groupId) - - const membership = await testPrismaService.getClient().userGroupMembership.findUnique({ - where: { - userId_userGroupId: { - userId, - userGroupId: groupId - } - } - }) - - expect(membership).toEqual({ - userId, - userGroupId: groupId - }) - expect(transactionSpy).toHaveBeenCalledTimes(1) - }) - - it('should create a new userGroup if it does not exist', async () => { - const userId = 'test-user-id' - const groupId = 'test-group-id' - - await repository.assignUserGroup(userId, groupId) - - const group = await testPrismaService.getClient().userGroup.findUnique({ - where: { - uid: groupId - } - }) - - expect(group).toEqual({ - uid: groupId - }) - expect(transactionSpy).toHaveBeenCalledTimes(1) - }) - }) - - describe('assignWalletGroup', () => { - it('should assign wallet to group', async () => { - const walletId = 'test-wallet-id' - const groupId = 'test-group-id' - - await repository.assignWalletGroup(walletId, groupId) - - const membership = await testPrismaService.getClient().walletGroupMembership.findUnique({ - where: { - walletId_walletGroupId: { - walletId, - walletGroupId: groupId - } - } - }) - - expect(membership).toEqual({ - walletId, - walletGroupId: groupId - }) - expect(transactionSpy).toHaveBeenCalledTimes(1) - }) - - it('should create a new walletGroup if it does not exist', async () => { - const walletId = 'test-wallet-id' - const groupId = 'test-group-id' - - await repository.assignWalletGroup(walletId, groupId) - - const group = await testPrismaService.getClient().walletGroup.findUnique({ - where: { - uid: groupId - } - }) - - expect(group).toEqual({ - uid: groupId - }) - expect(transactionSpy).toHaveBeenCalledTimes(1) - }) - }) -}) diff --git a/apps/authz/src/app/persistence/repository/admin.repository.ts b/apps/authz/src/app/persistence/repository/admin.repository.ts deleted file mode 100644 index 3e5d7038c..000000000 --- a/apps/authz/src/app/persistence/repository/admin.repository.ts +++ /dev/null @@ -1,467 +0,0 @@ -import { AccountClassification, AccountType, Address, Alg, AuthCredential, UserRole } from '@narval/authz-shared' -import { Injectable, Logger, OnModuleInit } from '@nestjs/common' -import { castArray } from 'lodash/fp' -import { PrismaService } from '../../../shared/module/persistence/service/prisma.service' -import { AddressBookAccount, Organization, RegoData, Token, User, Wallet } from '../../../shared/types/entities.types' -import { mockEntityData, userAddressStore, userCredentialStore } from './mock_data' - -function convertResponse( - response: T, - key: K, - validValues: V[] -): T & Record { - if (!validValues.includes(response[key] as V)) { - throw new Error(`Invalid value for key ${key as string}: ${response[key]}`) - } - - return { - ...response, - [key]: response[key] as V - } as T & Record -} - -@Injectable() -export class AdminRepository implements OnModuleInit { - private logger = new Logger(AdminRepository.name) - - constructor(private prismaService: PrismaService) {} - - async onModuleInit() { - this.logger.log('AdminRepository initialized') - } - - async getEntityData(): Promise { - const data = mockEntityData - return data - } - - async getUserForAddress(address: string): Promise { - const userId = userAddressStore[address] - if (!userId) throw new Error(`Could not find user for address ${address}`) - return userId - } - - async getCredentialForPubKey(pubKey: string): Promise { - const credential = userCredentialStore[pubKey] - if (!credential) throw new Error(`Could not find credential for pubKey ${pubKey}`) - return credential - } - - // CRUD - async getAllUsers(): Promise { - const users = await this.prismaService.user.findMany() - return users.map((d) => convertResponse(d, 'role', Object.values(UserRole))) - } - - async getAllWallets(): Promise { - const wallets = await this.prismaService.wallet.findMany() - return wallets.map((d) => ({ - ...convertResponse(d, 'accountType', Object.values(AccountType)), - address: d.address as Address, - chainId: d.chainId || undefined - })) - } - - async getAllUserWallets(): Promise< - { - userId: string - walletId: string - }[] - > { - const userWallets = await this.prismaService.userWalletAssignment.findMany() - return userWallets - } - - async getAllUserGroups(): Promise<{ userId: string; userGroupId: string }[]> { - const userGroups = await this.prismaService.userGroupMembership.findMany() - return userGroups - } - - async getAllWalletGroups(): Promise<{ walletId: string; walletGroupId: string }[]> { - const walletGroups = await this.prismaService.walletGroupMembership.findMany() - return walletGroups - } - - async getAllAddressBook(): Promise { - const addressBook = await this.prismaService.addressBookAccount.findMany() - return addressBook.map((d) => ({ - ...convertResponse(d, 'classification', Object.values(AccountClassification)), - address: d.address as Address - })) - } - - async getAllTokens(): Promise { - const tokens = await this.prismaService.token.findMany() - return tokens.map((d) => ({ - uid: d.uid, - address: d.address as Address, - symbol: d.symbol, - chainId: d.chainId, - decimals: d.decimals - })) - } - - async createOrganization( - organizationId: string, - rootCredential: AuthCredential - ): Promise<{ - organization: Organization - rootUser: User - rootCredential: AuthCredential - }> { - const result = await this.prismaService.$transaction(async (txn) => { - const organization = await txn.organization.create({ - data: { - uid: organizationId - } - }) - this.logger.log(`Created organization ${organization.uid}`) - - const rootUser: User = await txn.user - .create({ - data: { - uid: rootCredential.userId, - role: UserRole.ROOT - } - }) - .then((d) => convertResponse(d, 'role', Object.values(UserRole))) - - this.logger.log(`Created Root User ${rootUser.uid}`) - - const rootAuthCredential: AuthCredential = await txn.authCredential - .create({ - data: { - uid: rootCredential.uid, - pubKey: rootCredential.pubKey, - alg: rootCredential.alg, - userId: rootCredential.userId - } - }) - .then((d) => convertResponse(d, 'alg', Object.values(Alg))) - - this.logger.log(`Created Root User AuthCredential ${rootAuthCredential.pubKey}`) - - return { - organization, - rootUser, - rootCredential: rootAuthCredential - } - }) - - return result - } - - async createUser(uid: string, role: UserRole, credential?: AuthCredential): Promise { - const result = await this.prismaService.$transaction(async (txn) => { - // Create the User with the Role - // Create the user's Credential - const user = await txn.user - .create({ data: { uid, role } }) - .then((d) => convertResponse(d, 'role', Object.values(UserRole))) - - // If we're registering a credential at the same time, do that now; otherwise it can be assigned later. - if (credential) { - await txn.authCredential.create({ - data: { - uid: credential.uid, - pubKey: credential.pubKey, - alg: credential.alg, - userId: uid - } - }) - } - - return user - }) - return result - } - - async deleteUser(uid: string): Promise { - await this.prismaService.$transaction(async (txn) => { - // Delete the User - // Delete the user's Credentials - // Remove the user as an assignee of any wallets/groups - await txn.user.delete({ - where: { - uid - } - }) - await txn.authCredential.deleteMany({ - where: { - userId: uid - } - }) - - await txn.userWalletAssignment.deleteMany({ - where: { - userId: uid - } - }) - - await txn.userGroupMembership.deleteMany({ - where: { - userId: uid - } - }) - }) - - return true - } - - async createAuthCredential(credential: AuthCredential): Promise { - await this.prismaService.authCredential.create({ - data: { - uid: credential.uid, - pubKey: credential.pubKey, - alg: credential.alg, - userId: credential.userId - } - }) - - return true - } - - async deleteAuthCredential(uid: string): Promise { - await this.prismaService.authCredential.delete({ - where: { - uid: uid - } - }) - return true - } - - async assignUserRole(userId: string, role: UserRole): Promise { - await this.prismaService.user.update({ - where: { - uid: userId - }, - data: { - role - } - }) - - return true - } - - async assignUserGroup(userId: string, groupId: string): Promise { - await this.prismaService.$transaction(async (txn) => { - const group = await txn.userGroup.findUnique({ - where: { uid: groupId } - }) - if (!group) { - await txn.userGroup.create({ - data: { uid: groupId } - }) - } - await txn.userGroupMembership.create({ - data: { - userId, - userGroupId: groupId - } - }) - }) - - return true - } - - async unassignUserGroup(userId: string, groupId: string): Promise { - await this.prismaService.userGroupMembership.delete({ - where: { - userId_userGroupId: { - userId, - userGroupId: groupId - } - } - }) - - return true - } - - async registerWallet(uid: string, address: Address, accountType: AccountType, chainId?: number): Promise { - await this.prismaService.wallet.create({ - data: { - uid, - address: address, - accountType, - chainId - } - }) - - return true - } - - async unregisterWallet(uid: string): Promise { - await this.prismaService.$transaction(async (txn) => { - // Remove the wallet from any groups - await txn.walletGroupMembership.deleteMany({ - where: { - walletId: uid - } - }) - // Remove the wallet from assignees - await txn.userWalletAssignment.deleteMany({ - where: { - walletId: uid - } - }) - // Delete the wallet - await txn.wallet.delete({ - where: { - uid - } - }) - }) - - return true - } - - async createWalletGroup(uid: string): Promise { - await this.prismaService.walletGroup.create({ - data: { - uid - } - }) - - return true - } - - async deleteWalletGroup(uid: string): Promise { - await this.prismaService.$transaction(async (txn) => { - // unassign all wallets from the group - await txn.walletGroupMembership.deleteMany({ - where: { - walletGroupId: uid - } - }) - // delete the group - await txn.walletGroup.delete({ - where: { - uid - } - }) - }) - - return true - } - - async assignWalletGroup(walletId: string, groupId: string): Promise { - await this.prismaService.$transaction(async (txn) => { - const group = await txn.walletGroup.findUnique({ - where: { uid: groupId } - }) - if (!group) { - await txn.walletGroup.create({ - data: { uid: groupId } - }) - } - await txn.walletGroupMembership.create({ - data: { - walletId, - walletGroupId: groupId - } - }) - }) - return true - } - - async unassignWalletGroup(walletGroupId: string, walletId: string): Promise { - await this.prismaService.walletGroupMembership.delete({ - where: { - walletId_walletGroupId: { - walletId, - walletGroupId - } - } - }) - - return true - } - - async assignUserWallet(userId: string, walletId: string): Promise { - await this.prismaService.userWalletAssignment.create({ - data: { - userId, - walletId - } - }) - - return true - } - - async unassignUserWallet(userId: string, walletId: string): Promise { - await this.prismaService.userWalletAssignment.delete({ - where: { - userId_walletId: { - userId, - walletId - } - } - }) - - return true - } - - async createAddressBookAccount(account: AddressBookAccount): Promise { - await this.prismaService.addressBookAccount.create({ - data: { - uid: account.uid, - address: account.address, - classification: account.classification, - chainId: account.chainId - } - }) - - return true - } - - async deleteAddressBookAccount(uid: string): Promise { - await this.prismaService.addressBookAccount.delete({ - where: { - uid - } - }) - - return true - } - - async registerTokens(tokens: Token | Token[]): Promise { - await this.prismaService.$transaction(async (txn) => { - await Promise.all( - castArray(tokens).map(async (token) => { - await txn.token.upsert({ - create: { - uid: token.uid, - symbol: token.symbol, - address: token.address, - chainId: token.chainId, - decimals: token.decimals - }, - update: { - symbol: token.symbol, - address: token.address, - chainId: token.chainId, - decimals: token.decimals - }, - where: { - uid: token.uid - } - }) - }) - ) - }) - - return true - } - - async unregisterToken(uid: string): Promise { - await this.prismaService.token.delete({ - where: { - uid - } - }) - - return true - } - - async registerRootKey() {} -} diff --git a/apps/authz/src/app/persistence/repository/entity.repository.ts b/apps/authz/src/app/persistence/repository/entity.repository.ts new file mode 100644 index 000000000..01554e6e1 --- /dev/null +++ b/apps/authz/src/app/persistence/repository/entity.repository.ts @@ -0,0 +1,52 @@ +import { AuthCredential, Entities } from '@narval/authz-shared' +import { HttpService } from '@nestjs/axios' +import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common' +import { ORGANIZATION } from 'packages/authz-shared/src/lib/dev.fixture' +import { lastValueFrom, map, tap } from 'rxjs' + +@Injectable() +export class EntityRepository implements OnApplicationBootstrap { + private logger = new Logger(EntityRepository.name) + + private entities?: Entities + + constructor(private httpService: HttpService) {} + + fetch(orgId: string): Promise { + this.logger.log('Fetch organization entities', { orgId }) + + return lastValueFrom( + this.httpService + .get('http://localhost:3005/store/entities', { + headers: { + 'x-org-id': orgId + } + }) + .pipe( + map((response) => response.data), + tap((entities) => { + this.logger.log('Received entities snapshot', entities) + }) + ) + ) + } + + getCredentialForPubKey(pubKey: string): AuthCredential | null { + if (this.entities) { + return this.entities.credentials.find((cred) => cred.pubKey === pubKey) || null + } + + return null + } + + async onApplicationBootstrap() { + // TODO (@wcalderipe, 15/02/24): Figure out where the organization will come + // from. It depends on the deployment model: standalone engine per + // organization or cluster with multi tenant. + if (!this.entities) { + const entities = await this.fetch(ORGANIZATION.uid) + + this.entities = entities + } + } +} diff --git a/apps/authz/src/app/persistence/repository/mock_data.ts b/apps/authz/src/app/persistence/repository/mock_data.ts index c376fdef7..0afea3b27 100644 --- a/apps/authz/src/app/persistence/repository/mock_data.ts +++ b/apps/authz/src/app/persistence/repository/mock_data.ts @@ -1,341 +1,50 @@ -import { - AccountClassification, - AccountId, - AccountType, - Action, - Alg, - AssetId, - AuthCredential, - EvaluationRequest, - Request, - TransactionRequest, - UserRole, - hashRequest -} from '@narval/authz-shared' -import { Intents } from 'packages/transaction-request-intent/src/lib/domain' -import { TransferNative } from 'packages/transaction-request-intent/src/lib/intent.types' -import { Address, sha256, toHex } from 'viem' -import { privateKeyToAccount } from 'viem/accounts' -import { RegoInput } from '../../../shared/types/domain.type' -import { - AddressBookAccount, - RegoData, - User, - UserGroup, - Wallet, - WalletGroup -} from '../../../shared/types/entities.types' +import { Action, Alg, EvaluationRequest, FIXTURE, Request, TransactionRequest, hashRequest } from '@narval/authz-shared' +import { toHex } from 'viem' export const ONE_ETH = BigInt('1000000000000000000') -export const USDC_TOKEN = { - uid: 'eip155:137/erc20:0x2791bca1f2de4661ed88a30c99a7a9449aa84174', - address: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', - symbol: 'USDC', - chain_id: 137, - decimals: 6 -} - -/** - * User & User Groups - */ - -export const ROOT_USER: User = { - uid: 'u:root_user', - role: UserRole.ROOT -} - -export const MATT: User = { - uid: 'matt@narval.xyz', - role: UserRole.ADMIN -} - -export const MATT_CREDENTIAL_1: AuthCredential = { - uid: sha256('0xd75D626a116D4a1959fE3bB938B2e7c116A05890'), - alg: Alg.ES256K, - userId: MATT.uid, - pubKey: '0xd75D626a116D4a1959fE3bB938B2e7c116A05890' -} - -export const AAUser: User = { - uid: 'aa@narval.xyz', - role: UserRole.ADMIN -} - -export const AAUser_Credential_1: AuthCredential = { - uid: sha256('0x501D5c2Ce1EF208aadf9131a98BAa593258CfA06'), - userId: AAUser.uid, - alg: Alg.ES256K, - pubKey: '0x501D5c2Ce1EF208aadf9131a98BAa593258CfA06' -} - -export const BBUser: User = { - uid: 'bb@narval.xyz', - role: UserRole.ADMIN -} - -export const BBUser_Credential_1: AuthCredential = { - uid: sha256('0xab88c8785D0C00082dE75D801Fcb1d5066a6311e'), - userId: BBUser.uid, - alg: Alg.ES256K, - pubKey: '0xab88c8785D0C00082dE75D801Fcb1d5066a6311e' -} - -export const DEV_USER_GROUP: UserGroup = { - uid: 'ug:dev-group', - users: [MATT.uid] -} - -export const TREASURY_USER_GROUP: UserGroup = { - uid: 'ug:treasury-group', - users: [BBUser.uid, MATT.uid] -} - -/** - * User<>Authn mapping store - */ - -// Private keys used for USER AUTHN; these are _not_ "wallets" in our system. -export const UNSAFE_PRIVATE_KEY_MATT = '0x5f1049fa330544680cfa495285000d7a597adae224c070ccb9f1dc2d5f9204d1' // 0xd75D626a116D4a1959fE3bB938B2e7c116A05890 -export const UNSAFE_PRIVATE_KEY_AAUSER = '0x2f069925bbd2bc2a9fddeab641dea34f7893dd97013cd6282909897740e07539' // 0x501D5c2Ce1EF208aadf9131a98BAa593258CfA06 -export const UNSAFE_PRIVATE_KEY_BBUSER = '0xa1f1830a6d1765aa1b57ad76731d1c3463658523e11dc853b7af7827549096c3' // 0xab88c8785D0C00082dE75D801Fcb1d5066a6311e - -// User AuthN Address <> UserId mapping; one user can have multiple authn pubkeys -// @deprecated, use Credential store -export const userAddressStore: { [key: string]: string } = { - '0xd75D626a116D4a1959fE3bB938B2e7c116A05890': MATT.uid, - '0x501D5c2Ce1EF208aadf9131a98BAa593258CfA06': AAUser.uid, - '0xab88c8785D0C00082dE75D801Fcb1d5066a6311e': BBUser.uid -} - -export const userCredentialStore: { [key: string]: AuthCredential } = { - '0xd75D626a116D4a1959fE3bB938B2e7c116A05890': MATT_CREDENTIAL_1, - '0x501D5c2Ce1EF208aadf9131a98BAa593258CfA06': AAUser_Credential_1, - '0xab88c8785D0C00082dE75D801Fcb1d5066a6311e': BBUser_Credential_1 -} - -/** - * Wallet & Wallet Groups & Accounts - */ - -// Wallets -export const SHY_ACCOUNT_WALLET: Wallet = { - uid: 'eip155:eoa:0xddcf208f219a6e6af072f2cfdc615b2c1805f98e', - address: '0xddcf208f219a6e6af072f2cfdc615b2c1805f98e', - accountType: AccountType.EOA, - assignees: [MATT.uid] -} - -export const PIERRE_WALLET: Wallet = { - uid: 'eip155:eoa:0x22228d0504d4f3363a5b7fda1f5fff1c7bca8ad4', - address: '0x22228d0504d4f3363a5b7fda1f5fff1c7bca8ad4', - accountType: AccountType.EOA -} - -export const WALLET_Q: Wallet = { - uid: 'eip155:eoa:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4', - address: '0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4', - accountType: AccountType.EOA, - assignees: [MATT.uid] -} - -export const TREASURY_WALLET_X: Wallet = { - uid: 'eip155:eoa:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b', // Prod guild 58 - treasury wallet - address: '0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b', - accountType: AccountType.EOA, - assignees: [MATT.uid] -} - -// Wallet Groups - -export const DEV_WALLET_GROUP: WalletGroup = { - uid: 'wg:dev-group', - wallets: [SHY_ACCOUNT_WALLET.uid] -} - -export const TREASURY_WALLET_GROUP: WalletGroup = { - uid: 'wg:treasury-group', - wallets: [TREASURY_WALLET_X.uid] -} - -// Address Book - -export const SHY_ACCOUNT_137: AddressBookAccount = { - uid: 'eip155:137:0xddcf208f219a6e6af072f2cfdc615b2c1805f98e', - address: '0xddcf208f219a6e6af072f2cfdc615b2c1805f98e', - chainId: 137, - classification: AccountClassification.WALLET -} - -export const SHY_ACCOUNT_1: AddressBookAccount = { - uid: 'eip155:1:0xddcf208f219a6e6af072f2cfdc615b2c1805f98e', - address: '0xddcf208f219a6e6af072f2cfdc615b2c1805f98e', - chainId: 1, - classification: AccountClassification.WALLET -} - -export const ACCOUNT_Q_137: AddressBookAccount = { - uid: 'eip155:137:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4', - address: '0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4', - chainId: 137, - classification: AccountClassification.WALLET -} - -export const ACCOUNT_INTERNAL_WXZ_137: AddressBookAccount = { - uid: 'eip155:137:0xa45e21e9370ba031c5e1f47dedca74a7ce2ed7a3', - address: '0xa45e21e9370ba031c5e1f47dedca74a7ce2ed7a3', - chainId: 137, - classification: AccountClassification.INTERNAL -} - -export const NATIVE_TRANSFER_INTENT: TransferNative = { - from: TREASURY_WALLET_X.uid as AccountId, - to: ACCOUNT_Q_137.uid as AccountId, - type: Intents.TRANSFER_NATIVE, - amount: toHex(ONE_ETH), - token: 'eip155:1/slip44:60' as AssetId // Caip19 for ETH -} - -export const ERC20_TRANSFER_TX_REQUEST: TransactionRequest = { - from: TREASURY_WALLET_X.address as Address, - to: '0x031d8C0cA142921c459bCB28104c0FF37928F9eD' as Address, - chainId: ACCOUNT_Q_137.chainId, - data: '0xa9059cbb000000000000000000000000031d8c0ca142921c459bcb28104c0ff37928f9ed000000000000000000000000000000000000000000005ab7f55035d1e7b4fe6d', - nonce: 192, - type: '2' -} - -export const NATIVE_TRANSFER_TX_REQUEST: TransactionRequest = { - from: TREASURY_WALLET_X.address as Address, - to: ACCOUNT_Q_137.address as Address, - chainId: ACCOUNT_Q_137.chainId, - value: toHex(ONE_ETH), - data: '0x00000000', - nonce: 192, - type: '2' -} - -export const REGO_REQUEST: RegoInput = { - action: Action.SIGN_TRANSACTION, - transactionRequest: NATIVE_TRANSFER_TX_REQUEST, - intent: NATIVE_TRANSFER_INTENT, - resource: { - uid: TREASURY_WALLET_X.uid - }, - principal: MATT_CREDENTIAL_1, - approvals: [], - transfers: [] -} - -export const mockEntityData: RegoData = { - entities: { - users: { - [ROOT_USER.uid]: ROOT_USER, - [MATT.uid]: MATT, - [AAUser.uid]: AAUser, - [BBUser.uid]: BBUser - }, - userGroups: { - [DEV_USER_GROUP.uid]: DEV_USER_GROUP, - [TREASURY_USER_GROUP.uid]: TREASURY_USER_GROUP - }, - wallets: { - [SHY_ACCOUNT_WALLET.uid]: SHY_ACCOUNT_WALLET, - [PIERRE_WALLET.uid]: PIERRE_WALLET, - [WALLET_Q.uid]: WALLET_Q, - [TREASURY_WALLET_X.uid]: TREASURY_WALLET_X - }, - walletGroups: { - [DEV_WALLET_GROUP.uid]: DEV_WALLET_GROUP, - [TREASURY_WALLET_GROUP.uid]: TREASURY_WALLET_GROUP - }, - addressBook: { - [SHY_ACCOUNT_137.uid]: SHY_ACCOUNT_137, - [SHY_ACCOUNT_1.uid]: SHY_ACCOUNT_1, - [ACCOUNT_INTERNAL_WXZ_137.uid]: ACCOUNT_INTERNAL_WXZ_137, - [ACCOUNT_Q_137.uid]: ACCOUNT_Q_137 - }, - tokens: {} +export const generateInboundRequest = async (): Promise => { + const txRequest: TransactionRequest = { + from: FIXTURE.WALLET.engineering1.address, + to: FIXTURE.WALLET.treasury.address, + chainId: 137, + value: toHex(ONE_ETH), + data: '0x00000000', + nonce: 192, + type: '2' } -} -// stub out the actual tx request & signature -// This is what would be the initial input from the external service -export const generateInboundRequest = async (): Promise => { - const txRequest = NATIVE_TRANSFER_TX_REQUEST const request: Request = { action: Action.SIGN_TRANSACTION, nonce: 'random-nonce-111', transactionRequest: txRequest, - resourceId: TREASURY_WALLET_X.uid + resourceId: FIXTURE.WALLET.engineering1.uid } - const signatureMatt = await privateKeyToAccount(UNSAFE_PRIVATE_KEY_MATT).signMessage({ - message: hashRequest(request) - }) - // 0xe24d097cea880a40f8be2cf42f497b9fbda5f9e4a31b596827e051d78dce75c032fa7e5ee3046f7c6f116e5b98cb8d268fa9b9d222ff44719e2ec2a0d9159d0d1c - const approvalSigAAUser = await privateKeyToAccount(UNSAFE_PRIVATE_KEY_AAUSER).signMessage({ - message: hashRequest(request) - }) - // 0x48510e3b74799b8e8f4e01aba0d196e18f66d86a62ae91abf5b89be9391c15661c7d29ee4654a300ed6db977da512475ed5a39f70f677e23d1b2f53c1554d0dd1b - const approvalSigBBUser = await privateKeyToAccount(UNSAFE_PRIVATE_KEY_BBUSER).signMessage({ - message: hashRequest(request) - }) - // 0xcc645f43d8df80c4deeb2e60a8c0c15d58586d2c29ea7c85208cea81d1c47cbd787b1c8473dde70c3a7d49f573e491223107933257b2b99ecc4806b7cc16848d1c + const message = hashRequest(request) + + const aliceSignature = await FIXTURE.ACCOUNT.Alice.signMessage({ message }) + const bobSignature = await FIXTURE.ACCOUNT.Bob.signMessage({ message }) + const carolSignature = await FIXTURE.ACCOUNT.Carol.signMessage({ message }) return { authentication: { - sig: signatureMatt, + sig: aliceSignature, alg: Alg.ES256K, - pubKey: '0xd75D626a116D4a1959fE3bB938B2e7c116A05890' + pubKey: FIXTURE.ACCOUNT.Alice.address }, request, approvals: [ { - sig: approvalSigAAUser, + sig: bobSignature, alg: Alg.ES256K, - pubKey: '0x501D5c2Ce1EF208aadf9131a98BAa593258CfA06' + pubKey: FIXTURE.ACCOUNT.Bob.address }, { - sig: approvalSigBBUser, + sig: carolSignature, alg: Alg.ES256K, - pubKey: '0xab88c8785D0C00082dE75D801Fcb1d5066a6311e' + pubKey: FIXTURE.ACCOUNT.Carol.address } ] } } -/** - * Sample API POST body for POST /evaluation that does the same thing as `generateInboundRequest - { - "authentication": { - "sig": "0xe24d097cea880a40f8be2cf42f497b9fbda5f9e4a31b596827e051d78dce75c032fa7e5ee3046f7c6f116e5b98cb8d268fa9b9d222ff44719e2ec2a0d9159d0d1c", - "alg": "ES256K", - "pubKey": "0xd75D626a116D4a1959fE3bB938B2e7c116A05890" - }, - "approvals": [ - { - "sig": "0x48510e3b74799b8e8f4e01aba0d196e18f66d86a62ae91abf5b89be9391c15661c7d29ee4654a300ed6db977da512475ed5a39f70f677e23d1b2f53c1554d0dd1b", - "alg": "ES256K", - "pubKey": "0x501D5c2Ce1EF208aadf9131a98BAa593258CfA06" - }, - { - "sig": "0xcc645f43d8df80c4deeb2e60a8c0c15d58586d2c29ea7c85208cea81d1c47cbd787b1c8473dde70c3a7d49f573e491223107933257b2b99ecc4806b7cc16848d1c", - "alg": "ES256K", - "pubKey": "0xab88c8785D0C00082dE75D801Fcb1d5066a6311e" - } - ], - "request": { - "action": "signTransaction", - "transactionRequest": { - "from": "0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b", - "to": "0x031d8C0cA142921c459bCB28104c0FF37928F9eD", - "chainId": "137", - "data": "0xa9059cbb000000000000000000000000031d8c0ca142921c459bcb28104c0ff37928f9ed000000000000000000000000000000000000000000005ab7f55035d1e7b4fe6d", - "nonce": 192, - "type": "2" - }, - "resourceId": "eip155:eoa:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b" - } -} - - */ diff --git a/apps/authz/src/opa/template/mockData.ts b/apps/authz/src/opa/template/mockData.ts index 8e91d5119..8dd0a8686 100644 --- a/apps/authz/src/opa/template/mockData.ts +++ b/apps/authz/src/opa/template/mockData.ts @@ -1,4 +1,4 @@ -import { Action, EntityType, ValueOperators } from '@narval/authz-shared' +import { Action, EntityType, FIXTURE, UserRole, ValueOperators } from '@narval/authz-shared' import { Intents } from '@narval/transaction-request-intent' import { Criterion, Policy, Then } from '../../shared/types/policy.type' @@ -20,11 +20,11 @@ export const examplePermitPolicy: Policy = { }, { criterion: Criterion.CHECK_PRINCIPAL_ID, - args: ['matt@narval.xyz'] + args: [FIXTURE.USER.Alice.role] }, { criterion: Criterion.CHECK_WALLET_ID, - args: ['eip155:eoa:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b'] + args: [FIXTURE.WALLET.engineering1.address] }, { criterion: Criterion.CHECK_INTENT_TYPE, @@ -36,7 +36,11 @@ export const examplePermitPolicy: Policy = { }, { criterion: Criterion.CHECK_INTENT_AMOUNT, - args: { currency: '*', operator: ValueOperators.LESS_THAN_OR_EQUAL, value: '1000000000000000000' } + args: { + currency: '*', + operator: ValueOperators.LESS_THAN_OR_EQUAL, + value: '1000000000000000000' + } }, { criterion: Criterion.CHECK_APPROVALS, @@ -45,13 +49,13 @@ export const examplePermitPolicy: Policy = { approvalCount: 2, countPrincipal: false, approvalEntityType: EntityType.User, - entityIds: ['aa@narval.xyz', 'bb@narval.xyz'] + entityIds: [FIXTURE.USER.Bob.uid, FIXTURE.USER.Carol.uid] }, { approvalCount: 1, countPrincipal: false, approvalEntityType: EntityType.UserRole, - entityIds: ['admin'] + entityIds: [UserRole.ADMIN] } ] } @@ -76,11 +80,11 @@ export const exampleForbidPolicy: Policy = { }, { criterion: Criterion.CHECK_PRINCIPAL_ID, - args: ['matt@narval.xyz'] + args: [FIXTURE.USER.Alice.uid] }, { criterion: Criterion.CHECK_WALLET_ID, - args: ['eip155:eoa:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b'] + args: [FIXTURE.WALLET.engineering1.address] }, { criterion: Criterion.CHECK_INTENT_TYPE, @@ -140,7 +144,7 @@ export const permitMetaPermission: Policy = { }, { criterion: Criterion.CHECK_PRINCIPAL_ROLE, - args: ['admin'] + args: [UserRole.ADMIN] }, { criterion: Criterion.CHECK_APPROVALS, @@ -149,7 +153,7 @@ export const permitMetaPermission: Policy = { approvalCount: 2, countPrincipal: false, approvalEntityType: EntityType.UserRole, - entityIds: ['admin', 'root'] + entityIds: [UserRole.ADMIN, UserRole.ROOT] } ] } @@ -166,7 +170,7 @@ export const forbidMetaPermission: Policy = { }, { criterion: Criterion.CHECK_PRINCIPAL_ROLE, - args: ['admin'] + args: [UserRole.ADMIN] } ], then: Then.FORBID diff --git a/apps/authz/src/shared/module/persistence/seed.ts b/apps/authz/src/shared/module/persistence/seed.ts deleted file mode 100644 index ebd0fb76d..000000000 --- a/apps/authz/src/shared/module/persistence/seed.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Logger } from '@nestjs/common' -import { Organization, PrismaClient } from '@prisma/client/authz' -import { mockEntityData } from '../../../app/persistence/repository/mock_data' -import { User } from '../../../shared/types/entities.types' - -const prisma = new PrismaClient() - -const org: Organization = { - uid: '7d704a62-d15e-4382-a826-1eb41563043b' -} - -async function main() { - const logger = new Logger('EngineSeed') - - logger.log('Seeding Engine database') - await prisma.$transaction(async (txn) => { - await txn.organization.create({ - data: org - }) - - // USERS - for (const user of Object.values(mockEntityData.entities.users) as User[]) { - logger.log(`Creating user ${user.uid}`) - await txn.user.create({ - data: user - }) - } - - // USER GROUPS - for (const userGroup of Object.values(mockEntityData.entities.userGroups)) { - // create the group first - logger.log(`Creating user group ${userGroup.uid}`) - await txn.userGroup.create({ - data: { - uid: userGroup.uid - } - }) - // now assign each user to it - for (const userId of userGroup.users) { - logger.log(`Assigning user ${userId} to group ${userGroup.uid}`) - await txn.userGroupMembership.create({ - data: { - userGroupId: userGroup.uid, - userId - } - }) - } - } - - // WALLETS - for (const wallet of Object.values(mockEntityData.entities.wallets)) { - logger.log(`Creating wallet ${wallet.uid}`) - await txn.wallet.create({ - data: { - uid: wallet.uid, - address: wallet.address, - accountType: wallet.accountType - } - }) - if (wallet.assignees) { - // Assign the wallet to the assignees - for (const assigneeId of wallet.assignees) { - logger.log(`Assigning wallet ${wallet.uid} to user ${assigneeId}`) - await txn.userWalletAssignment.create({ - data: { - walletId: wallet.uid, - userId: assigneeId - } - }) - } - } - } - - // WALLET GROUPS - for (const walletGroup of Object.values(mockEntityData.entities.walletGroups)) { - // create the group first - logger.log(`Creating wallet group ${walletGroup.uid}`) - await txn.walletGroup.create({ - data: { - uid: walletGroup.uid - } - }) - // now assign each wallet to it - for (const walletId of walletGroup.wallets) { - logger.log(`Assigning wallet ${walletId} to group ${walletGroup.uid}`) - await txn.walletGroupMembership.create({ - data: { - walletGroupId: walletGroup.uid, - walletId - } - }) - } - } - - // ADDRESS BOOK - for (const addressBook of Object.values(mockEntityData.entities.addressBook)) { - logger.log(`Creating address book ${addressBook.uid}`) - await txn.addressBookAccount.create({ - data: addressBook - }) - } - }) - - logger.log('Engine database germinated 🌱') -} - -main() - .then(async () => { - await prisma.$disconnect() - }) - .catch(async (e) => { - console.error(e) - await prisma.$disconnect() - process.exit(1) - }) diff --git a/apps/authz/src/shared/types/entities.types.ts b/apps/authz/src/shared/types/entities.types.ts index 7b606d879..38faf3559 100644 --- a/apps/authz/src/shared/types/entities.types.ts +++ b/apps/authz/src/shared/types/entities.types.ts @@ -1,5 +1,7 @@ import { AccountClassification, AccountType, Address, UserRole } from '@narval/authz-shared' +// TODO: Move these to shared? + // ENTITIES: user, user group, wallet, wallet group, and address book. export type Organization = { uid: string diff --git a/apps/authz/src/shared/types/policy.type.ts b/apps/authz/src/shared/types/policy.type.ts index 651d216bc..5603dad61 100644 --- a/apps/authz/src/shared/types/policy.type.ts +++ b/apps/authz/src/shared/types/policy.type.ts @@ -18,12 +18,13 @@ import { ValueOperators } from '@narval/authz-shared' import { Intents } from '@narval/transaction-request-intent' -import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger' +import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger' import { Transform, Type, plainToInstance } from 'class-transformer' import { ArrayNotEmpty, IsBoolean, IsDefined, + IsEnum, IsIn, IsNotEmpty, IsNumber, @@ -90,39 +91,42 @@ export type TimeWindow = (typeof TimeWindow)[keyof typeof TimeWindow] export class AmountCondition { @IsIn([...Object.values(FiatCurrency), '*']) + @ApiProperty({ enum: [...Object.values(FiatCurrency), '*'] }) currency: FiatCurrency | '*' - @IsIn(Object.values(ValueOperators)) - @ApiProperty({ enum: Object.values(ValueOperators) }) + @IsEnum(ValueOperators) + @ApiProperty({ enum: ValueOperators }) operator: ValueOperators @IsNotEmpty() @IsNumberString() - @ApiProperty({ type: String }) + @ApiProperty() value: string } export class ERC1155AmountCondition { @IsAssetId() + @ApiProperty() tokenId: AssetId - @IsIn(Object.values(ValueOperators)) - @ApiProperty({ enum: Object.values(ValueOperators) }) + @IsEnum(ValueOperators) + @ApiProperty({ enum: ValueOperators }) operator: ValueOperators @IsNotEmpty() @IsNumberString() - @ApiProperty({ type: String }) + @ApiProperty() value: string } export class SignMessageCondition { @IsIn([ValueOperators.EQUAL, IdentityOperators.CONTAINS]) + @ApiProperty({ enum: [ValueOperators.EQUAL, IdentityOperators.CONTAINS] }) operator: ValueOperators.EQUAL | IdentityOperators.CONTAINS @IsNotEmpty() @IsString() - @ApiProperty({ type: String }) + @ApiProperty() value: string } @@ -130,68 +134,78 @@ export class SignTypedDataDomainCondition { @IsOptional() @IsNotEmptyArrayString() @IsNumberString({}, { each: true }) + @ApiPropertyOptional() version?: string[] @IsOptional() @IsNotEmptyArrayString() @IsNumberString({}, { each: true }) + @ApiPropertyOptional() chainId?: string[] @IsOptional() @IsNotEmptyArrayString() + @ApiPropertyOptional() name?: string[] @IsOptional() @IsNotEmptyArrayString() @IsHexString({ each: true }) + @ApiPropertyOptional() verifyingContract?: Address[] } export class PermitDeadlineCondition { - @IsIn(Object.values(ValueOperators)) - @ApiProperty({ enum: Object.values(ValueOperators) }) + @IsEnum(ValueOperators) + @ApiProperty({ enum: ValueOperators }) operator: ValueOperators @IsNotEmpty() @IsNumberString() - @ApiProperty({ type: String }) + @ApiProperty() value: string // timestamp in ms } export class ApprovalCondition { @IsDefined() @IsNumber() + @ApiProperty() approvalCount: number @IsDefined() @IsBoolean() + @ApiProperty() countPrincipal: boolean @IsDefined() - @IsIn(Object.values(EntityType)) - @ApiProperty({ enum: Object.values(EntityType) }) + @IsEnum(EntityType) + @ApiProperty({ enum: EntityType }) approvalEntityType: EntityType @IsNotEmptyArrayString() + @ApiProperty() entityIds: string[] } export class SpendingLimitTimeWindow { @IsOptional() - @IsIn(Object.values(TimeWindow)) - @ApiProperty({ enum: Object.values(TimeWindow) }) + @IsEnum(TimeWindow) + @ApiPropertyOptional({ enum: TimeWindow }) type?: TimeWindow @IsNumber() @IsOptional() + @ApiPropertyOptional() value?: number // in seconds @IsNumber() @IsOptional() + @ApiPropertyOptional() startDate?: number // in seconds @IsNumber() @IsOptional() + @ApiPropertyOptional() endDate?: number // in seconds } @@ -199,50 +213,58 @@ export class SpendingLimitFilters { @IsNotEmptyArrayString() @IsAssetId({ each: true }) @IsOptional() + @ApiPropertyOptional() tokens?: AssetId[] @IsNotEmptyArrayString() @IsOptional() + @ApiPropertyOptional() users?: string[] @IsNotEmptyArrayString() @IsAccountId({ each: true }) @IsOptional() + @ApiPropertyOptional() resources?: AccountId[] @IsNotEmptyArrayString() @IsNumberString({}, { each: true }) @IsOptional() + @ApiPropertyOptional() chains?: string[] @IsNotEmptyArrayString() @IsOptional() + @ApiPropertyOptional() userGroups?: string[] @IsNotEmptyArrayString() @IsOptional() + @ApiPropertyOptional() walletGroups?: string[] } export class SpendingLimitCondition { @IsNotEmpty() @IsString() - @ApiProperty({ type: String }) + @ApiProperty() limit: string @IsOptional() - @IsIn(Object.values(FiatCurrency)) - @ApiProperty({ enum: Object.values(FiatCurrency) }) + @IsEnum(FiatCurrency) + @ApiPropertyOptional({ enum: FiatCurrency }) currency?: FiatCurrency @ValidateNested() @Type(() => SpendingLimitTimeWindow) @IsOptional() + @ApiPropertyOptional() timeWindow?: SpendingLimitTimeWindow @ValidateNested() @Type(() => SpendingLimitFilters) @IsOptional() + @ApiPropertyOptional() filters?: SpendingLimitFilters } @@ -620,7 +642,7 @@ export type PolicyCriterion = export class Policy { @IsNotEmpty() @IsString() - @ApiProperty({ type: String }) + @ApiProperty() name: string @ArrayNotEmpty() @@ -637,8 +659,8 @@ export class Policy { }) when: PolicyCriterion[] - @IsIn(Object.values(Then)) - @ApiProperty({ enum: Object.values(Then) }) + @IsEnum(Then) + @ApiProperty({ enum: Then }) then: Then } diff --git a/apps/orchestration/jest.setup.ts b/apps/orchestration/jest.setup.ts index 0470b5137..213230162 100644 --- a/apps/orchestration/jest.setup.ts +++ b/apps/orchestration/jest.setup.ts @@ -23,6 +23,6 @@ dotenv.config({ path: testEnvFile, override: true }) // Disable outgoing HTTP requests to avoid flaky tests. nock.disableNetConnect() -// Enable outgoing HTTP requests to 127.0.0.1 to allow E2E tests with -// supertestwith supertest to work. -nock.enableNetConnect('127.0.0.1') +// Enable local outgoing HTTP request to allow E2E tests with supertestwith +// supertest to work. +nock.enableNetConnect(/127.0.0.1|localhost/) diff --git a/apps/orchestration/project.json b/apps/orchestration/project.json index cc4cbb5e1..3bd103b99 100644 --- a/apps/orchestration/project.json +++ b/apps/orchestration/project.json @@ -15,7 +15,12 @@ "main": "apps/orchestration/src/main.ts", "tsConfig": "apps/orchestration/tsconfig.app.json", "isolatedConfig": true, - "webpackConfig": "apps/orchestration/webpack.config.js" + "webpackConfig": "apps/orchestration/webpack.config.js", + "transformers": [ + { + "name": "@nestjs/swagger/plugin" + } + ] }, "configurations": { "repl": { diff --git a/apps/orchestration/src/__test__/fixture/shared.fixture.ts b/apps/orchestration/src/__test__/fixture/shared.fixture.ts index c3e6fe66a..db88241ec 100644 --- a/apps/orchestration/src/__test__/fixture/shared.fixture.ts +++ b/apps/orchestration/src/__test__/fixture/shared.fixture.ts @@ -20,7 +20,7 @@ export const addressGenerator = Generator({ output: () => generateAddress() }) -export const generateSupportedChainId = (): ChainId => sample(Array.from(CHAINS.keys())) as ChainId +export const generateSupportedChainId = (): number => sample(Array.from(CHAINS.keys())) || ChainId.ETHEREUM export const chainIdGenerator = Generator({ schema: chainIdSchema, diff --git a/apps/orchestration/src/data-feed/core/service/feed.service.ts b/apps/orchestration/src/data-feed/core/service/feed.service.ts index 8ff7f1248..21080ab5e 100644 --- a/apps/orchestration/src/data-feed/core/service/feed.service.ts +++ b/apps/orchestration/src/data-feed/core/service/feed.service.ts @@ -30,7 +30,7 @@ export class FeedService { throw new DataFeedException({ message: 'Failed to gather authorization request feeds', suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, - originalError: error, + origin: error, context: { orgId: authzRequest.orgId, requestId: authzRequest.id diff --git a/apps/orchestration/src/main.ts b/apps/orchestration/src/main.ts index c9fdacc53..82e979e16 100644 --- a/apps/orchestration/src/main.ts +++ b/apps/orchestration/src/main.ts @@ -23,7 +23,9 @@ const withSwagger = (app: INestApplication): INestApplication => { .setVersion('1.0') .build() ) - SwaggerModule.setup('docs', app, document) + SwaggerModule.setup('docs', app, document, { + customSiteTitle: 'Orchestration API' + }) return app } diff --git a/apps/orchestration/src/orchestration.constant.ts b/apps/orchestration/src/orchestration.constant.ts index 1e8f7cc89..05ba42a95 100644 --- a/apps/orchestration/src/orchestration.constant.ts +++ b/apps/orchestration/src/orchestration.constant.ts @@ -1,6 +1,6 @@ import { AssetId } from '@narval/authz-shared' import { BackoffOptions } from 'bull' -import { Chain, ChainId } from './shared/core/lib/chains.lib' +import { Chain } from './shared/core/lib/chains.lib' import { FiatId } from './shared/core/type/price.type' export const REQUEST_HEADER_ORG_ID = 'x-org-id' @@ -32,7 +32,7 @@ export const FIAT_ID_USD: FiatId = 'fiat:usd' // export const ETHEREUM: Chain = { - id: ChainId.ETHEREUM, + id: 1, isTestnet: false, name: 'Ethereum Mainnet', chain: 'ETH', @@ -49,7 +49,7 @@ export const ETHEREUM: Chain = { } export const POLYGON: Chain = { - id: ChainId.POLYGON, + id: 137, isTestnet: false, name: 'Polygon Mainnet', chain: 'Polygon', @@ -69,6 +69,6 @@ export const POLYGON: Chain = { * @see https://chainid.network/chains.json */ export const CHAINS = new Map([ - [ChainId.ETHEREUM, ETHEREUM], - [ChainId.POLYGON, POLYGON] + [1, ETHEREUM], + [137, POLYGON] ]) diff --git a/apps/orchestration/src/orchestration.module.ts b/apps/orchestration/src/orchestration.module.ts index be19ab2b9..04b2700bd 100644 --- a/apps/orchestration/src/orchestration.module.ts +++ b/apps/orchestration/src/orchestration.module.ts @@ -4,6 +4,7 @@ import { APP_INTERCEPTOR } from '@nestjs/core' import { load } from './orchestration.config' import { PolicyEngineModule } from './policy-engine/policy-engine.module' import { QueueModule } from './shared/module/queue/queue.module' +import { StoreModule } from './store/store.module' import { TransferTrackingModule } from './transfer-tracking/transfer-tracking.module' @Module({ @@ -14,7 +15,8 @@ import { TransferTrackingModule } from './transfer-tracking/transfer-tracking.mo }), QueueModule.forRoot(), PolicyEngineModule, - TransferTrackingModule + TransferTrackingModule, + StoreModule ], providers: [ { diff --git a/apps/orchestration/src/policy-engine/core/exception/evaluation-consensus.exception.ts b/apps/orchestration/src/policy-engine/core/exception/consensus-agreement-not-reach.exception.ts similarity index 53% rename from apps/orchestration/src/policy-engine/core/exception/evaluation-consensus.exception.ts rename to apps/orchestration/src/policy-engine/core/exception/consensus-agreement-not-reach.exception.ts index 6c37f84fa..d68dd744e 100644 --- a/apps/orchestration/src/policy-engine/core/exception/evaluation-consensus.exception.ts +++ b/apps/orchestration/src/policy-engine/core/exception/consensus-agreement-not-reach.exception.ts @@ -1,12 +1,11 @@ -import { EvaluationResponse } from '@narval/authz-shared' import { HttpStatus } from '@nestjs/common' import { Node } from '../type/clustering.type' import { PolicyEngineException } from './policy-engine.exception' -export class EvaluationConsensusException extends PolicyEngineException { - constructor(responses: EvaluationResponse[], nodes: Node[]) { +export class ConsensusAgreementNotReachException extends PolicyEngineException { + constructor(responses: unknown[], nodes: Node[]) { super({ - message: 'Evaluation request consensus error', + message: 'Cluster nodes responses have not reach a consensus', suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, context: { responses, nodes } }) diff --git a/apps/orchestration/src/policy-engine/core/service/__test__/unit/cluster.service.spec.ts b/apps/orchestration/src/policy-engine/core/service/__test__/unit/cluster.service.spec.ts index 75ef0d7d2..cbd5f9119 100644 --- a/apps/orchestration/src/policy-engine/core/service/__test__/unit/cluster.service.spec.ts +++ b/apps/orchestration/src/policy-engine/core/service/__test__/unit/cluster.service.spec.ts @@ -12,7 +12,7 @@ import { generatePrices } from '../../../../../__test__/fixture/price.fixture' import { PriceFeedService } from '../../../../../data-feed/core/service/price-feed.service' import { ChainId } from '../../../../../shared/core/lib/chains.lib' import { ClusterNotFoundException } from '../../../../core/exception/cluster-not-found.exception' -import { EvaluationConsensusException } from '../../../../core/exception/evaluation-consensus.exception' +import { ConsensusAgreementNotReachException } from '../../../../core/exception/consensus-agreement-not-reach.exception' import { InvalidAttestationSignatureException } from '../../../../core/exception/invalid-attestation-signature.exception' import { ClusterService } from '../../../../core/service/cluster.service' import { Cluster, Node } from '../../../../core/type/clustering.type' @@ -161,7 +161,7 @@ describe(ClusterService.name, () => { authzApplicationClientMock.evaluation.mockResolvedValueOnce(permit) authzApplicationClientMock.evaluation.mockResolvedValueOnce(forbid) - expect(service.evaluation(input)).rejects.toThrow(EvaluationConsensusException) + expect(service.evaluation(input)).rejects.toThrow(ConsensusAgreementNotReachException) }) it('throws when node attestation is invalid', async () => { diff --git a/apps/orchestration/src/policy-engine/core/service/cluster.service.ts b/apps/orchestration/src/policy-engine/core/service/cluster.service.ts index d268e9cd1..44831196b 100644 --- a/apps/orchestration/src/policy-engine/core/service/cluster.service.ts +++ b/apps/orchestration/src/policy-engine/core/service/cluster.service.ts @@ -2,11 +2,11 @@ import { Decision, EvaluationRequest, EvaluationResponse, hashRequest } from '@n import { Injectable, Logger } from '@nestjs/common' import { zip } from 'lodash/fp' import { ClusterNotFoundException } from '../../core/exception/cluster-not-found.exception' -import { EvaluationConsensusException } from '../../core/exception/evaluation-consensus.exception' import { InvalidAttestationSignatureException } from '../../core/exception/invalid-attestation-signature.exception' import { UnreachableClusterException } from '../../core/exception/unreachable-cluster.exception' import { Cluster, Node } from '../../core/type/clustering.type' import { AuthzApplicationClient } from '../../http/client/authz-application.client' +import { ConsensusAgreementNotReachException } from '../exception/consensus-agreement-not-reach.exception' // eslint-disable-next-line no-restricted-imports import { getAddress, isAddressEqual, recoverMessageAddress } from 'viem' @@ -52,14 +52,14 @@ export class ClusterService { throw new ClusterNotFoundException(input.orgId) } - const hosts = cluster.nodes.map(this.getHost) + const hosts = cluster.nodes.map((node) => ClusterService.getNodeHost(node)) this.logger.log('Sending evaluation request to cluster', { clusterId: cluster.id, clusterSize: cluster.size, nodes: cluster.nodes.map((node) => ({ id: node.id, - host: this.getHost(node) + host: ClusterService.getNodeHost(node) })) }) @@ -76,7 +76,7 @@ export class ClusterService { const decision = responses[0].decision if (!responses.every((response) => response.decision === decision)) { - throw new EvaluationConsensusException(responses, cluster.nodes) + throw new ConsensusAgreementNotReachException(responses, cluster.nodes) } const evaluations = this.combineNodeResponse(cluster.nodes, responses) @@ -98,7 +98,7 @@ export class ClusterService { throw new UnreachableClusterException(cluster) } - private getHost({ host, port }: Node): string { + static getNodeHost({ host, port }: Node): string { return `http://${host}:${port}` } diff --git a/apps/orchestration/src/policy-engine/gateway/authorization-request.gateway.ts b/apps/orchestration/src/policy-engine/gateway/authorization-request.gateway.ts new file mode 100644 index 000000000..fedb9c4f2 --- /dev/null +++ b/apps/orchestration/src/policy-engine/gateway/authorization-request.gateway.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common' +import { SetOptional } from 'type-fest' +import { AuthorizationRequestService } from '../core/service/authorization-request.service' +import { Approval, AuthorizationRequest, CreateAuthorizationRequest } from '../core/type/domain.type' + +@Injectable() +export class AuthorizationRequestGateway { + constructor(private authorizationRequestService: AuthorizationRequestService) {} + + async evaluate(input: CreateAuthorizationRequest): Promise { + return this.authorizationRequestService.create(input) + } + + async findById(id: string): Promise { + return this.authorizationRequestService.findById(id) + } + + async approve(id: string, approval: SetOptional): Promise { + return this.authorizationRequestService.approve(id, approval) + } +} diff --git a/apps/orchestration/src/policy-engine/http/client/authz-application.client.ts b/apps/orchestration/src/policy-engine/http/client/authz-application.client.ts index a1a20df75..91d2191a5 100644 --- a/apps/orchestration/src/policy-engine/http/client/authz-application.client.ts +++ b/apps/orchestration/src/policy-engine/http/client/authz-application.client.ts @@ -4,6 +4,8 @@ import { HttpStatus, Injectable, Logger } from '@nestjs/common' import { catchError, lastValueFrom, map, tap } from 'rxjs' import { ApplicationException } from '../../../shared/exception/application.exception' +export class AuthzApplicationClientException extends ApplicationException {} + @Injectable() export class AuthzApplicationClient { private logger = new Logger(AuthzApplicationClient.name) @@ -17,6 +19,7 @@ export class AuthzApplicationClient { this.httpService.post(`${option.host}/evaluation`, option.data).pipe( tap((response) => { this.logger.log('Received evaluation response', { + host: option.host, status: response.status, headers: response.headers, response: response.data @@ -24,12 +27,11 @@ export class AuthzApplicationClient { }), map((response) => response.data), catchError((error) => { - throw new ApplicationException({ + throw new AuthzApplicationClientException({ message: 'Evaluation request failed', suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, - context: { - sourceError: error - } + origin: error, + context: option }) }) ) diff --git a/apps/orchestration/src/policy-engine/http/rest/dto/transaction-request.dto.ts b/apps/orchestration/src/policy-engine/http/rest/dto/transaction-request.dto.ts index e7d06337b..93eb8e71e 100644 --- a/apps/orchestration/src/policy-engine/http/rest/dto/transaction-request.dto.ts +++ b/apps/orchestration/src/policy-engine/http/rest/dto/transaction-request.dto.ts @@ -1,6 +1,6 @@ -import { Address, Hex } from '@narval/authz-shared' -import { ApiProperty } from '@nestjs/swagger' -import { Transform } from 'class-transformer' +import { Address, Hex, IsHexString } from '@narval/authz-shared' +import { ApiPropertyOptional } from '@nestjs/swagger' +import { Transform, Type } from 'class-transformer' import { IsDefined, IsEthereumAddress, @@ -17,103 +17,53 @@ class AccessListDto { @IsDefined() @IsEthereumAddress() @Transform(({ value }) => value.toLowerCase()) - @ApiProperty({ - format: 'address', - required: true, - type: 'string' - }) address: Address @IsString() - @ApiProperty({ - format: 'hexadecimal', - isArray: true, - required: true, - type: 'string' - }) + @IsHexString() storageKeys: Hex[] } export class TransactionRequestDto { @IsInt() @Min(1) - @ApiProperty({ - minimum: 1 - }) chainId: number @IsString() @IsDefined() @IsEthereumAddress() @Transform(({ value }) => value.toLowerCase()) - @ApiProperty({ - format: 'address', - type: 'string' - }) from: Address @IsNumber() @IsOptional() @Min(0) - @ApiProperty({ - minimum: 0, - required: false - }) nonce?: number - @IsOptional() - @ValidateNested() - @ApiProperty({ - isArray: true, - required: false, - type: AccessListDto - }) + @Type(() => AccessListDto) + @ValidateNested({ each: true }) accessList?: AccessListDto[] @IsString() @IsOptional() - @ApiProperty({ - format: 'hexadecimal', - required: false, - type: 'string' - }) data?: Hex @IsOptional() @Transform(({ value }) => BigInt(value)) - @ApiProperty({ - format: 'bigint', - required: false, - type: 'string' - }) gas?: bigint @IsString() @IsEthereumAddress() @IsOptional() @Transform(({ value }) => value.toLowerCase()) - @ApiProperty({ - format: 'address', - required: false, - type: 'string' - }) to?: Address | null @IsString() @IsOptional() - @ApiProperty({ - default: '2', - required: false - }) type?: '2' @IsString() @IsOptional() - @ApiProperty({ - format: 'hexadecimal', - required: false, - type: 'string' - }) value?: Hex } @@ -130,8 +80,6 @@ export class TransactionResponseDto extends TransactionRequestDto { @IsString() @IsOptional() @Transform(({ value }) => value.toString()) - @ApiProperty({ - type: 'string' - }) + @ApiPropertyOptional({ type: String }) gas?: bigint } diff --git a/apps/orchestration/src/policy-engine/policy-engine.module.ts b/apps/orchestration/src/policy-engine/policy-engine.module.ts index 806bd3db2..18cb9e14e 100644 --- a/apps/orchestration/src/policy-engine/policy-engine.module.ts +++ b/apps/orchestration/src/policy-engine/policy-engine.module.ts @@ -14,6 +14,7 @@ import { PersistenceModule } from '../shared/module/persistence/persistence.modu import { TransferTrackingModule } from '../transfer-tracking/transfer-tracking.module' import { AuthorizationRequestService } from './core/service/authorization-request.service' import { ClusterService } from './core/service/cluster.service' +import { AuthorizationRequestGateway } from './gateway/authorization-request.gateway' import { AuthzApplicationClient } from './http/client/authz-application.client' import { AuthorizationRequestController } from './http/rest/controller/authorization-request.controller' import { AuthorizationRequestRepository } from './persistence/repository/authorization-request.repository' @@ -46,6 +47,7 @@ import { AuthorizationRequestProcessingProducer } from './queue/producer/authori AuthorizationRequestRepository, AuthorizationRequestProcessingConsumer, AuthorizationRequestProcessingProducer, + AuthorizationRequestGateway, AuthzApplicationClient, ClusterService, { @@ -64,6 +66,7 @@ import { AuthorizationRequestProcessingProducer } from './queue/producer/authori provide: APP_PIPE, useClass: ValidationPipe } - ] + ], + exports: [AuthorizationRequestGateway] }) export class PolicyEngineModule {} diff --git a/apps/orchestration/src/policy-engine/queue/consumer/__test__/integration/authorization-request-processing.consumer.spec.ts b/apps/orchestration/src/policy-engine/queue/consumer/__test__/integration/authorization-request-processing.consumer.spec.ts index 08dab9d29..c4546e579 100644 --- a/apps/orchestration/src/policy-engine/queue/consumer/__test__/integration/authorization-request-processing.consumer.spec.ts +++ b/apps/orchestration/src/policy-engine/queue/consumer/__test__/integration/authorization-request-processing.consumer.spec.ts @@ -19,7 +19,7 @@ import { QueueModule } from '../../../../../shared/module/queue/queue.module' import { TransferTrackingService } from '../../../../../transfer-tracking/core/service/transfer-tracking.service' import { AuthorizationRequestAlreadyProcessingException } from '../../../../core/exception/authorization-request-already-processing.exception' import { ClusterNotFoundException } from '../../../../core/exception/cluster-not-found.exception' -import { EvaluationConsensusException } from '../../../../core/exception/evaluation-consensus.exception' +import { ConsensusAgreementNotReachException } from '../../../../core/exception/consensus-agreement-not-reach.exception' import { InvalidAttestationSignatureException } from '../../../../core/exception/invalid-attestation-signature.exception' import { UnreachableClusterException } from '../../../../core/exception/unreachable-cluster.exception' import { AuthorizationRequestService } from '../../../../core/service/authorization-request.service' @@ -174,7 +174,7 @@ describe(AuthorizationRequestProcessingConsumer.name, () => { it('stops retrying on known unrecoverable errors', async () => { const unrecoverableErrors = [ new ClusterNotFoundException(authzRequest.orgId), - new EvaluationConsensusException([], []), + new ConsensusAgreementNotReachException([], []), new UnreachableClusterException(mock()), new InvalidAttestationSignatureException('test-pubkey', 'test-recovered-pubkey'), new AuthorizationRequestAlreadyProcessingException(authzRequest) diff --git a/apps/orchestration/src/policy-engine/queue/consumer/authorization-request-processing.consumer.ts b/apps/orchestration/src/policy-engine/queue/consumer/authorization-request-processing.consumer.ts index 92f619ec5..4962efe8d 100644 --- a/apps/orchestration/src/policy-engine/queue/consumer/authorization-request-processing.consumer.ts +++ b/apps/orchestration/src/policy-engine/queue/consumer/authorization-request-processing.consumer.ts @@ -7,7 +7,7 @@ import { } from '../../../orchestration.constant' import { AuthorizationRequestAlreadyProcessingException } from '../../core/exception/authorization-request-already-processing.exception' import { ClusterNotFoundException } from '../../core/exception/cluster-not-found.exception' -import { EvaluationConsensusException } from '../../core/exception/evaluation-consensus.exception' +import { ConsensusAgreementNotReachException } from '../../core/exception/consensus-agreement-not-reach.exception' import { InvalidAttestationSignatureException } from '../../core/exception/invalid-attestation-signature.exception' import { UnreachableClusterException } from '../../core/exception/unreachable-cluster.exception' import { AuthorizationRequestService } from '../../core/service/authorization-request.service' @@ -32,7 +32,7 @@ export class AuthorizationRequestProcessingConsumer { // Short-circuits the retry mechanism on unrecoverable domain errors. // // IMPORTANT: To stop retrying a job in Bull, the process must return an - // error instance. + // error instance. If the process throws, it'll automaticaly retry. if (this.isUnrecoverableError(error)) { return error } @@ -44,7 +44,7 @@ export class AuthorizationRequestProcessingConsumer { private isUnrecoverableError(error: Error): boolean { switch (error.constructor) { case ClusterNotFoundException: - case EvaluationConsensusException: + case ConsensusAgreementNotReachException: case UnreachableClusterException: case InvalidAttestationSignatureException: case AuthorizationRequestAlreadyProcessingException: diff --git a/apps/orchestration/src/price/http/client/coin-gecko/coin-gecko.client.ts b/apps/orchestration/src/price/http/client/coin-gecko/coin-gecko.client.ts index 35db93979..03d8f1860 100644 --- a/apps/orchestration/src/price/http/client/coin-gecko/coin-gecko.client.ts +++ b/apps/orchestration/src/price/http/client/coin-gecko/coin-gecko.client.ts @@ -83,7 +83,7 @@ export class CoinGeckoClient { new CoinGeckoException({ message: 'Request to CoinGecko failed', suggestedHttpStatusCode: error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR, - originalError: error, + origin: error, context: { cause: error.cause, request: redactedRequest, @@ -102,7 +102,7 @@ export class CoinGeckoClient { new CoinGeckoException({ message: 'Unknown CoinGecko client error', suggestedHttpStatusCode: HttpStatus.INTERNAL_SERVER_ERROR, - originalError: error, + origin: error, context: { request: redactedRequest } diff --git a/apps/orchestration/src/shared/core/lib/chains.lib.ts b/apps/orchestration/src/shared/core/lib/chains.lib.ts index 9bc6817e0..d1ee8e5db 100644 --- a/apps/orchestration/src/shared/core/lib/chains.lib.ts +++ b/apps/orchestration/src/shared/core/lib/chains.lib.ts @@ -3,10 +3,14 @@ import { HttpStatus } from '@nestjs/common' import { CHAINS } from '../../../orchestration.constant' import { ApplicationException } from '../../../shared/exception/application.exception' -export enum ChainId { - ETHEREUM = 1, - POLYGON = 137 -} +// TODO (@wcalderipe, 09/02/24): After the commit bddb7b3 [1], this constant +// stop working and the application doesn't boot anymore. +// +// [1] https://github.com/narval-xyz/armory/commit/bddb7b303c0549d2a0015da485e9ac37f72dfefc +export const ChainId = { + ETHEREUM: 1, + POLYGON: 137 +} as const /** * @see https://chainid.network/chains.json diff --git a/apps/orchestration/src/shared/exception/application.exception.ts b/apps/orchestration/src/shared/exception/application.exception.ts index f9ae60056..5f4f90d20 100644 --- a/apps/orchestration/src/shared/exception/application.exception.ts +++ b/apps/orchestration/src/shared/exception/application.exception.ts @@ -4,12 +4,12 @@ export type ApplicationExceptionParams = { message: string suggestedHttpStatusCode: HttpStatus context?: unknown - originalError?: Error + origin?: Error } export class ApplicationException extends HttpException { readonly context: unknown - readonly originalError?: Error + readonly origin?: Error constructor(params: ApplicationExceptionParams) { super(params.message, params.suggestedHttpStatusCode) @@ -20,6 +20,6 @@ export class ApplicationException extends HttpException { this.name = this.constructor.name this.context = params.context - this.originalError = params.originalError + this.origin = params.origin } } diff --git a/apps/orchestration/src/shared/filter/application-exception.filter.ts b/apps/orchestration/src/shared/filter/application-exception.filter.ts index a7664c113..e71af502f 100644 --- a/apps/orchestration/src/shared/filter/application-exception.filter.ts +++ b/apps/orchestration/src/shared/filter/application-exception.filter.ts @@ -30,7 +30,7 @@ export class ApplicationExceptionFilter implements ExceptionFilter { message: exception.message, context: exception.context, stack: exception.stack, - ...(exception.originalError && { origin: exception.originalError }) + ...(exception.origin && { origin: exception.origin }) } ) } @@ -45,7 +45,7 @@ export class ApplicationExceptionFilter implements ExceptionFilter { status: exception.getStatus(), context: exception.context, stacktrace: exception.stack, - origin: exception.originalError + origin: exception.origin }) } } diff --git a/apps/orchestration/src/shared/module/persistence/persistence.module.ts b/apps/orchestration/src/shared/module/persistence/persistence.module.ts index c21f85213..95d5dcd4c 100644 --- a/apps/orchestration/src/shared/module/persistence/persistence.module.ts +++ b/apps/orchestration/src/shared/module/persistence/persistence.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common' -import { PrismaService } from '../../..//shared/module/persistence/service/prisma.service' -import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service' +import { PrismaService } from './service/prisma.service' +import { TestPrismaService } from './service/test-prisma.service' @Module({ exports: [PrismaService, TestPrismaService], diff --git a/apps/orchestration/src/shared/module/persistence/persistence.type.ts b/apps/orchestration/src/shared/module/persistence/persistence.type.ts new file mode 100644 index 000000000..4e43da930 --- /dev/null +++ b/apps/orchestration/src/shared/module/persistence/persistence.type.ts @@ -0,0 +1,3 @@ +export interface Seeder { + germinate(): Promise +} diff --git a/apps/orchestration/src/shared/module/persistence/schema/migrations/20240202163221_init/migration.sql b/apps/orchestration/src/shared/module/persistence/schema/migrations/20240202163221_init/migration.sql deleted file mode 100644 index 6101565c7..000000000 --- a/apps/orchestration/src/shared/module/persistence/schema/migrations/20240202163221_init/migration.sql +++ /dev/null @@ -1,94 +0,0 @@ --- CreateEnum -CREATE TYPE "AuthorizationRequestStatus" AS ENUM ('CREATED', 'FAILED', 'CANCELED', 'PROCESSING', 'APPROVING', 'PERMITTED', 'FORBIDDEN'); - --- CreateTable -CREATE TABLE "organization" ( - "id" VARCHAR(255) NOT NULL, - "name" TEXT NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "organization_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "authorization_request" ( - "id" VARCHAR(255) NOT NULL, - "org_id" TEXT NOT NULL, - "status" "AuthorizationRequestStatus" NOT NULL DEFAULT 'CREATED', - "action" TEXT NOT NULL, - "request" JSONB NOT NULL, - "idempotency_key" TEXT, - "authn_alg" TEXT NOT NULL, - "authn_pub_key" TEXT NOT NULL, - "authn_sig" TEXT NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "authorization_request_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "authorization_request_approval" ( - "id" VARCHAR(255) NOT NULL, - "request_id" TEXT NOT NULL, - "alg" TEXT NOT NULL, - "pub_key" TEXT NOT NULL, - "sig" TEXT NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "authorization_request_approval_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "evaluation_log" ( - "id" VARCHAR(255) NOT NULL, - "org_id" TEXT NOT NULL, - "request_id" TEXT NOT NULL, - "decision" TEXT NOT NULL, - "signature" TEXT, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "evaluation_log_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "approved_transfer" ( - "id" VARCHAR(255) NOT NULL, - "org_id" TEXT NOT NULL, - "request_id" TEXT NOT NULL, - "chain_id" INTEGER NOT NULL, - "from" TEXT NOT NULL, - "to" TEXT NOT NULL, - "token" TEXT NOT NULL, - "amount" TEXT NOT NULL, - "rates" JSONB NOT NULL, - "initiated_by" TEXT NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "approved_transfer_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "feed" ( - "id" VARCHAR(255) NOT NULL, - "org_id" TEXT NOT NULL, - "request_id" TEXT NOT NULL, - "source" TEXT NOT NULL, - "sig" TEXT, - "alg" TEXT, - "pubKey" TEXT, - "data" JSONB NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "feed_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "authorization_request_idempotency_key_key" ON "authorization_request"("idempotency_key"); - --- AddForeignKey -ALTER TABLE "authorization_request_approval" ADD CONSTRAINT "authorization_request_approval_request_id_fkey" FOREIGN KEY ("request_id") REFERENCES "authorization_request"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "evaluation_log" ADD CONSTRAINT "evaluation_log_request_id_fkey" FOREIGN KEY ("request_id") REFERENCES "authorization_request"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/orchestration/src/shared/module/persistence/schema/migrations/20240214154959_init/migration.sql b/apps/orchestration/src/shared/module/persistence/schema/migrations/20240214154959_init/migration.sql new file mode 100644 index 000000000..3435b2533 --- /dev/null +++ b/apps/orchestration/src/shared/module/persistence/schema/migrations/20240214154959_init/migration.sql @@ -0,0 +1,214 @@ +-- CreateEnum +CREATE TYPE "AuthorizationRequestStatus" AS ENUM ('CREATED', 'FAILED', 'CANCELED', 'PROCESSING', 'APPROVING', 'PERMITTED', 'FORBIDDEN'); + +-- CreateTable +CREATE TABLE "organization" ( + "id" VARCHAR(255) NOT NULL, + "name" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "organization_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "authorization_request" ( + "id" VARCHAR(255) NOT NULL, + "org_id" TEXT NOT NULL, + "status" "AuthorizationRequestStatus" NOT NULL DEFAULT 'CREATED', + "action" TEXT NOT NULL, + "request" JSONB NOT NULL, + "idempotency_key" TEXT, + "authn_alg" TEXT NOT NULL, + "authn_pub_key" TEXT NOT NULL, + "authn_sig" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "authorization_request_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "authorization_request_approval" ( + "id" VARCHAR(255) NOT NULL, + "request_id" TEXT NOT NULL, + "alg" TEXT NOT NULL, + "pub_key" TEXT NOT NULL, + "sig" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "authorization_request_approval_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "evaluation_log" ( + "id" VARCHAR(255) NOT NULL, + "org_id" TEXT NOT NULL, + "request_id" TEXT NOT NULL, + "decision" TEXT NOT NULL, + "signature" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "evaluation_log_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "approved_transfer" ( + "id" VARCHAR(255) NOT NULL, + "org_id" TEXT NOT NULL, + "request_id" TEXT NOT NULL, + "chain_id" INTEGER NOT NULL, + "from" TEXT NOT NULL, + "to" TEXT NOT NULL, + "token" TEXT NOT NULL, + "amount" TEXT NOT NULL, + "rates" JSONB NOT NULL, + "initiated_by" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "approved_transfer_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "feed" ( + "id" VARCHAR(255) NOT NULL, + "org_id" TEXT NOT NULL, + "request_id" TEXT NOT NULL, + "source" TEXT NOT NULL, + "sig" TEXT, + "alg" TEXT, + "pubKey" TEXT, + "data" JSONB NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "feed_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "organization_entity" ( + "uid" TEXT NOT NULL, + + CONSTRAINT "organization_entity_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "auth_credential_entity" ( + "org_id" TEXT NOT NULL, + "uid" TEXT NOT NULL, + "pub_key" TEXT NOT NULL, + "alg" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + + CONSTRAINT "auth_credential_entity_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "user_entity" ( + "org_id" TEXT NOT NULL, + "uid" TEXT NOT NULL, + "role" TEXT NOT NULL, + + CONSTRAINT "user_entity_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "user_group_entity" ( + "org_id" TEXT NOT NULL, + "uid" TEXT NOT NULL, + + CONSTRAINT "user_group_entity_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "user_group_member_entity" ( + "user_uid" TEXT NOT NULL, + "user_group_uid" TEXT NOT NULL, + + CONSTRAINT "user_group_member_entity_pkey" PRIMARY KEY ("user_uid","user_group_uid") +); + +-- CreateTable +CREATE TABLE "wallet_entity" ( + "org_id" TEXT NOT NULL, + "uid" TEXT NOT NULL, + "address" TEXT NOT NULL, + "account_type" TEXT NOT NULL, + "chain_id" INTEGER, + + CONSTRAINT "wallet_entity_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "wallet_group_entity" ( + "org_id" TEXT NOT NULL, + "uid" TEXT NOT NULL, + + CONSTRAINT "wallet_group_entity_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "wallet_group_member_entity" ( + "wallet_uid" TEXT NOT NULL, + "wallet_group_uid" TEXT NOT NULL, + + CONSTRAINT "wallet_group_member_entity_pkey" PRIMARY KEY ("wallet_uid","wallet_group_uid") +); + +-- CreateTable +CREATE TABLE "user_wallet_entity" ( + "org_id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "wallet_id" TEXT NOT NULL, + + CONSTRAINT "user_wallet_entity_pkey" PRIMARY KEY ("user_id","wallet_id") +); + +-- CreateTable +CREATE TABLE "address_book_account_entity" ( + "org_id" TEXT NOT NULL, + "uid" TEXT NOT NULL, + "address" TEXT NOT NULL, + "chain_id" INTEGER NOT NULL, + "classification" TEXT NOT NULL, + + CONSTRAINT "address_book_account_entity_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "token_entity" ( + "org_id" TEXT NOT NULL, + "uid" TEXT NOT NULL, + "address" TEXT NOT NULL, + "symbol" TEXT NOT NULL, + "chain_id" INTEGER NOT NULL, + "decimals" INTEGER NOT NULL, + + CONSTRAINT "token_entity_pkey" PRIMARY KEY ("uid") +); + +-- CreateIndex +CREATE UNIQUE INDEX "authorization_request_idempotency_key_key" ON "authorization_request"("idempotency_key"); + +-- AddForeignKey +ALTER TABLE "authorization_request_approval" ADD CONSTRAINT "authorization_request_approval_request_id_fkey" FOREIGN KEY ("request_id") REFERENCES "authorization_request"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "evaluation_log" ADD CONSTRAINT "evaluation_log_request_id_fkey" FOREIGN KEY ("request_id") REFERENCES "authorization_request"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "user_group_member_entity" ADD CONSTRAINT "user_group_member_entity_user_uid_fkey" FOREIGN KEY ("user_uid") REFERENCES "user_entity"("uid") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "user_group_member_entity" ADD CONSTRAINT "user_group_member_entity_user_group_uid_fkey" FOREIGN KEY ("user_group_uid") REFERENCES "user_group_entity"("uid") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "wallet_group_member_entity" ADD CONSTRAINT "wallet_group_member_entity_wallet_uid_fkey" FOREIGN KEY ("wallet_uid") REFERENCES "wallet_entity"("uid") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "wallet_group_member_entity" ADD CONSTRAINT "wallet_group_member_entity_wallet_group_uid_fkey" FOREIGN KEY ("wallet_group_uid") REFERENCES "wallet_group_entity"("uid") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "user_wallet_entity" ADD CONSTRAINT "user_wallet_entity_wallet_id_fkey" FOREIGN KEY ("wallet_id") REFERENCES "wallet_entity"("uid") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "user_wallet_entity" ADD CONSTRAINT "user_wallet_entity_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user_entity"("uid") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/orchestration/src/shared/module/persistence/schema/schema.prisma b/apps/orchestration/src/shared/module/persistence/schema/schema.prisma index 7f6c827bf..1020cc92d 100644 --- a/apps/orchestration/src/shared/module/persistence/schema/schema.prisma +++ b/apps/orchestration/src/shared/module/persistence/schema/schema.prisma @@ -103,7 +103,7 @@ model ApprovedTransfer { } // -// Data Feed Module +// Data Feed Module // model Feed { @@ -119,3 +119,131 @@ model Feed { @@map("feed") } + +// +// Store Module +// + +model OrganizationEntity { + uid String @id + + @@map("organization_entity") +} + +// TODO (@wcalderipe, 12/02/24): Evaluate the location of this model. The +// Identity Provider (IdP) seems the right place. +model AuthCredentialEntity { + orgId String @map("org_id") + + uid String @id + pubKey String @map("pub_key") + alg String + userId String @map("user_id") + + @@map("auth_credential_entity") +} + +model UserEntity { + orgId String @map("org_id") + + uid String @id + role String + + wallets UserWalletEntity[] + groups UserGroupMemberEntity[] + + @@map("user_entity") +} + +model UserGroupEntity { + orgId String @map("org_id") + + uid String @id + + members UserGroupMemberEntity[] + + @@map("user_group_entity") +} + +model UserGroupMemberEntity { + userId String @map("user_uid") + groupId String @map("user_group_uid") + + user UserEntity @relation(fields: [userId], references: [uid], onDelete: Cascade, onUpdate: Cascade) + group UserGroupEntity @relation(fields: [groupId], references: [uid], onDelete: Cascade, onUpdate: Cascade) + + @@id([userId, groupId]) + @@map("user_group_member_entity") +} + +model WalletEntity { + orgId String @map("org_id") + + uid String @id + address String + accountType String @map("account_type") + /// Chain ID is only needed for chain-specific wallets (smart accounts). + chainId Int? @map("chain_id") + + groups WalletGroupMemberEntity[] + users UserWalletEntity[] + + @@map("wallet_entity") +} + +model WalletGroupEntity { + orgId String @map("org_id") + + uid String @id + + members WalletGroupMemberEntity[] + + @@map("wallet_group_entity") +} + +model WalletGroupMemberEntity { + walletId String @map("wallet_uid") + groupId String @map("wallet_group_uid") + + wallet WalletEntity @relation(fields: [walletId], references: [uid], onDelete: Cascade, onUpdate: Cascade) + group WalletGroupEntity @relation(fields: [groupId], references: [uid], onDelete: Cascade, onUpdate: Cascade) + + @@id([walletId, groupId]) + @@map("wallet_group_member_entity") +} + +model UserWalletEntity { + orgId String @map("org_id") + + userId String @map("user_id") + walletId String @map("wallet_id") + + wallet WalletEntity @relation(fields: [walletId], references: [uid], onDelete: Cascade, onUpdate: Cascade) + user UserEntity @relation(fields: [userId], references: [uid], onDelete: Cascade, onUpdate: Cascade) + + @@id([userId, walletId]) + @@map("user_wallet_entity") +} + +model AddressBookAccountEntity { + orgId String @map("org_id") + + uid String @id + address String + chainId Int @map("chain_id") + classification String + + @@map("address_book_account_entity") +} + +model TokenEntity { + orgId String @map("org_id") + + uid String @id + address String + symbol String + chainId Int @map("chain_id") + decimals Int + + @@map("token_entity") +} diff --git a/apps/orchestration/src/shared/module/persistence/seed.ts b/apps/orchestration/src/shared/module/persistence/seed.ts index 573a14139..5cdf6539f 100644 --- a/apps/orchestration/src/shared/module/persistence/seed.ts +++ b/apps/orchestration/src/shared/module/persistence/seed.ts @@ -1,5 +1,9 @@ import { Logger } from '@nestjs/common' +import { NestFactory } from '@nestjs/core' import { Organization, PrismaClient } from '@prisma/client/orchestration' +import { ORGANIZATION } from 'packages/authz-shared/src/lib/dev.fixture' +import { OrchestrationModule } from '../../../orchestration.module' +import { EntityStoreSeed } from '../../../store/entity/persistence/entity-store.seed' import { germinate as germinateTransferTrackingModule } from '../../../transfer-tracking/persistence/transfer.seed' const now = new Date() @@ -7,7 +11,7 @@ const prisma = new PrismaClient() const orgs: Organization[] = [ { - id: '7d704a62-d15e-4382-a826-1eb41563043b', + id: ORGANIZATION.uid, name: 'Dev', createdAt: now, updatedAt: now @@ -16,6 +20,11 @@ const orgs: Organization[] = [ async function main() { const logger = new Logger('OrchestrationSeed') + // Create a standalone application without any network listeners like controllers. + // + // See https://docs.nestjs.com/standalone-applications + const application = await NestFactory.createApplicationContext(OrchestrationModule) + const entityStoreSeed = application.get(EntityStoreSeed) logger.log('Seeding Orchestration database') @@ -26,9 +35,15 @@ async function main() { }) } - await germinateTransferTrackingModule(prisma) + try { + await entityStoreSeed.germinate() + // TODO (@wcalderipe, 15/02/24): Refactor to a seeder provider like entity store. + await germinateTransferTrackingModule(prisma) - logger.log('Orchestration database germinated 🌱') + logger.log('Orchestration database germinated 🌱') + } finally { + await application.close() + } } main() diff --git a/apps/orchestration/src/store/entity/__test__/e2e/address-book.spec.ts b/apps/orchestration/src/store/entity/__test__/e2e/address-book.spec.ts new file mode 100644 index 000000000..f5a527fd7 --- /dev/null +++ b/apps/orchestration/src/store/entity/__test__/e2e/address-book.spec.ts @@ -0,0 +1,104 @@ +import { AccountClassification, Action, OrganizationEntity, Signature } from '@narval/authz-shared' +import { HttpStatus, INestApplication } from '@nestjs/common' +import { ConfigModule } from '@nestjs/config' +import { Test, TestingModule } from '@nestjs/testing' +import request from 'supertest' +import { generateSignature } from '../../../../__test__/fixture/authorization-request.fixture' +import { load } from '../../../../orchestration.config' +import { REQUEST_HEADER_ORG_ID } from '../../../../orchestration.constant' +import { PolicyEngineModule } from '../../../../policy-engine/policy-engine.module' +import { PersistenceModule } from '../../../../shared/module/persistence/persistence.module' +import { TestPrismaService } from '../../../../shared/module/persistence/service/test-prisma.service' +import { QueueModule } from '../../../../shared/module/queue/queue.module' +import { EntityStoreModule } from '../../entity-store.module' +import { AddressBookRepository } from '../../persistence/repository/address-book.repository' +import { OrganizationRepository } from '../../persistence/repository/organization.repository' + +const API_RESOURCE_USER_ENTITY = '/store/address-book' + +describe('Address Book Entity', () => { + let app: INestApplication + let module: TestingModule + let testPrismaService: TestPrismaService + let orgRepository: OrganizationRepository + let addressBookRepository: AddressBookRepository + + const organization: OrganizationEntity = { + uid: 'ac1374c2-fd62-4b6e-bd49-a4afcdcb91cc' + } + + const authentication: Signature = generateSignature() + + const approvals: Signature[] = [generateSignature(), generateSignature()] + + const nonce = 'b6d826b4-72cb-4c14-a6ca-235a2d8e9060' + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: [load], + isGlobal: true + }), + PersistenceModule, + QueueModule.forRoot(), + PolicyEngineModule, + EntityStoreModule + ] + }).compile() + + testPrismaService = module.get(TestPrismaService) + orgRepository = module.get(OrganizationRepository) + addressBookRepository = module.get(AddressBookRepository) + + app = module.createNestApplication() + + await app.init() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await orgRepository.create(organization.uid) + }) + + afterEach(async () => { + await testPrismaService.truncateAll() + }) + + describe(`POST ${API_RESOURCE_USER_ENTITY}`, () => { + it('registers a new account in the address book', async () => { + const account = { + uid: '089c131e-9507-412a-8b4a-45e6a8213d77', + address: '0xaaa8ee1cbaa1856f4550c6fc24abb16c5c9b2a43', + chainId: 1, + classification: AccountClassification.INTERNAL + } + + const payload = { + authentication, + approvals, + request: { + nonce, + action: Action.CREATE_ADDRESS_BOOK_ACCOUNT, + account + } + } + + const { status, body } = await request(app.getHttpServer()) + .post(API_RESOURCE_USER_ENTITY) + .set(REQUEST_HEADER_ORG_ID, organization.uid) + .send(payload) + + const actualAccount = await addressBookRepository.findById(account.uid) + + expect(body).toEqual({ account }) + expect(actualAccount).toEqual(account) + expect(status).toEqual(HttpStatus.CREATED) + }) + }) +}) diff --git a/apps/orchestration/src/store/entity/__test__/e2e/credential.spec.ts b/apps/orchestration/src/store/entity/__test__/e2e/credential.spec.ts new file mode 100644 index 000000000..9bf2da219 --- /dev/null +++ b/apps/orchestration/src/store/entity/__test__/e2e/credential.spec.ts @@ -0,0 +1,105 @@ +import { Action, Alg, AuthCredential, OrganizationEntity, Signature } from '@narval/authz-shared' +import { HttpStatus, INestApplication } from '@nestjs/common' +import { ConfigModule } from '@nestjs/config' +import { Test, TestingModule } from '@nestjs/testing' +import request from 'supertest' +import { sha256 } from 'viem' +import { generateSignature } from '../../../../__test__/fixture/authorization-request.fixture' +import { load } from '../../../../orchestration.config' +import { REQUEST_HEADER_ORG_ID } from '../../../../orchestration.constant' +import { PolicyEngineModule } from '../../../../policy-engine/policy-engine.module' +import { PersistenceModule } from '../../../../shared/module/persistence/persistence.module' +import { TestPrismaService } from '../../../../shared/module/persistence/service/test-prisma.service' +import { QueueModule } from '../../../../shared/module/queue/queue.module' +import { EntityStoreModule } from '../../entity-store.module' +import { CredentialRepository } from '../../persistence/repository/credential.repository' +import { OrganizationRepository } from '../../persistence/repository/organization.repository' + +const API_RESOURCE_USER_ENTITY = '/store/credentials' + +describe('Credential Entity', () => { + let app: INestApplication + let module: TestingModule + let testPrismaService: TestPrismaService + let orgRepository: OrganizationRepository + let credentialRepository: CredentialRepository + + const organization: OrganizationEntity = { + uid: 'ac1374c2-fd62-4b6e-bd49-a4afcdcb91cc' + } + + const authentication: Signature = generateSignature() + + const approvals: Signature[] = [generateSignature(), generateSignature()] + + const nonce = 'b6d826b4-72cb-4c14-a6ca-235a2d8e9060' + + const credential: AuthCredential = { + uid: sha256('0x501d5c2ce1ef208aadf9131a98baa593258cfa06'), + userId: '68182475-4365-4c4d-a7bd-295daad634c9', + alg: Alg.ES256K, + pubKey: '0x501d5c2ce1ef208aadf9131a98baa593258cfa06' + } + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: [load], + isGlobal: true + }), + PersistenceModule, + QueueModule.forRoot(), + PolicyEngineModule, + EntityStoreModule + ] + }).compile() + + testPrismaService = module.get(TestPrismaService) + orgRepository = module.get(OrganizationRepository) + credentialRepository = module.get(CredentialRepository) + + app = module.createNestApplication() + + await app.init() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await orgRepository.create(organization.uid) + }) + + afterEach(async () => { + await testPrismaService.truncateAll() + }) + + describe(`POST ${API_RESOURCE_USER_ENTITY}`, () => { + it('registers credential entity', async () => { + const payload = { + authentication, + approvals, + request: { + nonce, + action: Action.CREATE_CREDENTIAL, + credential + } + } + + const { status, body } = await request(app.getHttpServer()) + .post(API_RESOURCE_USER_ENTITY) + .set(REQUEST_HEADER_ORG_ID, organization.uid) + .send(payload) + + const actualCredential = await credentialRepository.findById(credential.uid) + + expect(body).toEqual({ credential }) + expect(actualCredential).toEqual(credential) + expect(status).toEqual(HttpStatus.CREATED) + }) + }) +}) diff --git a/apps/orchestration/src/store/entity/__test__/e2e/entity.spec.ts b/apps/orchestration/src/store/entity/__test__/e2e/entity.spec.ts new file mode 100644 index 000000000..2da08789e --- /dev/null +++ b/apps/orchestration/src/store/entity/__test__/e2e/entity.spec.ts @@ -0,0 +1,246 @@ +import { + AccountClassification, + AccountType, + AddressBookAccountEntity, + Alg, + AuthCredential, + Entities, + OrganizationEntity, + TokenEntity, + UserEntity, + UserGroupEntity, + UserRole, + UserWalletEntity, + WalletEntity, + WalletGroupEntity +} from '@narval/authz-shared' +import { HttpStatus, INestApplication } from '@nestjs/common' +import { ConfigModule } from '@nestjs/config' +import { Test, TestingModule } from '@nestjs/testing' +import { map } from 'lodash/fp' +import request from 'supertest' +import { sha256 } from 'viem' +import { load } from '../../../../orchestration.config' +import { REQUEST_HEADER_ORG_ID } from '../../../../orchestration.constant' +import { PolicyEngineModule } from '../../../../policy-engine/policy-engine.module' +import { PersistenceModule } from '../../../../shared/module/persistence/persistence.module' +import { TestPrismaService } from '../../../../shared/module/persistence/service/test-prisma.service' +import { QueueModule } from '../../../../shared/module/queue/queue.module' +import { EntityStoreModule } from '../../entity-store.module' +import { AddressBookRepository } from '../../persistence/repository/address-book.repository' +import { CredentialRepository } from '../../persistence/repository/credential.repository' +import { OrganizationRepository } from '../../persistence/repository/organization.repository' +import { TokenRepository } from '../../persistence/repository/token.repository' +import { UserGroupRepository } from '../../persistence/repository/user-group.repository' +import { UserWalletRepository } from '../../persistence/repository/user-wallet.repository' +import { UserRepository } from '../../persistence/repository/user.repository' +import { WalletGroupRepository } from '../../persistence/repository/wallet-group.repository' +import { WalletRepository } from '../../persistence/repository/wallet.repository' + +const API_RESOURCE_USER_ENTITY = '/store/entities' + +describe('Entity', () => { + let app: INestApplication + let module: TestingModule + let testPrismaService: TestPrismaService + + let addressBookRepository: AddressBookRepository + let credentialRepository: CredentialRepository + let orgRepository: OrganizationRepository + let tokenRepository: TokenRepository + let userGroupRepository: UserGroupRepository + let userRepository: UserRepository + let userWalletRepository: UserWalletRepository + let walletGroupRepository: WalletGroupRepository + let walletRepository: WalletRepository + + const organization: OrganizationEntity = { + uid: 'ac1374c2-fd62-4b6e-bd49-a4afcdcb91cc' + } + + const users: UserEntity[] = [ + { + uid: '2d7a6811-509f-4bee-90fb-e382fc127de5', + role: UserRole.ADMIN + }, + { + uid: '70d4128a-4b47-4944-859b-c570c69d3120', + role: UserRole.ADMIN + } + ] + + const credentials: AuthCredential[] = [ + { + uid: sha256('0x5a4c3948723e02cbdef57d0eeb0fa8e2fc8f81fc'), + pubKey: '0x5a4c3948723e02cbdef57d0eeb0fa8e2fc8f81fc', + alg: Alg.ES256K, + userId: users[0].uid + } + ] + + const wallets: WalletEntity[] = [ + { + uid: 'a5c1fd4e-b021-4fad-b5a6-256b434916ef', + address: '0x648edbd0e1bd5f15d58481bc7f034a790f9741fe', + accountType: AccountType.EOA, + chainId: 1 + }, + { + uid: '3fe39a8e-1721-4111-bc3a-4f89c0d67594', + address: '0x40710fae7b7d1200b644a579ddee65aecd7a991a', + accountType: AccountType.EOA, + chainId: 1 + } + ] + + const walletGroups: WalletGroupEntity[] = [ + { + uid: 'a104baeb-c9dd-4066-ae56-d85168715f90', + wallets: map('uid', wallets) + } + ] + + const userWallets: UserWalletEntity[] = [ + { + userId: users[0].uid, + walletId: wallets[0].uid + }, + { + userId: users[1].uid, + walletId: wallets[1].uid + } + ] + + const userGroups: UserGroupEntity[] = [ + { + uid: 'd160dab5-211a-447d-9c25-2772e3ecbe17', + users: [users[0].uid] + } + ] + + const addressBook: AddressBookAccountEntity[] = [ + { + uid: '6b88f31f-564f-4463-86a6-28c3ad9105ff', + address: '0xeff7eda2dd2567b80f96ba5eb292e399cc360a05', + chainId: 1, + classification: AccountClassification.EXTERNAL + } + ] + + const tokens: TokenEntity[] = [ + { + uid: '2ece731a-51be-4b4f-91de-5665eacf7006', + address: '0x63d74e23f70f66511417bc7acf95f002d1dbd33c', + chainId: 1, + symbol: 'AAA', + decimals: 18 + } + ] + + const sortByUid = (entities: E[]): E[] => { + return entities.sort((a, b) => a.uid.localeCompare(b.uid)) + } + + const getDeterministicEntities = ({ + users, + wallets, + walletGroups, + userGroups, + addressBook, + credentials + }: Entities): Entities => { + return { + addressBook: sortByUid(addressBook), + credentials: sortByUid(credentials), + tokens: sortByUid(tokens), + userGroups: sortByUid(userGroups), + users: sortByUid(users), + walletGroups: sortByUid(walletGroups), + wallets: sortByUid(wallets) + } + } + + const bulkCreate = async ( + orgId: string, + entities: E[], + repository: { create: (orgId: string, entity: E) => Promise } + ): Promise => { + await Promise.all(entities.map((entity) => repository.create(orgId, entity))) + } + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: [load], + isGlobal: true + }), + PersistenceModule, + QueueModule.forRoot(), + PolicyEngineModule, + EntityStoreModule + ] + }).compile() + + testPrismaService = module.get(TestPrismaService) + + addressBookRepository = module.get(AddressBookRepository) + credentialRepository = module.get(CredentialRepository) + orgRepository = module.get(OrganizationRepository) + tokenRepository = module.get(TokenRepository) + userGroupRepository = module.get(UserGroupRepository) + userRepository = module.get(UserRepository) + userWalletRepository = module.get(UserWalletRepository) + walletGroupRepository = module.get(WalletGroupRepository) + walletRepository = module.get(WalletRepository) + + app = module.createNestApplication() + + await app.init() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await orgRepository.create(organization.uid) + await tokenRepository.create(organization.uid, tokens) + + // The order entities are created matters. + await bulkCreate(organization.uid, users, userRepository) + await bulkCreate(organization.uid, wallets, walletRepository) + await bulkCreate(organization.uid, walletGroups, walletGroupRepository) + await bulkCreate(organization.uid, userGroups, userGroupRepository) + await bulkCreate(organization.uid, addressBook, addressBookRepository) + await bulkCreate(organization.uid, userWallets, userWalletRepository) + await bulkCreate(organization.uid, credentials, credentialRepository) + }) + + afterEach(async () => { + await testPrismaService.truncateAll() + }) + + describe(`GET ${API_RESOURCE_USER_ENTITY}`, () => { + it('responds with the organization entities', async () => { + const { status, body } = await request(app.getHttpServer()) + .get(API_RESOURCE_USER_ENTITY) + .set(REQUEST_HEADER_ORG_ID, organization.uid) + + expect(getDeterministicEntities(body)).toEqual( + getDeterministicEntities({ + addressBook, + credentials, + userGroups, + users, + walletGroups, + wallets, + tokens + }) + ) + expect(status).toEqual(HttpStatus.OK) + }) + }) +}) diff --git a/apps/orchestration/src/store/entity/__test__/e2e/organization.spec.ts b/apps/orchestration/src/store/entity/__test__/e2e/organization.spec.ts new file mode 100644 index 000000000..35bf11031 --- /dev/null +++ b/apps/orchestration/src/store/entity/__test__/e2e/organization.spec.ts @@ -0,0 +1,117 @@ +import { Action, Alg, AuthCredential, OrganizationEntity, Signature, UserRole } from '@narval/authz-shared' +import { HttpStatus, INestApplication } from '@nestjs/common' +import { ConfigModule } from '@nestjs/config' +import { Test, TestingModule } from '@nestjs/testing' +import request from 'supertest' +import { sha256 } from 'viem' +import { generateSignature } from '../../../../__test__/fixture/authorization-request.fixture' +import { load } from '../../../../orchestration.config' +import { PolicyEngineModule } from '../../../../policy-engine/policy-engine.module' +import { PersistenceModule } from '../../../../shared/module/persistence/persistence.module' +import { TestPrismaService } from '../../../../shared/module/persistence/service/test-prisma.service' +import { QueueModule } from '../../../../shared/module/queue/queue.module' +import { EntityStoreModule } from '../../entity-store.module' +import { CredentialRepository } from '../../persistence/repository/credential.repository' +import { OrganizationRepository } from '../../persistence/repository/organization.repository' +import { UserRepository } from '../../persistence/repository/user.repository' + +const API_RESOURCE_USER_ENTITY = '/store/organizations' + +describe('Organization Entity', () => { + let app: INestApplication + let module: TestingModule + let testPrismaService: TestPrismaService + let orgRepository: OrganizationRepository + let userRepository: UserRepository + let authCredentialRepository: CredentialRepository + + const organization: OrganizationEntity = { + uid: 'ac1374c2-fd62-4b6e-bd49-a4afcdcb91cc' + } + + const nonce = 'b6d826b4-72cb-4c14-a6ca-235a2d8e9060' + + const authentication: Signature = generateSignature() + + const approvals: Signature[] = [generateSignature(), generateSignature()] + + const credential: AuthCredential = { + uid: sha256('0x501d5c2ce1ef208aadf9131a98baa593258cfa06'), + userId: '68182475-4365-4c4d-a7bd-295daad634c9', + alg: Alg.ES256K, + pubKey: '0x501d5c2ce1ef208aadf9131a98baa593258cfa06' + } + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: [load], + isGlobal: true + }), + PersistenceModule, + QueueModule.forRoot(), + PolicyEngineModule, + EntityStoreModule + ] + }).compile() + + testPrismaService = module.get(TestPrismaService) + orgRepository = module.get(OrganizationRepository) + userRepository = module.get(UserRepository) + authCredentialRepository = module.get(CredentialRepository) + + app = module.createNestApplication() + + await app.init() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + afterEach(async () => { + await testPrismaService.truncateAll() + }) + + describe(`POST ${API_RESOURCE_USER_ENTITY}`, () => { + it('creates organization and root user', async () => { + const payload = { + authentication, + approvals, + request: { + action: Action.CREATE_ORGANIZATION, + nonce, + organization: { + uid: organization.uid, + credential + } + } + } + + const expectedRootUser = { + uid: credential.userId, + role: UserRole.ROOT + } + + const { status, body } = await request(app.getHttpServer()).post(API_RESOURCE_USER_ENTITY).send(payload) + + const actualOrganization = await orgRepository.findById(organization.uid) + const actualRootUser = await userRepository.findById(credential.userId) + const actualCredential = await authCredentialRepository.findById(credential.uid) + + expect(body).toEqual({ + organization, + rootCredential: credential, + rootUser: expectedRootUser + }) + expect(status).toEqual(HttpStatus.CREATED) + + expect(actualOrganization).toEqual(organization) + expect(actualCredential).toEqual(credential) + expect(actualRootUser).toEqual(expectedRootUser) + }) + }) +}) diff --git a/apps/orchestration/src/store/entity/__test__/e2e/token.spec.ts b/apps/orchestration/src/store/entity/__test__/e2e/token.spec.ts new file mode 100644 index 000000000..7c4611e9a --- /dev/null +++ b/apps/orchestration/src/store/entity/__test__/e2e/token.spec.ts @@ -0,0 +1,113 @@ +import { Action, OrganizationEntity, Signature, TokenEntity } from '@narval/authz-shared' +import { HttpStatus, INestApplication } from '@nestjs/common' +import { ConfigModule } from '@nestjs/config' +import { Test, TestingModule } from '@nestjs/testing' +import request from 'supertest' +import { generateSignature } from '../../../../__test__/fixture/authorization-request.fixture' +import { load } from '../../../../orchestration.config' +import { REQUEST_HEADER_ORG_ID } from '../../../../orchestration.constant' +import { PolicyEngineModule } from '../../../../policy-engine/policy-engine.module' +import { PersistenceModule } from '../../../../shared/module/persistence/persistence.module' +import { TestPrismaService } from '../../../../shared/module/persistence/service/test-prisma.service' +import { QueueModule } from '../../../../shared/module/queue/queue.module' +import { EntityStoreModule } from '../../entity-store.module' +import { OrganizationRepository } from '../../persistence/repository/organization.repository' +import { TokenRepository } from '../../persistence/repository/token.repository' + +const API_RESOURCE_USER_ENTITY = '/store/tokens' + +describe('Token Entity', () => { + let app: INestApplication + let module: TestingModule + let testPrismaService: TestPrismaService + let orgRepository: OrganizationRepository + let tokenRepository: TokenRepository + + const organization: OrganizationEntity = { + uid: 'ac1374c2-fd62-4b6e-bd49-a4afcdcb91cc' + } + + const authentication: Signature = generateSignature() + + const approvals: Signature[] = [generateSignature(), generateSignature()] + + const nonce = 'b6d826b4-72cb-4c14-a6ca-235a2d8e9060' + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: [load], + isGlobal: true + }), + PersistenceModule, + QueueModule.forRoot(), + PolicyEngineModule, + EntityStoreModule + ] + }).compile() + + testPrismaService = module.get(TestPrismaService) + orgRepository = module.get(OrganizationRepository) + tokenRepository = module.get(TokenRepository) + + app = module.createNestApplication() + + await app.init() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await orgRepository.create(organization.uid) + }) + + afterEach(async () => { + await testPrismaService.truncateAll() + }) + + describe(`POST ${API_RESOURCE_USER_ENTITY}`, () => { + it('registers new tokens', async () => { + const tokenOne: TokenEntity = { + uid: '2ece731a-51be-4b4f-91de-5665eacf7006', + address: '0x63d74e23f70f66511417bc7acf95f002d1dbd33c', + chainId: 1, + symbol: 'AAA', + decimals: 18 + } + + const tokenTwo: TokenEntity = { + uid: '50972056-fb28-4701-a2b6-b784a7d23e70', + address: '0x33a184D851506C23d7B97f3d8d062483B9Cf495c', + chainId: 1, + symbol: 'BBB', + decimals: 18 + } + + const payload = { + authentication, + approvals, + request: { + action: Action.REGISTER_TOKENS, + nonce, + tokens: [tokenOne, tokenTwo] + } + } + + const { status, body } = await request(app.getHttpServer()) + .post(API_RESOURCE_USER_ENTITY) + .set(REQUEST_HEADER_ORG_ID, organization.uid) + .send(payload) + + const actualTokens = await tokenRepository.findByOrgId(organization.uid) + + expect(body).toEqual({ tokens: [tokenOne, tokenTwo] }) + expect(actualTokens).toEqual([tokenOne, tokenTwo]) + expect(status).toEqual(HttpStatus.CREATED) + }) + }) +}) diff --git a/apps/orchestration/src/store/entity/__test__/e2e/user-group.spec.ts b/apps/orchestration/src/store/entity/__test__/e2e/user-group.spec.ts new file mode 100644 index 000000000..fa9144c7c --- /dev/null +++ b/apps/orchestration/src/store/entity/__test__/e2e/user-group.spec.ts @@ -0,0 +1,121 @@ +import { Action, Signature, UserRole } from '@narval/authz-shared' +import { HttpStatus, INestApplication } from '@nestjs/common' +import { ConfigModule } from '@nestjs/config' +import { Test, TestingModule } from '@nestjs/testing' +import { Organization } from '@prisma/client/orchestration' +import request from 'supertest' +import { generateSignature } from '../../../../__test__/fixture/authorization-request.fixture' +import { load } from '../../../../orchestration.config' +import { REQUEST_HEADER_ORG_ID } from '../../../../orchestration.constant' +import { PolicyEngineModule } from '../../../../policy-engine/policy-engine.module' +import { PersistenceModule } from '../../../../shared/module/persistence/persistence.module' +import { TestPrismaService } from '../../../../shared/module/persistence/service/test-prisma.service' +import { QueueModule } from '../../../../shared/module/queue/queue.module' +import { EntityStoreModule } from '../../entity-store.module' +import { UserGroupRepository } from '../../persistence/repository/user-group.repository' +import { UserRepository } from '../../persistence/repository/user.repository' + +const API_RESOURCE_USER_ENTITY = '/store/user-groups' + +describe('User Group Entity', () => { + let app: INestApplication + let module: TestingModule + let testPrismaService: TestPrismaService + let userRepository: UserRepository + let userGroupRepository: UserGroupRepository + + const org: Organization = { + id: 'ac1374c2-fd62-4b6e-bd49-a4afcdcb91cc', + name: 'Test Evaluation', + createdAt: new Date(), + updatedAt: new Date() + } + + const authentication: Signature = generateSignature() + + const approvals: Signature[] = [generateSignature(), generateSignature()] + + const user = { + uid: '68182475-4365-4c4d-a7bd-295daad634c9', + role: UserRole.MEMBER + } + + const nonce = 'b6d826b4-72cb-4c14-a6ca-235a2d8e9060' + + const groupId = '2a1509ad-ea87-422e-bebd-974547cd4fee' + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: [load], + isGlobal: true + }), + PersistenceModule, + QueueModule.forRoot(), + PolicyEngineModule, + EntityStoreModule + ] + }).compile() + + testPrismaService = module.get(TestPrismaService) + userRepository = module.get(UserRepository) + userGroupRepository = module.get(UserGroupRepository) + + app = module.createNestApplication() + + await app.init() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await testPrismaService.getClient().organization.create({ data: org }) + await userRepository.create(org.id, user) + }) + + afterEach(async () => { + await testPrismaService.truncateAll() + }) + + describe(`POST ${API_RESOURCE_USER_ENTITY}`, () => { + it('assigns a user to a group', async () => { + const payload = { + authentication, + approvals, + request: { + action: Action.ASSIGN_USER_GROUP, + nonce, + data: { + userId: user.uid, + groupId + } + } + } + + const { status, body } = await request(app.getHttpServer()) + .post(API_RESOURCE_USER_ENTITY) + .set(REQUEST_HEADER_ORG_ID, org.id) + .send(payload) + + const group = await userGroupRepository.findById(groupId) + + expect(body).toEqual({ + data: { + userId: user.uid, + groupId + } + }) + + expect(group).toEqual({ + uid: groupId, + users: [user.uid] + }) + expect(status).toEqual(HttpStatus.CREATED) + }) + }) +}) diff --git a/apps/orchestration/src/store/entity/__test__/e2e/user-wallet.spec.ts b/apps/orchestration/src/store/entity/__test__/e2e/user-wallet.spec.ts new file mode 100644 index 000000000..95df22419 --- /dev/null +++ b/apps/orchestration/src/store/entity/__test__/e2e/user-wallet.spec.ts @@ -0,0 +1,127 @@ +import { + AccountType, + Action, + OrganizationEntity, + Signature, + UserEntity, + UserRole, + WalletEntity +} from '@narval/authz-shared' +import { HttpStatus, INestApplication } from '@nestjs/common' +import { ConfigModule } from '@nestjs/config' +import { Test, TestingModule } from '@nestjs/testing' +import request from 'supertest' +import { generateSignature } from '../../../../__test__/fixture/authorization-request.fixture' +import { load } from '../../../../orchestration.config' +import { REQUEST_HEADER_ORG_ID } from '../../../../orchestration.constant' +import { PolicyEngineModule } from '../../../../policy-engine/policy-engine.module' +import { PersistenceModule } from '../../../../shared/module/persistence/persistence.module' +import { TestPrismaService } from '../../../../shared/module/persistence/service/test-prisma.service' +import { QueueModule } from '../../../../shared/module/queue/queue.module' +import { EntityStoreModule } from '../../entity-store.module' +import { OrganizationRepository } from '../../persistence/repository/organization.repository' +import { UserRepository } from '../../persistence/repository/user.repository' +import { WalletRepository } from '../../persistence/repository/wallet.repository' + +const API_RESOURCE_USER_ENTITY = '/store/user-wallets' + +describe('User Wallet Entity', () => { + let app: INestApplication + let module: TestingModule + let testPrismaService: TestPrismaService + let orgRepository: OrganizationRepository + let walletRepository: WalletRepository + let userRepository: UserRepository + + const organization: OrganizationEntity = { + uid: 'ac1374c2-fd62-4b6e-bd49-a4afcdcb91cc' + } + + const authentication: Signature = generateSignature() + + const approvals: Signature[] = [generateSignature(), generateSignature()] + + const nonce = 'b6d826b4-72cb-4c14-a6ca-235a2d8e9060' + + const wallet: WalletEntity = { + uid: 'a5c1fd4e-b021-4fad-b5a6-256b434916ef', + address: '0x648edbd0e1bd5f15d58481bc7f034a790f9741fe', + accountType: AccountType.EOA, + chainId: 1 + } + + const user: UserEntity = { + uid: '2d7a6811-509f-4bee-90fb-e382fc127de5', + role: UserRole.ADMIN + } + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: [load], + isGlobal: true + }), + PersistenceModule, + QueueModule.forRoot(), + PolicyEngineModule, + EntityStoreModule + ] + }).compile() + + orgRepository = module.get(OrganizationRepository) + testPrismaService = module.get(TestPrismaService) + userRepository = module.get(UserRepository) + walletRepository = module.get(WalletRepository) + + app = module.createNestApplication() + + await app.init() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await orgRepository.create(organization.uid) + await walletRepository.create(organization.uid, wallet) + await userRepository.create(organization.uid, user) + }) + + afterEach(async () => { + await testPrismaService.truncateAll() + }) + + describe(`POST ${API_RESOURCE_USER_ENTITY}`, () => { + it('assigns a wallet to a user', async () => { + const payload = { + authentication, + approvals, + request: { + nonce, + action: Action.ASSIGN_USER_WALLET, + data: { + userId: user.uid, + walletId: wallet.uid + } + } + } + + const { status, body } = await request(app.getHttpServer()) + .post(API_RESOURCE_USER_ENTITY) + .set(REQUEST_HEADER_ORG_ID, organization.uid) + .send(payload) + + expect(body).toEqual({ + data: { + userId: user.uid, + walletId: wallet.uid + } + }) + expect(status).toEqual(HttpStatus.CREATED) + }) + }) +}) diff --git a/apps/orchestration/src/store/entity/__test__/e2e/user.spec.ts b/apps/orchestration/src/store/entity/__test__/e2e/user.spec.ts new file mode 100644 index 000000000..3e0be7723 --- /dev/null +++ b/apps/orchestration/src/store/entity/__test__/e2e/user.spec.ts @@ -0,0 +1,224 @@ +import { Action, Alg, AuthCredential, Signature, UserEntity, UserRole } from '@narval/authz-shared' +import { HttpStatus, INestApplication } from '@nestjs/common' +import { ConfigModule } from '@nestjs/config' +import { Test, TestingModule } from '@nestjs/testing' +import { Organization } from '@prisma/client/orchestration' +import request from 'supertest' +import { sha256 } from 'viem' +import { generateSignature } from '../../../../__test__/fixture/authorization-request.fixture' +import { load } from '../../../../orchestration.config' +import { REQUEST_HEADER_ORG_ID } from '../../../../orchestration.constant' +import { PolicyEngineModule } from '../../../../policy-engine/policy-engine.module' +import { PersistenceModule } from '../../../../shared/module/persistence/persistence.module' +import { TestPrismaService } from '../../../../shared/module/persistence/service/test-prisma.service' +import { QueueModule } from '../../../../shared/module/queue/queue.module' +import { EntityStoreModule } from '../../entity-store.module' +import { CredentialRepository } from '../../persistence/repository/credential.repository' +import { UserRepository } from '../../persistence/repository/user.repository' + +const API_RESOURCE_USER_ENTITY = '/store/users' + +describe('User Entity', () => { + let app: INestApplication + let module: TestingModule + let testPrismaService: TestPrismaService + let userRepository: UserRepository + let authCredentialRepository: CredentialRepository + + const org: Organization = { + id: 'ac1374c2-fd62-4b6e-bd49-a4afcdcb91cc', + name: 'Test Evaluation', + createdAt: new Date(), + updatedAt: new Date() + } + + const authentication: Signature = generateSignature() + + const approvals: Signature[] = [generateSignature(), generateSignature()] + + const user: UserEntity = { + uid: '68182475-4365-4c4d-a7bd-295daad634c9', + role: UserRole.MEMBER + } + + const nonce = 'b6d826b4-72cb-4c14-a6ca-235a2d8e9060' + + const credential: AuthCredential = { + uid: sha256('0x501d5c2ce1ef208aadf9131a98baa593258cfa06'), + userId: '68182475-4365-4c4d-a7bd-295daad634c9', + alg: Alg.ES256K, + pubKey: '0x501d5c2ce1ef208aadf9131a98baa593258cfa06' + } + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: [load], + isGlobal: true + }), + PersistenceModule, + QueueModule.forRoot(), + PolicyEngineModule, + EntityStoreModule + ] + }).compile() + + testPrismaService = module.get(TestPrismaService) + userRepository = module.get(UserRepository) + authCredentialRepository = module.get(CredentialRepository) + + app = module.createNestApplication() + + await app.init() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await testPrismaService.getClient().organization.create({ data: org }) + }) + + afterEach(async () => { + await testPrismaService.truncateAll() + }) + + describe(`POST ${API_RESOURCE_USER_ENTITY}`, () => { + it('creates user entity with credential', async () => { + const payload = { + authentication, + approvals, + request: { + action: Action.CREATE_USER, + nonce, + user: { + ...user, + credential + } + } + } + + const { status, body } = await request(app.getHttpServer()) + .post(API_RESOURCE_USER_ENTITY) + .set(REQUEST_HEADER_ORG_ID, org.id) + .send(payload) + + const actualUser = await userRepository.findById(user.uid) + const actualCredential = await authCredentialRepository.findById(credential.uid) + + expect(status).toEqual(HttpStatus.CREATED) + expect(body).toEqual({ user }) + + expect(actualUser).toEqual(user) + expect(actualCredential).toEqual(credential) + }) + + it('creates user entity without credential', async () => { + const payload = { + authentication, + approvals, + request: { + action: Action.CREATE_USER, + nonce, + user + } + } + + const { status, body } = await request(app.getHttpServer()) + .post(API_RESOURCE_USER_ENTITY) + .set(REQUEST_HEADER_ORG_ID, org.id) + .send(payload) + + const actualUser = await userRepository.findById(user.uid) + const actualCredential = await authCredentialRepository.findById(credential.uid) + + expect(status).toEqual(HttpStatus.CREATED) + expect(body).toEqual({ user }) + + expect(actualUser).toEqual(user) + expect(actualCredential).toEqual(null) + }) + + it('responds with error on user entity duplication', async () => { + const payload = { + authentication, + approvals, + request: { + action: Action.CREATE_USER, + nonce, + user + } + } + + const { status: firstResponseStatus, body: firstResponseBody } = await request(app.getHttpServer()) + .post(API_RESOURCE_USER_ENTITY) + .set(REQUEST_HEADER_ORG_ID, org.id) + .send(payload) + + const { status: secondResponseStatus, body: secondResponseBody } = await request(app.getHttpServer()) + .post(API_RESOURCE_USER_ENTITY) + .set(REQUEST_HEADER_ORG_ID, org.id) + .send(payload) + + expect(firstResponseBody).toEqual({ user }) + expect(firstResponseStatus).toEqual(HttpStatus.CREATED) + + expect(secondResponseBody).toEqual({ + statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + message: 'Internal server error' + }) + expect(secondResponseStatus).toEqual(HttpStatus.INTERNAL_SERVER_ERROR) + }) + }) + + describe(`PATCH ${API_RESOURCE_USER_ENTITY}/:uid`, () => { + it('updates user entity', async () => { + const create = { + authentication, + approvals, + request: { + action: Action.CREATE_USER, + nonce, + user + } + } + + const update = { + authentication, + approvals, + request: { + action: Action.UPDATE_USER, + nonce, + user: { + ...user, + role: UserRole.MANAGER + } + } + } + + const { status: createResponseStatus } = await request(app.getHttpServer()) + .post(API_RESOURCE_USER_ENTITY) + .set(REQUEST_HEADER_ORG_ID, org.id) + .send(create) + + const { status: updateResponseStatus, body: updateResponseBody } = await request(app.getHttpServer()) + .patch(`${API_RESOURCE_USER_ENTITY}/${user.uid}`) + .set(REQUEST_HEADER_ORG_ID, org.id) + .send(update) + + expect(createResponseStatus).toEqual(HttpStatus.CREATED) + + expect(updateResponseBody).toEqual({ + user: { + uid: user.uid, + role: update.request.user.role + } + }) + expect(updateResponseStatus).toEqual(HttpStatus.OK) + }) + }) +}) diff --git a/apps/orchestration/src/store/entity/__test__/e2e/wallet-group.spec.ts b/apps/orchestration/src/store/entity/__test__/e2e/wallet-group.spec.ts new file mode 100644 index 000000000..19d375232 --- /dev/null +++ b/apps/orchestration/src/store/entity/__test__/e2e/wallet-group.spec.ts @@ -0,0 +1,122 @@ +import { AccountType, Action, OrganizationEntity, Signature, WalletEntity } from '@narval/authz-shared' +import { HttpStatus, INestApplication } from '@nestjs/common' +import { ConfigModule } from '@nestjs/config' +import { Test, TestingModule } from '@nestjs/testing' +import request from 'supertest' +import { generateSignature } from '../../../../__test__/fixture/authorization-request.fixture' +import { load } from '../../../../orchestration.config' +import { REQUEST_HEADER_ORG_ID } from '../../../../orchestration.constant' +import { PolicyEngineModule } from '../../../../policy-engine/policy-engine.module' +import { PersistenceModule } from '../../../../shared/module/persistence/persistence.module' +import { TestPrismaService } from '../../../../shared/module/persistence/service/test-prisma.service' +import { QueueModule } from '../../../../shared/module/queue/queue.module' +import { EntityStoreModule } from '../../entity-store.module' +import { OrganizationRepository } from '../../persistence/repository/organization.repository' +import { WalletGroupRepository } from '../../persistence/repository/wallet-group.repository' +import { WalletRepository } from '../../persistence/repository/wallet.repository' + +const API_RESOURCE_USER_ENTITY = '/store/wallet-groups' + +describe('Wallet Group Entity', () => { + let app: INestApplication + let module: TestingModule + let testPrismaService: TestPrismaService + let orgRepository: OrganizationRepository + let walletGroupRepository: WalletGroupRepository + let walletRepository: WalletRepository + + const organization: OrganizationEntity = { + uid: 'ac1374c2-fd62-4b6e-bd49-a4afcdcb91cc' + } + + const authentication: Signature = generateSignature() + + const approvals: Signature[] = [generateSignature(), generateSignature()] + + const nonce = 'b6d826b4-72cb-4c14-a6ca-235a2d8e9060' + + const wallet: WalletEntity = { + uid: 'a5c1fd4e-b021-4fad-b5a6-256b434916ef', + address: '0x648edbd0e1bd5f15d58481bc7f034a790f9741fe', + accountType: AccountType.EOA, + chainId: 1 + } + + const groupId = '2a1509ad-ea87-422e-bebd-974547cd4fee' + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: [load], + isGlobal: true + }), + PersistenceModule, + QueueModule.forRoot(), + PolicyEngineModule, + EntityStoreModule + ] + }).compile() + + testPrismaService = module.get(TestPrismaService) + orgRepository = module.get(OrganizationRepository) + walletRepository = module.get(WalletRepository) + walletGroupRepository = module.get(WalletGroupRepository) + + app = module.createNestApplication() + + await app.init() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await orgRepository.create(organization.uid) + await walletRepository.create(organization.uid, wallet) + }) + + afterEach(async () => { + await testPrismaService.truncateAll() + }) + + describe(`POST ${API_RESOURCE_USER_ENTITY}`, () => { + it('assigns a wallet to a group', async () => { + const payload = { + authentication, + approvals, + request: { + action: Action.ASSIGN_WALLET_GROUP, + nonce, + data: { + groupId, + walletId: wallet.uid + } + } + } + + const { status, body } = await request(app.getHttpServer()) + .post(API_RESOURCE_USER_ENTITY) + .set(REQUEST_HEADER_ORG_ID, organization.uid) + .send(payload) + + const actualGroup = await walletGroupRepository.findById(groupId) + + expect(body).toEqual({ + data: { + groupId, + walletId: wallet.uid + } + }) + expect(status).toEqual(HttpStatus.CREATED) + + expect(actualGroup).toEqual({ + uid: groupId, + wallets: [wallet.uid] + }) + }) + }) +}) diff --git a/apps/orchestration/src/store/entity/__test__/e2e/wallet.spec.ts b/apps/orchestration/src/store/entity/__test__/e2e/wallet.spec.ts new file mode 100644 index 000000000..9cc9e2007 --- /dev/null +++ b/apps/orchestration/src/store/entity/__test__/e2e/wallet.spec.ts @@ -0,0 +1,104 @@ +import { AccountType, Action, OrganizationEntity, Signature } from '@narval/authz-shared' +import { HttpStatus, INestApplication } from '@nestjs/common' +import { ConfigModule } from '@nestjs/config' +import { Test, TestingModule } from '@nestjs/testing' +import request from 'supertest' +import { generateSignature } from '../../../../__test__/fixture/authorization-request.fixture' +import { load } from '../../../../orchestration.config' +import { REQUEST_HEADER_ORG_ID } from '../../../../orchestration.constant' +import { PolicyEngineModule } from '../../../../policy-engine/policy-engine.module' +import { PersistenceModule } from '../../../../shared/module/persistence/persistence.module' +import { TestPrismaService } from '../../../../shared/module/persistence/service/test-prisma.service' +import { QueueModule } from '../../../../shared/module/queue/queue.module' +import { EntityStoreModule } from '../../entity-store.module' +import { OrganizationRepository } from '../../persistence/repository/organization.repository' +import { WalletRepository } from '../../persistence/repository/wallet.repository' + +const API_RESOURCE_USER_ENTITY = '/store/wallets' + +describe('Wallet Entity', () => { + let app: INestApplication + let module: TestingModule + let testPrismaService: TestPrismaService + let orgRepository: OrganizationRepository + let walletRepository: WalletRepository + + const organization: OrganizationEntity = { + uid: 'ac1374c2-fd62-4b6e-bd49-a4afcdcb91cc' + } + + const nonce = 'b6d826b4-72cb-4c14-a6ca-235a2d8e9060' + + const authentication: Signature = generateSignature() + + const approvals: Signature[] = [generateSignature(), generateSignature()] + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: [load], + isGlobal: true + }), + PersistenceModule, + QueueModule.forRoot(), + PolicyEngineModule, + EntityStoreModule + ] + }).compile() + + testPrismaService = module.get(TestPrismaService) + orgRepository = module.get(OrganizationRepository) + walletRepository = module.get(WalletRepository) + + app = module.createNestApplication() + + await app.init() + }) + + afterAll(async () => { + await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + beforeEach(async () => { + await orgRepository.create(organization.uid) + }) + + afterEach(async () => { + await testPrismaService.truncateAll() + }) + + describe(`POST ${API_RESOURCE_USER_ENTITY}`, () => { + it('creates a new wallet entity', async () => { + const wallet = { + uid: 'a5c1fd4e-b021-4fad-b5a6-256b434916ef', + address: '0x648edbd0e1bd5f15d58481bc7f034a790f9741fe', + accountType: AccountType.EOA, + chainId: 1 + } + + const payload = { + authentication, + approvals, + request: { + action: Action.REGISTER_WALLET, + nonce, + wallet + } + } + + const { status, body } = await request(app.getHttpServer()) + .post(API_RESOURCE_USER_ENTITY) + .set(REQUEST_HEADER_ORG_ID, organization.uid) + .send(payload) + + const actualWallet = await walletRepository.findById(wallet.uid) + + expect(body).toEqual({ wallet }) + expect(status).toEqual(HttpStatus.CREATED) + expect(actualWallet).toEqual(wallet) + }) + }) +}) diff --git a/apps/orchestration/src/store/entity/core/service/address-book.service.ts b/apps/orchestration/src/store/entity/core/service/address-book.service.ts new file mode 100644 index 000000000..cf893230c --- /dev/null +++ b/apps/orchestration/src/store/entity/core/service/address-book.service.ts @@ -0,0 +1,12 @@ +import { AddressBookAccountEntity } from '@narval/authz-shared' +import { Injectable } from '@nestjs/common' +import { AddressBookRepository } from '../../persistence/repository/address-book.repository' + +@Injectable() +export class AddressBookService { + constructor(private addressBookRepository: AddressBookRepository) {} + + create(orgId: string, account: AddressBookAccountEntity): Promise { + return this.addressBookRepository.create(orgId, account) + } +} diff --git a/apps/orchestration/src/store/entity/core/service/credential.service.ts b/apps/orchestration/src/store/entity/core/service/credential.service.ts new file mode 100644 index 000000000..55724b691 --- /dev/null +++ b/apps/orchestration/src/store/entity/core/service/credential.service.ts @@ -0,0 +1,12 @@ +import { AuthCredential, CreateCredentialRequest } from '@narval/authz-shared' +import { Injectable } from '@nestjs/common' +import { CredentialRepository } from '../../persistence/repository/credential.repository' + +@Injectable() +export class CredentialService { + constructor(private credentialRepository: CredentialRepository) {} + + create(orgId: string, request: CreateCredentialRequest): Promise { + return this.credentialRepository.create(orgId, request.request.credential) + } +} diff --git a/apps/orchestration/src/store/entity/core/service/entity.service.ts b/apps/orchestration/src/store/entity/core/service/entity.service.ts new file mode 100644 index 000000000..abd2102d2 --- /dev/null +++ b/apps/orchestration/src/store/entity/core/service/entity.service.ts @@ -0,0 +1,44 @@ +import { Entities } from '@narval/authz-shared' +import { Injectable } from '@nestjs/common' +import { AddressBookRepository } from '../../persistence/repository/address-book.repository' +import { CredentialRepository } from '../../persistence/repository/credential.repository' +import { TokenRepository } from '../../persistence/repository/token.repository' +import { UserGroupRepository } from '../../persistence/repository/user-group.repository' +import { UserRepository } from '../../persistence/repository/user.repository' +import { WalletGroupRepository } from '../../persistence/repository/wallet-group.repository' +import { WalletRepository } from '../../persistence/repository/wallet.repository' + +@Injectable() +export class EntityService { + constructor( + private addressBookRepository: AddressBookRepository, + private credentialRepository: CredentialRepository, + private tokenRepository: TokenRepository, + private userGroupRepository: UserGroupRepository, + private userRepository: UserRepository, + private walletGroupRepository: WalletGroupRepository, + private walletRepository: WalletRepository + ) {} + + async getEntities(orgId: string): Promise { + const [addressBook, credentials, tokens, userGroups, users, walletGroups, wallets] = await Promise.all([ + this.addressBookRepository.findByOrgId(orgId), + this.credentialRepository.findByOrgId(orgId), + this.tokenRepository.findByOrgId(orgId), + this.userGroupRepository.findByOrgId(orgId), + this.userRepository.findByOrgId(orgId), + this.walletGroupRepository.findByOrgId(orgId), + this.walletRepository.findByOrgId(orgId) + ]) + + return { + addressBook, + credentials, + tokens, + userGroups, + users, + walletGroups, + wallets + } + } +} diff --git a/apps/orchestration/src/store/entity/core/service/organization.service.ts b/apps/orchestration/src/store/entity/core/service/organization.service.ts new file mode 100644 index 000000000..347d02e3c --- /dev/null +++ b/apps/orchestration/src/store/entity/core/service/organization.service.ts @@ -0,0 +1,32 @@ +import { AuthCredential, OrganizationEntity, UserEntity, UserRole } from '@narval/authz-shared' +import { Injectable } from '@nestjs/common' +import { OrganizationRepository } from '../../persistence/repository/organization.repository' +import { UserService } from './user.service' + +@Injectable() +export class OrganizationService { + constructor(private orgRepository: OrganizationRepository, private userService: UserService) {} + + async create(input: { uid: string; rootCredential: AuthCredential }): Promise<{ + organization: OrganizationEntity + rootUser: UserEntity + rootCredential: AuthCredential + }> { + const { uid, rootCredential } = input + + const rootUser: UserEntity = { + uid: input.rootCredential.userId, + role: UserRole.ROOT + } + + await this.userService.create(uid, rootUser, input.rootCredential) + + const organization = await this.orgRepository.create(uid) + + return { + organization, + rootUser, + rootCredential + } + } +} diff --git a/apps/orchestration/src/store/entity/core/service/token.service.ts b/apps/orchestration/src/store/entity/core/service/token.service.ts new file mode 100644 index 000000000..c33e847d2 --- /dev/null +++ b/apps/orchestration/src/store/entity/core/service/token.service.ts @@ -0,0 +1,12 @@ +import { RegisterTokensRequest, TokenEntity } from '@narval/authz-shared' +import { Injectable } from '@nestjs/common' +import { TokenRepository } from '../../persistence/repository/token.repository' + +@Injectable() +export class TokenService { + constructor(private tokenRepository: TokenRepository) {} + + register(orgId: string, request: RegisterTokensRequest): Promise { + return this.tokenRepository.create(orgId, request.request.tokens) + } +} diff --git a/apps/orchestration/src/store/entity/core/service/user-group.service.ts b/apps/orchestration/src/store/entity/core/service/user-group.service.ts new file mode 100644 index 000000000..b6e099d76 --- /dev/null +++ b/apps/orchestration/src/store/entity/core/service/user-group.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@nestjs/common' +import { UserGroupRepository } from '../../persistence/repository/user-group.repository' + +@Injectable() +export class UserGroupService { + constructor(private userGroupRepository: UserGroupRepository) {} + + async assign(orgId: string, userId: string, groupId: string): Promise { + const group = await this.userGroupRepository.findById(groupId) + + if (group) { + await this.userGroupRepository.update({ + ...group, + users: group.users.concat(userId) + }) + } else { + await this.userGroupRepository.create(orgId, { + uid: groupId, + users: [userId] + }) + } + + return true + } +} diff --git a/apps/orchestration/src/store/entity/core/service/user.service.ts b/apps/orchestration/src/store/entity/core/service/user.service.ts new file mode 100644 index 000000000..ebc43c469 --- /dev/null +++ b/apps/orchestration/src/store/entity/core/service/user.service.ts @@ -0,0 +1,28 @@ +import { AuthCredential, UserEntity, UserRole, UserWalletEntity } from '@narval/authz-shared' +import { Injectable } from '@nestjs/common' +import { UserWalletRepository } from '../../persistence/repository/user-wallet.repository' +import { UserRepository } from '../../persistence/repository/user.repository' + +@Injectable() +export class UserService { + constructor(private userRepository: UserRepository, private userWalletRepository: UserWalletRepository) {} + + create(orgId: string, user: UserEntity, credential?: AuthCredential): Promise { + return this.userRepository.create(orgId, user, credential) + } + + delete(uid: string): Promise { + return this.userRepository.delete(uid) + } + + async grantRole(uid: string, role: UserRole): Promise { + return this.userRepository.update({ + uid, + role + }) + } + + async assignWallet(orgId: string, assignment: UserWalletEntity): Promise { + return this.userWalletRepository.create(orgId, assignment) + } +} diff --git a/apps/orchestration/src/store/entity/core/service/wallet.service.ts b/apps/orchestration/src/store/entity/core/service/wallet.service.ts new file mode 100644 index 000000000..9dad1601e --- /dev/null +++ b/apps/orchestration/src/store/entity/core/service/wallet.service.ts @@ -0,0 +1,22 @@ +import { WalletEntity, WalletGroupMembership } from '@narval/authz-shared' +import { Injectable } from '@nestjs/common' +import { WalletGroupRepository } from '../../persistence/repository/wallet-group.repository' +import { WalletRepository } from '../../persistence/repository/wallet.repository' + +@Injectable() +export class WalletService { + constructor(private walletRepository: WalletRepository, private walletGroupRepository: WalletGroupRepository) {} + + async create(orgId: string, wallet: WalletEntity): Promise { + return this.walletRepository.create(orgId, wallet) + } + + async assignGroup(orgId: string, walletId: string, groupId: string): Promise { + await this.walletGroupRepository.create(orgId, { + uid: groupId, + wallets: [walletId] + }) + + return { groupId, walletId } + } +} diff --git a/apps/orchestration/src/store/entity/entity-store.constant.ts b/apps/orchestration/src/store/entity/entity-store.constant.ts new file mode 100644 index 000000000..eaf3e2f67 --- /dev/null +++ b/apps/orchestration/src/store/entity/entity-store.constant.ts @@ -0,0 +1,2 @@ +export const API_PREFIX = '/store' +export const API_TAG = 'Entity Store' diff --git a/apps/orchestration/src/store/entity/entity-store.module.ts b/apps/orchestration/src/store/entity/entity-store.module.ts new file mode 100644 index 000000000..1d29742fb --- /dev/null +++ b/apps/orchestration/src/store/entity/entity-store.module.ts @@ -0,0 +1,79 @@ +import { ClassSerializerInterceptor, Module, ValidationPipe } from '@nestjs/common' +import { ConfigModule } from '@nestjs/config' +import { APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core' +import { load } from '../../orchestration.config' +import { PolicyEngineModule } from '../../policy-engine/policy-engine.module' +import { PersistenceModule } from '../../shared/module/persistence/persistence.module' +import { AddressBookService } from './core/service/address-book.service' +import { CredentialService } from './core/service/credential.service' +import { EntityService } from './core/service/entity.service' +import { OrganizationService } from './core/service/organization.service' +import { TokenService } from './core/service/token.service' +import { UserGroupService } from './core/service/user-group.service' +import { UserService } from './core/service/user.service' +import { WalletService } from './core/service/wallet.service' +import { AddressBookController } from './http/rest/controller/address-book.controller' +import { CredentialController } from './http/rest/controller/credential.controller' +import { EntityController } from './http/rest/controller/entity.controller' +import { OrganizationController } from './http/rest/controller/organization.controller' +import { TokenController } from './http/rest/controller/token.controller' +import { UserGroupController } from './http/rest/controller/user-group.controller' +import { UserWalletController } from './http/rest/controller/user-wallet.controller' +import { UserController } from './http/rest/controller/user.controller' +import { WalletGroupController } from './http/rest/controller/wallet-group.controller' +import { WalletController } from './http/rest/controller/wallet.controller' +import { EntityStoreSeed } from './persistence/entity-store.seed' +import { AddressBookRepository } from './persistence/repository/address-book.repository' +import { CredentialRepository } from './persistence/repository/credential.repository' +import { OrganizationRepository } from './persistence/repository/organization.repository' +import { TokenRepository } from './persistence/repository/token.repository' +import { UserGroupRepository } from './persistence/repository/user-group.repository' +import { UserWalletRepository } from './persistence/repository/user-wallet.repository' +import { UserRepository } from './persistence/repository/user.repository' +import { WalletGroupRepository } from './persistence/repository/wallet-group.repository' +import { WalletRepository } from './persistence/repository/wallet.repository' + +@Module({ + imports: [ConfigModule.forRoot({ load: [load] }), PersistenceModule, PolicyEngineModule], + controllers: [ + AddressBookController, + CredentialController, + EntityController, + OrganizationController, + TokenController, + UserController, + UserGroupController, + UserWalletController, + WalletController, + WalletGroupController + ], + providers: [ + AddressBookRepository, + AddressBookService, + CredentialRepository, + CredentialService, + EntityService, + EntityStoreSeed, + OrganizationRepository, + OrganizationService, + TokenRepository, + TokenService, + UserGroupRepository, + UserGroupService, + UserRepository, + UserService, + UserWalletRepository, + WalletGroupRepository, + WalletRepository, + WalletService, + { + provide: APP_INTERCEPTOR, + useClass: ClassSerializerInterceptor + }, + { + provide: APP_PIPE, + useClass: ValidationPipe + } + ] +}) +export class EntityStoreModule {} diff --git a/apps/orchestration/src/store/entity/http/rest/controller/address-book.controller.ts b/apps/orchestration/src/store/entity/http/rest/controller/address-book.controller.ts new file mode 100644 index 000000000..fe8483a2b --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/controller/address-book.controller.ts @@ -0,0 +1,36 @@ +import { Body, Controller, HttpStatus, Post } from '@nestjs/common' +import { ApiHeader, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' +import { REQUEST_HEADER_ORG_ID } from '../../../../../orchestration.constant' +import { OrgId } from '../../../../../shared/decorator/org-id.decorator' +import { AddressBookService } from '../../../core/service/address-book.service' +import { API_PREFIX, API_TAG } from '../../../entity-store.constant' +import { CreateAddressBookAccountRequestDto } from '../dto/create-address-book-request.dto' +import { CreateAddressBookAccountResponseDto } from '../dto/create-address-book-response.dto' + +@Controller(`${API_PREFIX}/address-book`) +@ApiTags(API_TAG) +export class AddressBookController { + constructor(private addressBookService: AddressBookService) {} + + @Post() + @ApiOperation({ + summary: 'Registers an account in the address book entity.' + }) + @ApiHeader({ + name: REQUEST_HEADER_ORG_ID + }) + @ApiResponse({ + status: HttpStatus.CREATED, + type: CreateAddressBookAccountResponseDto + }) + async registerAccount( + @OrgId() orgId: string, + @Body() body: CreateAddressBookAccountRequestDto + ): Promise { + const { account } = body.request + + await this.addressBookService.create(orgId, account) + + return new CreateAddressBookAccountResponseDto({ account }) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/controller/credential.controller.ts b/apps/orchestration/src/store/entity/http/rest/controller/credential.controller.ts new file mode 100644 index 000000000..364fe8ade --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/controller/credential.controller.ts @@ -0,0 +1,31 @@ +import { Body, Controller, HttpStatus, Post } from '@nestjs/common' +import { ApiHeader, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' +import { REQUEST_HEADER_ORG_ID } from '../../../../../orchestration.constant' +import { OrgId } from '../../../../../shared/decorator/org-id.decorator' +import { CredentialService } from '../../../core/service/credential.service' +import { API_PREFIX, API_TAG } from '../../../entity-store.constant' +import { CreateCredentialRequestDto } from '../dto/create-credential-request.dto' +import { CreateCredentialResponseDto } from '../dto/create-credential-response.dto' + +@Controller(`${API_PREFIX}/credentials`) +@ApiTags(API_TAG) +export class CredentialController { + constructor(private credentialService: CredentialService) {} + + @Post() + @ApiOperation({ + summary: 'Registers a new user credential.' + }) + @ApiHeader({ + name: REQUEST_HEADER_ORG_ID + }) + @ApiResponse({ + status: HttpStatus.CREATED, + type: CreateCredentialResponseDto + }) + async create(@OrgId() orgId: string, @Body() body: CreateCredentialRequestDto): Promise { + const credential = await this.credentialService.create(orgId, body) + + return new CreateCredentialResponseDto({ credential }) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/controller/entity.controller.ts b/apps/orchestration/src/store/entity/http/rest/controller/entity.controller.ts new file mode 100644 index 000000000..caf8194d4 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/controller/entity.controller.ts @@ -0,0 +1,30 @@ +import { Controller, Get, HttpStatus } from '@nestjs/common' +import { ApiHeader, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' +import { REQUEST_HEADER_ORG_ID } from '../../../../../orchestration.constant' +import { OrgId } from '../../../../../shared/decorator/org-id.decorator' +import { EntityService } from '../../../core/service/entity.service' +import { API_PREFIX, API_TAG } from '../../../entity-store.constant' +import { EntitiesDto } from '../dto/entities.dto' + +@Controller(`${API_PREFIX}/entities`) +@ApiTags(API_TAG) +export class EntityController { + constructor(private entityService: EntityService) {} + + @Get() + @ApiOperation({ + summary: "Returns the organization's entities" + }) + @ApiHeader({ + name: REQUEST_HEADER_ORG_ID + }) + @ApiResponse({ + status: HttpStatus.OK, + type: EntitiesDto + }) + async getEntities(@OrgId() orgId: string): Promise { + const entities = await this.entityService.getEntities(orgId) + + return new EntitiesDto(entities) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/controller/organization.controller.ts b/apps/orchestration/src/store/entity/http/rest/controller/organization.controller.ts new file mode 100644 index 000000000..5e72763d0 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/controller/organization.controller.ts @@ -0,0 +1,33 @@ +import { Body, Controller, HttpStatus, Post } from '@nestjs/common' +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' +import { OrganizationService } from '../../../core/service/organization.service' +import { API_PREFIX, API_TAG } from '../../../entity-store.constant' +import { CreateOrganizationRequestDto } from '../dto/create-organization-request.dto' +import { CreateOrganizationResponseDto } from '../dto/create-organization-response.dto' + +@Controller(`${API_PREFIX}/organizations`) +@ApiTags(API_TAG) +export class OrganizationController { + constructor(private orgService: OrganizationService) {} + + @Post() + @ApiOperation({ + summary: 'Creates a new organization and root user.' + }) + @ApiResponse({ + status: HttpStatus.CREATED, + type: CreateOrganizationResponseDto + }) + async create(@Body() body: CreateOrganizationRequestDto): Promise { + const { organization, rootCredential, rootUser } = await this.orgService.create({ + uid: body.request.organization.uid, + rootCredential: body.request.organization.credential + }) + + return new CreateOrganizationResponseDto({ + organization, + rootCredential, + rootUser + }) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/controller/token.controller.ts b/apps/orchestration/src/store/entity/http/rest/controller/token.controller.ts new file mode 100644 index 000000000..159767703 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/controller/token.controller.ts @@ -0,0 +1,31 @@ +import { Body, Controller, HttpStatus, Post } from '@nestjs/common' +import { ApiHeader, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' +import { REQUEST_HEADER_ORG_ID } from '../../../../../orchestration.constant' +import { OrgId } from '../../../../../shared/decorator/org-id.decorator' +import { TokenService } from '../../../core/service/token.service' +import { API_PREFIX, API_TAG } from '../../../entity-store.constant' +import { RegisterTokensRequestDto } from '../dto/register-tokens-request.dto' +import { RegisterTokensResponseDto } from '../dto/register-tokens-response.dto' + +@Controller(`${API_PREFIX}/tokens`) +@ApiTags(API_TAG) +export class TokenController { + constructor(private tokenService: TokenService) {} + + @Post() + @ApiOperation({ + summary: 'Registers a token entity.' + }) + @ApiHeader({ + name: REQUEST_HEADER_ORG_ID + }) + @ApiResponse({ + status: HttpStatus.CREATED, + type: RegisterTokensResponseDto + }) + async register(@OrgId() orgId: string, @Body() body: RegisterTokensRequestDto): Promise { + const tokens = await this.tokenService.register(orgId, body) + + return new RegisterTokensResponseDto({ tokens }) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/controller/user-group.controller.ts b/apps/orchestration/src/store/entity/http/rest/controller/user-group.controller.ts new file mode 100644 index 000000000..771cba295 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/controller/user-group.controller.ts @@ -0,0 +1,35 @@ +import { Body, Controller, HttpStatus, Post } from '@nestjs/common' +import { ApiHeader, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' +import { REQUEST_HEADER_ORG_ID } from '../../../../../orchestration.constant' +import { OrgId } from '../../../../../shared/decorator/org-id.decorator' +import { UserGroupService } from '../../../core/service/user-group.service' +import { API_PREFIX, API_TAG } from '../../../entity-store.constant' +import { AssignUserGroupRequestDto } from '../dto/assign-user-group-request.dto' +import { AssignUserGroupResponseDto } from '../dto/assign-user-group-response.dto' + +@Controller(`${API_PREFIX}/user-groups`) +@ApiTags(API_TAG) +export class UserGroupController { + constructor(private userGroupService: UserGroupService) {} + + @Post() + @ApiOperation({ + summary: "Assigns a user to a group. If the group doesn't exist, creates it first." + }) + @ApiHeader({ + name: REQUEST_HEADER_ORG_ID + }) + @ApiResponse({ + status: HttpStatus.CREATED, + type: AssignUserGroupResponseDto + }) + async assign(@OrgId() orgId: string, @Body() body: AssignUserGroupRequestDto): Promise { + const { userId, groupId } = body.request.data + + await this.userGroupService.assign(orgId, userId, groupId) + + return new AssignUserGroupResponseDto({ + data: { userId, groupId } + }) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/controller/user-wallet.controller.ts b/apps/orchestration/src/store/entity/http/rest/controller/user-wallet.controller.ts new file mode 100644 index 000000000..3b4f56be2 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/controller/user-wallet.controller.ts @@ -0,0 +1,33 @@ +import { Body, Controller, HttpStatus, Post } from '@nestjs/common' +import { ApiHeader, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' +import { REQUEST_HEADER_ORG_ID } from '../../../../../orchestration.constant' +import { OrgId } from '../../../../../shared/decorator/org-id.decorator' +import { UserService } from '../../../core/service/user.service' +import { API_PREFIX, API_TAG } from '../../../entity-store.constant' +import { AssignUserWalletRequestDto } from '../dto/assign-user-wallet-request.dto' +import { AssignUserWalletResponseDto } from '../dto/assign-user-wallet-response.dto' + +@Controller(`${API_PREFIX}/user-wallets`) +@ApiTags(API_TAG) +export class UserWalletController { + constructor(private userService: UserService) {} + + @Post() + @ApiOperation({ + summary: 'Assigns a wallet to a user.' + }) + @ApiHeader({ + name: REQUEST_HEADER_ORG_ID + }) + @ApiResponse({ + status: HttpStatus.CREATED, + type: AssignUserWalletResponseDto + }) + async assign(@OrgId() orgId: string, @Body() body: AssignUserWalletRequestDto) { + const { data } = body.request + + await this.userService.assignWallet(orgId, body.request.data) + + return new AssignUserWalletResponseDto({ data }) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/controller/user.controller.ts b/apps/orchestration/src/store/entity/http/rest/controller/user.controller.ts new file mode 100644 index 000000000..13d6b196b --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/controller/user.controller.ts @@ -0,0 +1,58 @@ +import { Body, Controller, HttpStatus, Patch, Post } from '@nestjs/common' +import { ApiHeader, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' +import { REQUEST_HEADER_ORG_ID } from '../../../../../orchestration.constant' +import { OrgId } from '../../../../../shared/decorator/org-id.decorator' +import { UserService } from '../../../core/service/user.service' +import { API_PREFIX, API_TAG } from '../../../entity-store.constant' +import { CreateUserRequestDto } from '../dto/create-user-request.dto' +import { CreateUserResponseDto } from '../dto/create-user-response.dto' +import { UpdateUserRequestDto } from '../dto/update-user-request.dto' +import { UpdateUserResponseDto } from '../dto/update-user-response.dto' + +@Controller(`${API_PREFIX}/users`) +@ApiTags(API_TAG) +export class UserController { + constructor(private userService: UserService) {} + + @Post() + @ApiOperation({ + summary: 'Creates a new user entity' + }) + @ApiHeader({ + name: REQUEST_HEADER_ORG_ID + }) + @ApiResponse({ + status: HttpStatus.CREATED, + type: CreateUserResponseDto + }) + async create(@OrgId() orgId: string, @Body() body: CreateUserRequestDto): Promise { + const { uid, role, credential } = body.request.user + + await this.userService.create(orgId, { uid, role }, credential) + + return new CreateUserResponseDto({ + user: { uid, role } + }) + } + + @Patch('/:uid') + @ApiOperation({ + summary: 'Updates an existing user' + }) + @ApiHeader({ + name: REQUEST_HEADER_ORG_ID + }) + @ApiResponse({ + status: HttpStatus.OK, + type: UpdateUserResponseDto + }) + async update(@OrgId() orgId: string, @Body() body: UpdateUserRequestDto): Promise { + const { uid, role } = body.request.user + + await this.userService.grantRole(uid, role) + + return new UpdateUserResponseDto({ + user: { uid, role } + }) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/controller/wallet-group.controller.ts b/apps/orchestration/src/store/entity/http/rest/controller/wallet-group.controller.ts new file mode 100644 index 000000000..51fc4f329 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/controller/wallet-group.controller.ts @@ -0,0 +1,38 @@ +import { Body, Controller, HttpStatus, Post } from '@nestjs/common' +import { ApiHeader, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' +import { REQUEST_HEADER_ORG_ID } from '../../../../../orchestration.constant' +import { OrgId } from '../../../../../shared/decorator/org-id.decorator' +import { WalletService } from '../../../core/service/wallet.service' +import { API_PREFIX, API_TAG } from '../../../entity-store.constant' +import { AssignWalletGroupRequestDto } from '../dto/assign-wallet-group-request.dto' +import { AssignWalletGroupResponseDto } from '../dto/assign-wallet-group-response.dto' + +@Controller(`${API_PREFIX}/wallet-groups`) +@ApiTags(API_TAG) +export class WalletGroupController { + constructor(private walletService: WalletService) {} + + @Post() + @ApiOperation({ + summary: "Assigns a wallet to a group. If the group doesn't exist, creates it first." + }) + @ApiHeader({ + name: REQUEST_HEADER_ORG_ID + }) + @ApiResponse({ + status: HttpStatus.CREATED, + type: AssignWalletGroupResponseDto + }) + async assign( + @OrgId() orgId: string, + @Body() body: AssignWalletGroupRequestDto + ): Promise { + const membership = await this.walletService.assignGroup( + orgId, + body.request.data.walletId, + body.request.data.groupId + ) + + return new AssignWalletGroupResponseDto({ data: membership }) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/controller/wallet.controller.ts b/apps/orchestration/src/store/entity/http/rest/controller/wallet.controller.ts new file mode 100644 index 000000000..7698e0c58 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/controller/wallet.controller.ts @@ -0,0 +1,31 @@ +import { Body, Controller, HttpStatus, Post } from '@nestjs/common' +import { ApiHeader, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' +import { REQUEST_HEADER_ORG_ID } from '../../../../../orchestration.constant' +import { OrgId } from '../../../../../shared/decorator/org-id.decorator' +import { WalletService } from '../../../core/service/wallet.service' +import { API_PREFIX, API_TAG } from '../../../entity-store.constant' +import { RegisterWalletRequestDto } from '../dto/register-wallet-request.dto' +import { RegisterWalletResponseDto } from '../dto/register-wallet-response.dto' + +@Controller(`${API_PREFIX}/wallets`) +@ApiTags(API_TAG) +export class WalletController { + constructor(private walletService: WalletService) {} + + @Post() + @ApiOperation({ + summary: 'Registers wallet as an entity.' + }) + @ApiHeader({ + name: REQUEST_HEADER_ORG_ID + }) + @ApiResponse({ + status: HttpStatus.CREATED, + type: RegisterWalletResponseDto + }) + async register(@OrgId() orgId: string, @Body() body: RegisterWalletRequestDto): Promise { + const wallet = await this.walletService.create(orgId, body.request.wallet) + + return new RegisterWalletResponseDto({ wallet }) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/address-book-account.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/address-book-account.dto.ts new file mode 100644 index 000000000..ac7ac8e29 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/address-book-account.dto.ts @@ -0,0 +1,23 @@ +import { AccountClassification, Address } from '@narval/authz-shared' +import { ApiProperty } from '@nestjs/swagger' +import { IsEnum, IsEthereumAddress, IsNotEmpty, IsNumber, IsString } from 'class-validator' + +export class AddressBookAccountDto { + @IsString() + @IsNotEmpty() + uid: string + + @IsEnum(AccountClassification) + @ApiProperty({ enum: AccountClassification }) + classification: AccountClassification + + @IsEthereumAddress() + @ApiProperty({ + format: 'address', + type: String + }) + address: Address + + @IsNumber() + chainId: number +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/assign-user-group-request.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/assign-user-group-request.dto.ts new file mode 100644 index 000000000..71a7c25d3 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/assign-user-group-request.dto.ts @@ -0,0 +1,28 @@ +import { Action, BaseActionDto, BaseActionRequestDto } from '@narval/authz-shared' +import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { IsDefined, Matches, ValidateNested } from 'class-validator' +import { UserGroupMembershipDto } from './user-group-membership.dto' + +class AssignUserGroupActionDto extends BaseActionDto { + @Matches(Action.ASSIGN_USER_GROUP) + @ApiProperty({ + enum: [Action.ASSIGN_USER_GROUP], + default: Action.ASSIGN_USER_GROUP + }) + action: typeof Action.ASSIGN_USER_GROUP + + @IsDefined() + @Type(() => UserGroupMembershipDto) + @ValidateNested() + @ApiProperty() + data: UserGroupMembershipDto +} + +export class AssignUserGroupRequestDto extends BaseActionRequestDto { + @IsDefined() + @Type(() => AssignUserGroupActionDto) + @ValidateNested() + @ApiProperty() + request: AssignUserGroupActionDto +} diff --git a/apps/authz/src/app/http/rest/dto/assign-user-group-response.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/assign-user-group-response.dto.ts similarity index 63% rename from apps/authz/src/app/http/rest/dto/assign-user-group-response.dto.ts rename to apps/orchestration/src/store/entity/http/rest/dto/assign-user-group-response.dto.ts index 55191078a..f6ba30084 100644 --- a/apps/authz/src/app/http/rest/dto/assign-user-group-response.dto.ts +++ b/apps/orchestration/src/store/entity/http/rest/dto/assign-user-group-response.dto.ts @@ -1,15 +1,16 @@ -import { UserGroupMembership } from '@narval/authz-shared' import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' import { IsDefined, ValidateNested } from 'class-validator' import { UserGroupMembershipDto } from './user-group-membership.dto' export class AssignUserGroupResponseDto { - constructor(userGroup: UserGroupMembership) { - this.data = new UserGroupMembershipDto(userGroup) - } - @IsDefined() + @Type(() => UserGroupMembershipDto) @ValidateNested() @ApiProperty() data: UserGroupMembershipDto + + constructor(partial: Partial) { + Object.assign(this, partial) + } } diff --git a/apps/orchestration/src/store/entity/http/rest/dto/assign-user-wallet-request.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/assign-user-wallet-request.dto.ts new file mode 100644 index 000000000..13a3c3318 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/assign-user-wallet-request.dto.ts @@ -0,0 +1,28 @@ +import { Action, BaseActionDto, BaseActionRequestDto } from '@narval/authz-shared' +import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { IsDefined, Matches, ValidateNested } from 'class-validator' +import { UserWalletDto } from './user-wallet.dto' + +class AssignUserWalletActionDto extends BaseActionDto { + @Matches(Action.ASSIGN_USER_WALLET) + @ApiProperty({ + enum: [Action.ASSIGN_USER_WALLET], + default: Action.ASSIGN_USER_WALLET + }) + action: typeof Action.ASSIGN_USER_WALLET + + @IsDefined() + @Type(() => UserWalletDto) + @ValidateNested() + @ApiProperty() + data: UserWalletDto +} + +export class AssignUserWalletRequestDto extends BaseActionRequestDto { + @IsDefined() + @Type(() => AssignUserWalletActionDto) + @ValidateNested() + @ApiProperty() + request: AssignUserWalletActionDto +} diff --git a/apps/authz/src/app/http/rest/dto/assign-user-wallet-response.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/assign-user-wallet-response.dto.ts similarity index 62% rename from apps/authz/src/app/http/rest/dto/assign-user-wallet-response.dto.ts rename to apps/orchestration/src/store/entity/http/rest/dto/assign-user-wallet-response.dto.ts index 134296da3..8843250d3 100644 --- a/apps/authz/src/app/http/rest/dto/assign-user-wallet-response.dto.ts +++ b/apps/orchestration/src/store/entity/http/rest/dto/assign-user-wallet-response.dto.ts @@ -1,15 +1,16 @@ -import { UserWallet } from '@narval/authz-shared' import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' import { IsDefined, ValidateNested } from 'class-validator' import { UserWalletDto } from './user-wallet.dto' export class AssignUserWalletResponseDto { - constructor(userWallet: UserWallet) { - this.data = new UserWalletDto(userWallet) - } - @IsDefined() + @Type(() => UserWalletDto) @ValidateNested() @ApiProperty() data: UserWalletDto + + constructor(partial: Partial) { + Object.assign(this, partial) + } } diff --git a/apps/orchestration/src/store/entity/http/rest/dto/assign-wallet-group-request.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/assign-wallet-group-request.dto.ts new file mode 100644 index 000000000..4ce28af08 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/assign-wallet-group-request.dto.ts @@ -0,0 +1,28 @@ +import { Action, BaseActionDto, BaseActionRequestDto } from '@narval/authz-shared' +import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { IsDefined, Matches, ValidateNested } from 'class-validator' +import { WalletGroupMembershipDto } from './wallet-group-membership.dto' + +class AssignWalletGroupActionDto extends BaseActionDto { + @Matches(Action.ASSIGN_WALLET_GROUP) + @ApiProperty({ + enum: [Action.ASSIGN_WALLET_GROUP], + default: Action.ASSIGN_WALLET_GROUP + }) + action: typeof Action.ASSIGN_WALLET_GROUP + + @IsDefined() + @Type(() => WalletGroupMembershipDto) + @ValidateNested() + @ApiProperty() + data: WalletGroupMembershipDto +} + +export class AssignWalletGroupRequestDto extends BaseActionRequestDto { + @IsDefined() + @Type(() => AssignWalletGroupActionDto) + @ValidateNested() + @ApiProperty() + request: AssignWalletGroupActionDto +} diff --git a/apps/authz/src/app/http/rest/dto/assign-wallet-group-response.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/assign-wallet-group-response.dto.ts similarity index 63% rename from apps/authz/src/app/http/rest/dto/assign-wallet-group-response.dto.ts rename to apps/orchestration/src/store/entity/http/rest/dto/assign-wallet-group-response.dto.ts index 6b91e564a..afbcb33ae 100644 --- a/apps/authz/src/app/http/rest/dto/assign-wallet-group-response.dto.ts +++ b/apps/orchestration/src/store/entity/http/rest/dto/assign-wallet-group-response.dto.ts @@ -1,15 +1,16 @@ -import { WalletGroupMembership } from '@narval/authz-shared' import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' import { IsDefined, ValidateNested } from 'class-validator' import { WalletGroupMembershipDto } from './wallet-group-membership.dto' export class AssignWalletGroupResponseDto { - constructor(walletGroup: WalletGroupMembership) { - this.data = new WalletGroupMembershipDto(walletGroup) - } - @IsDefined() + @Type(() => WalletGroupMembershipDto) @ValidateNested() @ApiProperty() data: WalletGroupMembershipDto + + constructor(partial: Partial) { + Object.assign(this, partial) + } } diff --git a/apps/orchestration/src/store/entity/http/rest/dto/auth-credential.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/auth-credential.dto.ts new file mode 100644 index 000000000..6b3d76e6e --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/auth-credential.dto.ts @@ -0,0 +1,24 @@ +import { Alg } from '@narval/authz-shared' +import { ApiProperty } from '@nestjs/swagger' +import { IsEnum, IsNotEmpty, IsString } from 'class-validator' + +export class AuthCredentialDto { + @IsString() + @IsNotEmpty() + @ApiProperty() + uid: string + + @IsString() + @IsNotEmpty() + @ApiProperty() + pubKey: string + + @IsEnum(Alg) + @ApiProperty({ enum: Alg }) + alg: Alg + + @IsString() + @IsNotEmpty() + @ApiProperty() + userId: string +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/create-address-book-request.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/create-address-book-request.dto.ts new file mode 100644 index 000000000..3c5e3fa8e --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/create-address-book-request.dto.ts @@ -0,0 +1,32 @@ +import { Action, BaseActionDto, BaseActionRequestDto } from '@narval/authz-shared' +import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { IsDefined, Matches, ValidateNested } from 'class-validator' +import { AddressBookAccountDto } from './address-book-account.dto' + +class CreateAddressBookAccountActionDto extends BaseActionDto { + @Matches(Action.CREATE_ADDRESS_BOOK_ACCOUNT) + @ApiProperty({ + enum: [Action.CREATE_ADDRESS_BOOK_ACCOUNT], + default: Action.CREATE_ADDRESS_BOOK_ACCOUNT + }) + action: typeof Action.CREATE_ADDRESS_BOOK_ACCOUNT + + @IsDefined() + @Type(() => AddressBookAccountDto) + @ValidateNested() + @ApiProperty({ + type: AddressBookAccountDto + }) + account: AddressBookAccountDto +} + +export class CreateAddressBookAccountRequestDto extends BaseActionRequestDto { + @IsDefined() + @Type(() => CreateAddressBookAccountActionDto) + @ValidateNested() + @ApiProperty({ + type: CreateAddressBookAccountActionDto + }) + request: CreateAddressBookAccountActionDto +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/create-address-book-response.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/create-address-book-response.dto.ts new file mode 100644 index 000000000..46b4f663d --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/create-address-book-response.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { IsDefined, ValidateNested } from 'class-validator' +import { AddressBookAccountDto } from './address-book-account.dto' + +export class CreateAddressBookAccountResponseDto { + @IsDefined() + @Type(() => AddressBookAccountDto) + @ValidateNested() + @ApiProperty() + account: AddressBookAccountDto + + constructor(partial: Partial) { + Object.assign(this, partial) + } +} diff --git a/apps/authz/src/app/http/rest/dto/create-credential-request.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/create-credential-request.dto.ts similarity index 51% rename from apps/authz/src/app/http/rest/dto/create-credential-request.dto.ts rename to apps/orchestration/src/store/entity/http/rest/dto/create-credential-request.dto.ts index 7ea96ecb0..c77411aed 100644 --- a/apps/authz/src/app/http/rest/dto/create-credential-request.dto.ts +++ b/apps/orchestration/src/store/entity/http/rest/dto/create-credential-request.dto.ts @@ -1,27 +1,27 @@ -import { Action } from '@narval/authz-shared' +import { Action, BaseActionDto, BaseActionRequestDto } from '@narval/authz-shared' import { ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsIn, ValidateNested } from 'class-validator' +import { Type } from 'class-transformer' +import { IsDefined, Matches, ValidateNested } from 'class-validator' import { AuthCredentialDto } from './auth-credential.dto' -import { BaseActionDto } from './base-action.dto' -import { BaseAdminRequestPayloadDto } from './base-admin-request-payload.dto' class CreateCredentialActionDto extends BaseActionDto { - @IsIn(Object.values(Action)) - @IsDefined() + @Matches(Action.CREATE_CREDENTIAL) @ApiProperty({ - enum: Object.values(Action), + enum: [Action.CREATE_CREDENTIAL], default: Action.CREATE_CREDENTIAL }) action: typeof Action.CREATE_CREDENTIAL @IsDefined() + @Type(() => AuthCredentialDto) @ValidateNested() @ApiProperty() credential: AuthCredentialDto } -export class CreateCredentialRequestDto extends BaseAdminRequestPayloadDto { +export class CreateCredentialRequestDto extends BaseActionRequestDto { @IsDefined() + @Type(() => CreateCredentialActionDto) @ValidateNested() @ApiProperty() request: CreateCredentialActionDto diff --git a/apps/authz/src/app/http/rest/dto/create-credential-response.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/create-credential-response.dto.ts similarity index 63% rename from apps/authz/src/app/http/rest/dto/create-credential-response.dto.ts rename to apps/orchestration/src/store/entity/http/rest/dto/create-credential-response.dto.ts index 50913a550..df458e864 100644 --- a/apps/authz/src/app/http/rest/dto/create-credential-response.dto.ts +++ b/apps/orchestration/src/store/entity/http/rest/dto/create-credential-response.dto.ts @@ -1,15 +1,16 @@ -import { AuthCredential } from '@narval/authz-shared' import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' import { IsDefined, ValidateNested } from 'class-validator' import { AuthCredentialDto } from './auth-credential.dto' export class CreateCredentialResponseDto { - constructor(authCredential: AuthCredential) { - this.credential = new AuthCredentialDto(authCredential) - } - @IsDefined() + @Type(() => AuthCredentialDto) @ValidateNested() @ApiProperty() credential: AuthCredentialDto + + constructor(partial: Partial) { + Object.assign(this, partial) + } } diff --git a/apps/authz/src/app/http/rest/dto/create-organization-request.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/create-organization-request.dto.ts similarity index 55% rename from apps/authz/src/app/http/rest/dto/create-organization-request.dto.ts rename to apps/orchestration/src/store/entity/http/rest/dto/create-organization-request.dto.ts index bea8a45af..b57da1e68 100644 --- a/apps/authz/src/app/http/rest/dto/create-organization-request.dto.ts +++ b/apps/orchestration/src/store/entity/http/rest/dto/create-organization-request.dto.ts @@ -1,40 +1,40 @@ -import { Action } from '@narval/authz-shared' +import { Action, BaseActionDto, BaseActionRequestDto } from '@narval/authz-shared' import { ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsIn, IsString, ValidateNested } from 'class-validator' +import { Type } from 'class-transformer' +import { IsDefined, IsNotEmpty, IsString, Matches, ValidateNested } from 'class-validator' import { AuthCredentialDto } from './auth-credential.dto' -import { BaseActionDto } from './base-action.dto' -import { BaseAdminRequestPayloadDto } from './base-admin-request-payload.dto' class CreateOrganizationDataDto { @IsString() - @IsDefined() + @IsNotEmpty() @ApiProperty() uid: string - @IsString() @IsDefined() - @ApiProperty() + @Type(() => AuthCredentialDto) @ValidateNested() + @ApiProperty() credential: AuthCredentialDto } class CreateOrganizationActionDto extends BaseActionDto { - @IsIn(Object.values(Action)) - @IsDefined() + @Matches(Action.CREATE_ORGANIZATION) @ApiProperty({ - enum: Object.values(Action), + enum: [Action.CREATE_ORGANIZATION], default: Action.CREATE_ORGANIZATION }) action: typeof Action.CREATE_ORGANIZATION @IsDefined() + @Type(() => CreateOrganizationDataDto) @ValidateNested() @ApiProperty() organization: CreateOrganizationDataDto } -export class CreateOrganizationRequestDto extends BaseAdminRequestPayloadDto { +export class CreateOrganizationRequestDto extends BaseActionRequestDto { @IsDefined() + @Type(() => CreateOrganizationActionDto) @ValidateNested() @ApiProperty() request: CreateOrganizationActionDto diff --git a/apps/orchestration/src/store/entity/http/rest/dto/create-organization-response.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/create-organization-response.dto.ts new file mode 100644 index 000000000..2833e73a0 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/create-organization-response.dto.ts @@ -0,0 +1,36 @@ +import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { IsDefined, IsNotEmpty, IsString, ValidateNested } from 'class-validator' +import { AuthCredentialDto } from './auth-credential.dto' +import { UserDto } from './user.dto' + +class OrganizationDataDto { + @IsString() + @IsNotEmpty() + @ApiProperty() + uid: string +} + +export class CreateOrganizationResponseDto { + @IsDefined() + @Type(() => OrganizationDataDto) + @ValidateNested() + @ApiProperty() + organization: OrganizationDataDto + + @IsDefined() + @Type(() => AuthCredentialDto) + @ValidateNested() + @ApiProperty() + rootCredential: AuthCredentialDto + + @IsDefined() + @Type(() => UserDto) + @ValidateNested() + @ApiProperty() + rootUser: UserDto + + constructor(partial: Partial) { + Object.assign(this, partial) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/create-user-request.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/create-user-request.dto.ts new file mode 100644 index 000000000..9e54c3ca7 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/create-user-request.dto.ts @@ -0,0 +1,45 @@ +import { Action, BaseActionDto, BaseActionRequestDto, UserRole } from '@narval/authz-shared' +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { IsDefined, IsEnum, IsNotEmpty, IsOptional, IsString, Matches, ValidateNested } from 'class-validator' +import { AuthCredentialDto } from './auth-credential.dto' + +class CreateUserDataDto { + @IsString() + @IsNotEmpty() + @ApiProperty() + uid: string + + @IsEnum(UserRole) + @ApiProperty({ enum: UserRole }) + role: UserRole + + @IsOptional() + @Type(() => AuthCredentialDto) + @ValidateNested() + @ApiPropertyOptional() + credential?: AuthCredentialDto +} + +class CreateUserActionDto extends BaseActionDto { + @Matches(Action.CREATE_USER) + @ApiProperty({ + enum: [Action.CREATE_USER], + default: Action.CREATE_USER + }) + action: typeof Action.CREATE_USER + + @IsDefined() + @Type(() => CreateUserDataDto) + @ValidateNested() + @ApiProperty() + user: CreateUserDataDto +} + +export class CreateUserRequestDto extends BaseActionRequestDto { + @IsDefined() + @Type(() => CreateUserActionDto) + @ValidateNested() + @ApiProperty() + request: CreateUserActionDto +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/create-user-response.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/create-user-response.dto.ts new file mode 100644 index 000000000..0cdd74087 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/create-user-response.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { IsDefined, ValidateNested } from 'class-validator' +import { UserDto } from './user.dto' + +export class CreateUserResponseDto { + @IsDefined() + @Type(() => UserDto) + @ValidateNested() + @ApiProperty() + user: UserDto + + constructor(partial: Partial) { + Object.assign(this, partial) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/entities.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/entities.dto.ts new file mode 100644 index 000000000..ca0a9aa94 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/entities.dto.ts @@ -0,0 +1,43 @@ +import { Type } from 'class-transformer' +import { ValidateNested } from 'class-validator' +import { AddressBookAccountDto } from './address-book-account.dto' +import { AuthCredentialDto } from './auth-credential.dto' +import { TokenDto } from './token.dto' +import { UserWalletDto } from './user-wallet.dto' +import { UserDto } from './user.dto' +import { WalletGroupDto } from './wallet-group.dto' +import { WalletDto } from './wallet.dto' + +export class EntitiesDto { + @Type(() => AddressBookAccountDto) + @ValidateNested({ each: true }) + addressBook: AddressBookAccountDto[] + + @Type(() => AuthCredentialDto) + @ValidateNested({ each: true }) + credentials: AuthCredentialDto[] + + @Type(() => TokenDto) + @ValidateNested({ each: true }) + tokens: TokenDto[] + + @Type(() => UserDto) + @ValidateNested({ each: true }) + users: UserDto[] + + @Type(() => UserWalletDto) + @ValidateNested({ each: true }) + userWallets: UserWalletDto[] + + @Type(() => WalletDto) + @ValidateNested({ each: true }) + wallets: WalletDto[] + + @Type(() => WalletDto) + @ValidateNested({ each: true }) + walletGroups: WalletGroupDto[] + + constructor(partial: Partial) { + Object.assign(this, partial) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/register-tokens-request.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/register-tokens-request.dto.ts new file mode 100644 index 000000000..955b25ef9 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/register-tokens-request.dto.ts @@ -0,0 +1,28 @@ +import { Action, BaseActionDto, BaseActionRequestDto } from '@narval/authz-shared' +import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { ArrayNotEmpty, IsDefined, Matches, ValidateNested } from 'class-validator' +import { TokenDto } from './token.dto' + +class RegisterTokensActionDto extends BaseActionDto { + @Matches(Action.REGISTER_TOKENS) + @ApiProperty({ + enum: [Action.REGISTER_TOKENS], + default: Action.REGISTER_TOKENS + }) + action: typeof Action.REGISTER_TOKENS + + @ArrayNotEmpty() + @Type(() => TokenDto) + @ValidateNested({ each: true }) + @ApiProperty({ type: [TokenDto] }) + tokens: TokenDto[] +} + +export class RegisterTokensRequestDto extends BaseActionRequestDto { + @IsDefined() + @Type(() => RegisterTokensActionDto) + @ValidateNested() + @ApiProperty() + request: RegisterTokensActionDto +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/register-tokens-response.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/register-tokens-response.dto.ts new file mode 100644 index 000000000..824af5d18 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/register-tokens-response.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { ArrayNotEmpty, ValidateNested } from 'class-validator' +import { TokenDto } from './token.dto' + +export class RegisterTokensResponseDto { + @ArrayNotEmpty() + @Type(() => TokenDto) + @ValidateNested({ each: true }) + @ApiProperty({ type: [TokenDto] }) + tokens: TokenDto[] + + constructor(partial: Partial) { + Object.assign(this, partial) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/register-wallet-request.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/register-wallet-request.dto.ts new file mode 100644 index 000000000..f002d643f --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/register-wallet-request.dto.ts @@ -0,0 +1,28 @@ +import { Action, BaseActionDto, BaseActionRequestDto } from '@narval/authz-shared' +import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { IsDefined, Matches, ValidateNested } from 'class-validator' +import { WalletDto } from './wallet.dto' + +class RegisterWalletActionDto extends BaseActionDto { + @Matches(Action.REGISTER_WALLET) + @ApiProperty({ + enum: [Action.REGISTER_WALLET], + default: Action.REGISTER_WALLET + }) + action: typeof Action.REGISTER_WALLET + + @IsDefined() + @Type(() => WalletDto) + @ValidateNested() + @ApiProperty() + wallet: WalletDto +} + +export class RegisterWalletRequestDto extends BaseActionRequestDto { + @IsDefined() + @Type(() => RegisterWalletActionDto) + @ValidateNested() + @ApiProperty() + request: RegisterWalletActionDto +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/register-wallet-response.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/register-wallet-response.dto.ts new file mode 100644 index 000000000..3fcb24cbe --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/register-wallet-response.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { IsDefined, ValidateNested } from 'class-validator' +import { WalletDto } from './wallet.dto' + +export class RegisterWalletResponseDto { + @IsDefined() + @Type(() => WalletDto) + @ValidateNested() + @ApiProperty() + wallet: WalletDto + + constructor(partial: Partial) { + Object.assign(this, partial) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/request-signature.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/request-signature.dto.ts new file mode 100644 index 000000000..b72e77678 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/request-signature.dto.ts @@ -0,0 +1,19 @@ +import { Alg } from '@narval/authz-shared' +import { ApiProperty } from '@nestjs/swagger' +import { IsEnum, IsNotEmpty, IsString } from 'class-validator' + +export class RequestSignatureDto { + @IsString() + @IsNotEmpty() + @ApiProperty() + sig: string + + @IsEnum(Alg) + @ApiProperty({ enum: Alg }) + alg: Alg + + @IsString() + @IsNotEmpty() + @ApiProperty() + pubKey: string +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/token.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/token.dto.ts new file mode 100644 index 000000000..af8365a72 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/token.dto.ts @@ -0,0 +1,33 @@ +import { Address } from '@narval/authz-shared' +import { ApiProperty } from '@nestjs/swagger' +import { IsDefined, IsEthereumAddress, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator' + +export class TokenDto { + @IsString() + @IsNotEmpty() + uid: string + + @IsEthereumAddress() + @ApiProperty({ + format: 'address', + type: String + }) + address: Address + + @IsNumber() + @IsDefined() + @Min(1) + chainId: number + + @IsString() + @IsNotEmpty() + symbol: string + + @IsNumber() + @IsDefined() + decimals: number + + constructor(partial: Partial) { + Object.assign(this, partial) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/update-user-request.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/update-user-request.dto.ts new file mode 100644 index 000000000..800e6de66 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/update-user-request.dto.ts @@ -0,0 +1,38 @@ +import { Action, BaseActionDto, BaseActionRequestDto, UserRole } from '@narval/authz-shared' +import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { IsDefined, IsEnum, IsNotEmpty, IsString, Matches, ValidateNested } from 'class-validator' + +class UpdateUserDataDto { + @IsString() + @IsNotEmpty() + @ApiProperty() + uid: string + + @IsEnum(UserRole) + @ApiProperty({ enum: UserRole }) + role: UserRole +} + +class UpdateUserActionDto extends BaseActionDto { + @Matches(Action.UPDATE_USER) + @ApiProperty({ + enum: [Action.UPDATE_USER], + default: Action.UPDATE_USER + }) + action: typeof Action.UPDATE_USER + + @IsDefined() + @Type(() => UpdateUserDataDto) + @ValidateNested() + @ApiProperty() + user: UpdateUserDataDto +} + +export class UpdateUserRequestDto extends BaseActionRequestDto { + @IsDefined() + @Type(() => UpdateUserActionDto) + @ValidateNested() + @ApiProperty() + request: UpdateUserActionDto +} diff --git a/apps/authz/src/app/http/rest/dto/update-user-response.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/update-user-response.dto.ts similarity index 53% rename from apps/authz/src/app/http/rest/dto/update-user-response.dto.ts rename to apps/orchestration/src/store/entity/http/rest/dto/update-user-response.dto.ts index aeb4559c6..c31a6914f 100644 --- a/apps/authz/src/app/http/rest/dto/update-user-response.dto.ts +++ b/apps/orchestration/src/store/entity/http/rest/dto/update-user-response.dto.ts @@ -1,15 +1,16 @@ import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' import { IsDefined, ValidateNested } from 'class-validator' -import { User } from '../../../../shared/types/entities.types' -import { UserDto } from './user-dto' +import { UserDto } from './user.dto' export class UpdateUserResponseDto { - constructor(user: User) { - this.user = new UserDto(user) - } - @IsDefined() + @Type(() => UserDto) @ValidateNested() @ApiProperty() user: UserDto + + constructor(partial: Partial) { + Object.assign(this, partial) + } } diff --git a/apps/orchestration/src/store/entity/http/rest/dto/user-group-membership.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/user-group-membership.dto.ts new file mode 100644 index 000000000..2de46ce54 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/user-group-membership.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger' +import { IsNotEmpty, IsString } from 'class-validator' + +export class UserGroupMembershipDto { + @IsString() + @IsNotEmpty() + @ApiProperty() + userId: string + + @IsString() + @IsNotEmpty() + @ApiProperty() + groupId: string + + constructor(partial: Partial) { + Object.assign(this, partial) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/user-wallet.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/user-wallet.dto.ts new file mode 100644 index 000000000..192ece581 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/user-wallet.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger' +import { IsNotEmpty, IsString } from 'class-validator' + +export class UserWalletDto { + @IsString() + @IsNotEmpty() + @ApiProperty() + walletId: string + + @IsString() + @IsNotEmpty() + @ApiProperty() + userId: string + + constructor(partial: Partial) { + Object.assign(this, partial) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/user.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/user.dto.ts new file mode 100644 index 000000000..467321218 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/user.dto.ts @@ -0,0 +1,18 @@ +import { UserRole } from '@narval/authz-shared' +import { ApiProperty } from '@nestjs/swagger' +import { IsEnum, IsNotEmpty, IsString } from 'class-validator' + +export class UserDto { + @IsString() + @IsNotEmpty() + @ApiProperty() + uid: string + + @IsEnum(UserRole) + @ApiProperty({ enum: UserRole }) + role: UserRole + + constructor(partial: Partial) { + Object.assign(this, partial) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/wallet-group-membership.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/wallet-group-membership.dto.ts new file mode 100644 index 000000000..a3cbdf285 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/wallet-group-membership.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger' +import { IsNotEmpty, IsString } from 'class-validator' + +export class WalletGroupMembershipDto { + @IsString() + @IsNotEmpty() + @ApiProperty() + walletId: string + + @IsString() + @IsNotEmpty() + @ApiProperty() + groupId: string + + constructor(partial: Partial) { + Object.assign(this, partial) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/wallet-group.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/wallet-group.dto.ts new file mode 100644 index 000000000..72db551b6 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/wallet-group.dto.ts @@ -0,0 +1,14 @@ +import { IsArray, IsNotEmpty, IsString } from 'class-validator' + +export class WalletGroupDto { + @IsString() + @IsNotEmpty() + uid: string + + @IsArray() + wallets: string[] + + constructor(partial: Partial) { + Object.assign(this, partial) + } +} diff --git a/apps/orchestration/src/store/entity/http/rest/dto/wallet.dto.ts b/apps/orchestration/src/store/entity/http/rest/dto/wallet.dto.ts new file mode 100644 index 000000000..80f905a77 --- /dev/null +++ b/apps/orchestration/src/store/entity/http/rest/dto/wallet.dto.ts @@ -0,0 +1,30 @@ +import { AccountType, Address } from '@narval/authz-shared' +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' +import { IsEnum, IsEthereumAddress, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator' + +export class WalletDto { + @IsString() + @IsNotEmpty() + @ApiProperty() + uid: string + + @IsEnum(AccountType) + @ApiProperty({ enum: AccountType }) + accountType: AccountType + + @IsEthereumAddress() + @ApiProperty({ + type: String, + format: 'address' + }) + address: Address + + @IsNumber() + @IsOptional() + @ApiPropertyOptional() + chainId?: number + + constructor(partial: Partial) { + Object.assign(this, partial) + } +} diff --git a/apps/orchestration/src/store/entity/persistence/decode.util.ts b/apps/orchestration/src/store/entity/persistence/decode.util.ts new file mode 100644 index 000000000..4bd7d2d68 --- /dev/null +++ b/apps/orchestration/src/store/entity/persistence/decode.util.ts @@ -0,0 +1,14 @@ +export const decodeConstant = ( + response: T, + key: K, + validValues: V[] +): T & Record => { + if (!validValues.includes(response[key] as V)) { + throw new Error(`Invalid value for key ${key as string}: ${response[key]}`) + } + + return { + ...response, + [key]: response[key] as V + } as T & Record +} diff --git a/apps/orchestration/src/store/entity/persistence/entity-store.seed.ts b/apps/orchestration/src/store/entity/persistence/entity-store.seed.ts new file mode 100644 index 000000000..0c9fd9cd5 --- /dev/null +++ b/apps/orchestration/src/store/entity/persistence/entity-store.seed.ts @@ -0,0 +1,72 @@ +import { FIXTURE } from '@narval/authz-shared' +import { Injectable, Logger } from '@nestjs/common' +import { compact } from 'lodash/fp' +import { ORGANIZATION } from 'packages/authz-shared/src/lib/dev.fixture' +import { Seeder } from '../../../shared/module/persistence/persistence.type' +import { AddressBookRepository } from './repository/address-book.repository' +import { CredentialRepository } from './repository/credential.repository' +import { TokenRepository } from './repository/token.repository' +import { UserGroupRepository } from './repository/user-group.repository' +import { UserWalletRepository } from './repository/user-wallet.repository' +import { UserRepository } from './repository/user.repository' +import { WalletGroupRepository } from './repository/wallet-group.repository' +import { WalletRepository } from './repository/wallet.repository' + +@Injectable() +export class EntityStoreSeed implements Seeder { + private logger = new Logger(EntityStoreSeed.name) + + constructor( + private addressBookRepository: AddressBookRepository, + private credentialRepository: CredentialRepository, + private tokenRepository: TokenRepository, + private userGroupRepository: UserGroupRepository, + private userRepository: UserRepository, + private userWalletRepository: UserWalletRepository, + private walletGroupRepository: WalletGroupRepository, + private walletRepository: WalletRepository + ) {} + + async germinate(): Promise { + this.logger.log('Germinating the Entity Store module database') + + await Promise.all( + Object.values(FIXTURE.USER).map((entity) => this.userRepository.create(FIXTURE.ORGANIZATION.uid, entity)) + ) + + await Promise.all( + Object.values(FIXTURE.CREDENTIAL).map((entity) => this.credentialRepository.create(ORGANIZATION.uid, entity)) + ) + + await Promise.all( + Object.values(FIXTURE.WALLET).map((entity) => this.walletRepository.create(ORGANIZATION.uid, entity)) + ) + + await Promise.all( + Object.values(FIXTURE.WALLET_GROUP).map((entity) => this.walletGroupRepository.create(ORGANIZATION.uid, entity)) + ) + + await Promise.all( + Object.values(FIXTURE.USER_GROUP).map((entity) => this.userGroupRepository.create(ORGANIZATION.uid, entity)) + ) + + await Promise.all( + compact( + Object.values(FIXTURE.WALLET).map(({ uid, assignees }) => { + if (assignees?.length) { + return assignees.map((userId) => + this.userWalletRepository.create(ORGANIZATION.uid, { + userId, + walletId: uid + }) + ) + } + }) + ) + ) + + await Promise.all(FIXTURE.ADDRESS_BOOK.map((entity) => this.addressBookRepository.create(ORGANIZATION.uid, entity))) + + await this.tokenRepository.create(ORGANIZATION.uid, Object.values(FIXTURE.TOKEN)) + } +} diff --git a/apps/orchestration/src/store/entity/persistence/repository/address-book.repository.ts b/apps/orchestration/src/store/entity/persistence/repository/address-book.repository.ts new file mode 100644 index 000000000..d0b8eeb76 --- /dev/null +++ b/apps/orchestration/src/store/entity/persistence/repository/address-book.repository.ts @@ -0,0 +1,49 @@ +import { AccountClassification, AddressBookAccountEntity, getAddress } from '@narval/authz-shared' +import { Injectable } from '@nestjs/common' +import { AddressBookAccountEntity as Model } from '@prisma/client/orchestration' +import { PrismaService } from '../../../../shared/module/persistence/service/prisma.service' +import { decodeConstant } from '../decode.util' + +@Injectable() +export class AddressBookRepository { + constructor(private prismaService: PrismaService) {} + + async create(orgId: string, account: AddressBookAccountEntity): Promise { + await this.prismaService.addressBookAccountEntity.create({ + data: { orgId, ...account } + }) + + return account + } + + async findById(uid: string): Promise { + const model = await this.prismaService.addressBookAccountEntity.findUnique({ + where: { uid } + }) + + if (model) { + return this.decode(model) + } + + return null + } + + async findByOrgId(orgId: string): Promise { + const models = await this.prismaService.addressBookAccountEntity.findMany({ where: { orgId } }) + + return models.map(this.decode) + } + + private decode({ uid, address, chainId, classification }: Model): AddressBookAccountEntity { + return decodeConstant( + { + uid, + address: getAddress(address), + chainId, + classification + }, + 'classification', + Object.values(AccountClassification) + ) + } +} diff --git a/apps/orchestration/src/store/entity/persistence/repository/credential.repository.ts b/apps/orchestration/src/store/entity/persistence/repository/credential.repository.ts new file mode 100644 index 000000000..527f03ed1 --- /dev/null +++ b/apps/orchestration/src/store/entity/persistence/repository/credential.repository.ts @@ -0,0 +1,49 @@ +import { Alg, AuthCredential } from '@narval/authz-shared' +import { Injectable } from '@nestjs/common' +import { AuthCredentialEntity as Model } from '@prisma/client/orchestration' +import { omit } from 'lodash/fp' +import { PrismaService } from '../../../../shared/module/persistence/service/prisma.service' +import { decodeConstant } from '../decode.util' + +@Injectable() +export class CredentialRepository { + constructor(private prismaService: PrismaService) {} + + async create(orgId: string, credential: AuthCredential): Promise { + await this.prismaService.authCredentialEntity.create({ + data: { + orgId, + uid: credential.uid, + pubKey: credential.pubKey, + alg: credential.alg, + userId: credential.userId + } + }) + + return credential + } + + async findById(uid: string): Promise { + const model = await this.prismaService.authCredentialEntity.findUnique({ + where: { uid } + }) + + if (model) { + return this.decode(model) + } + + return null + } + + async findByOrgId(orgId: string): Promise { + const models = await this.prismaService.authCredentialEntity.findMany({ + where: { orgId } + }) + + return models.map(this.decode) + } + + private decode(model: Model): AuthCredential { + return decodeConstant(omit('orgId', model), 'alg', Object.values(Alg)) + } +} diff --git a/apps/orchestration/src/store/entity/persistence/repository/organization.repository.ts b/apps/orchestration/src/store/entity/persistence/repository/organization.repository.ts new file mode 100644 index 000000000..2a9af3517 --- /dev/null +++ b/apps/orchestration/src/store/entity/persistence/repository/organization.repository.ts @@ -0,0 +1,20 @@ +import { OrganizationEntity } from '@narval/authz-shared' +import { Injectable } from '@nestjs/common' +import { PrismaService } from '../../../../shared/module/persistence/service/prisma.service' + +@Injectable() +export class OrganizationRepository { + constructor(private prismaService: PrismaService) {} + + async create(uid: string): Promise { + await this.prismaService.organizationEntity.create({ + data: { uid } + }) + + return { uid } + } + + async findById(uid: string): Promise { + return this.prismaService.organizationEntity.findUnique({ where: { uid } }) + } +} diff --git a/apps/orchestration/src/store/entity/persistence/repository/token.repository.ts b/apps/orchestration/src/store/entity/persistence/repository/token.repository.ts new file mode 100644 index 000000000..5ce7b0a7a --- /dev/null +++ b/apps/orchestration/src/store/entity/persistence/repository/token.repository.ts @@ -0,0 +1,29 @@ +import { TokenEntity, getAddress } from '@narval/authz-shared' +import { Injectable } from '@nestjs/common' +import { pick } from 'lodash/fp' +import { PrismaService } from '../../../../shared/module/persistence/service/prisma.service' + +@Injectable() +export class TokenRepository { + constructor(private prismaService: PrismaService) {} + + async create(orgId: string, tokens: TokenEntity[]): Promise { + await this.prismaService.tokenEntity.createMany({ + data: tokens.map((token) => ({ orgId, ...token })), + skipDuplicates: true + }) + + return tokens + } + + async findByOrgId(orgId: string): Promise { + const entities = await this.prismaService.tokenEntity.findMany({ + where: { orgId } + }) + + return entities.map((entity) => ({ + ...pick(['uid', 'address', 'symbol', 'chainId', 'decimals'], entity), + address: getAddress(entity.address) + })) + } +} diff --git a/apps/orchestration/src/store/entity/persistence/repository/user-group.repository.ts b/apps/orchestration/src/store/entity/persistence/repository/user-group.repository.ts new file mode 100644 index 000000000..f55d1e7b6 --- /dev/null +++ b/apps/orchestration/src/store/entity/persistence/repository/user-group.repository.ts @@ -0,0 +1,81 @@ +import { UserGroupEntity } from '@narval/authz-shared' +import { Injectable } from '@nestjs/common' +import { UserGroupEntity as GroupModel, UserGroupMemberEntity as MemberModel } from '@prisma/client/orchestration' +import { map } from 'lodash/fp' +import { PrismaService } from '../../../../shared/module/persistence/service/prisma.service' + +type Model = GroupModel & { + members: MemberModel[] +} + +@Injectable() +export class UserGroupRepository { + constructor(private prismaService: PrismaService) {} + + async create(orgId: string, userGroup: UserGroupEntity): Promise { + await this.prismaService.userGroupEntity.create({ + data: { + orgId, + uid: userGroup.uid + } + }) + + if (userGroup.users.length) { + await this.enroll(userGroup.uid, userGroup.users) + } + + return userGroup + } + + async update(userGroup: UserGroupEntity): Promise { + if (userGroup.users.length) { + await this.enroll(userGroup.uid, userGroup.users) + } + + return userGroup + } + + private async enroll(groupId: string, userIds: string[]): Promise { + const members = userIds.map((userId) => ({ userId, groupId })) + + await this.prismaService.userGroupMemberEntity.createMany({ + data: members, + skipDuplicates: true + }) + + return true + } + + async findById(uid: string): Promise { + const model = await this.prismaService.userGroupEntity.findUnique({ + where: { uid }, + include: { + members: true + } + }) + + if (model) { + return this.decode(model) + } + + return null + } + + async findByOrgId(orgId: string): Promise { + const models = await this.prismaService.userGroupEntity.findMany({ + where: { orgId }, + include: { + members: true + } + }) + + return models.map(this.decode) + } + + private decode(model: Model): UserGroupEntity { + return { + uid: model.uid, + users: map('userId', model.members) + } + } +} diff --git a/apps/orchestration/src/store/entity/persistence/repository/user-wallet.repository.ts b/apps/orchestration/src/store/entity/persistence/repository/user-wallet.repository.ts new file mode 100644 index 000000000..1a55ac843 --- /dev/null +++ b/apps/orchestration/src/store/entity/persistence/repository/user-wallet.repository.ts @@ -0,0 +1,25 @@ +import { UserWalletEntity } from '@narval/authz-shared' +import { Injectable } from '@nestjs/common' +import { PrismaService } from '../../../../shared/module/persistence/service/prisma.service' + +@Injectable() +export class UserWalletRepository { + constructor(private prismaService: PrismaService) {} + + async create(orgId: string, userWallet: UserWalletEntity): Promise { + await this.prismaService.userWalletEntity.create({ + data: { + orgId, + ...userWallet + } + }) + + return userWallet + } + + async findByOrgId(orgId: string): Promise { + return this.prismaService.userWalletEntity.findMany({ + where: { orgId } + }) + } +} diff --git a/apps/orchestration/src/store/entity/persistence/repository/user.repository.ts b/apps/orchestration/src/store/entity/persistence/repository/user.repository.ts new file mode 100644 index 000000000..952b2132d --- /dev/null +++ b/apps/orchestration/src/store/entity/persistence/repository/user.repository.ts @@ -0,0 +1,98 @@ +import { AuthCredential, UserEntity, UserRole } from '@narval/authz-shared' +import { Injectable } from '@nestjs/common' +import { UserEntity as UserModel } from '@prisma/client/orchestration' +import { omit } from 'lodash/fp' +import { SetRequired } from 'type-fest' +import { PrismaService } from '../../../../shared/module/persistence/service/prisma.service' +import { decodeConstant } from '../decode.util' + +@Injectable() +export class UserRepository { + constructor(private prismaService: PrismaService) {} + + async create(orgId: string, user: UserEntity, credential?: AuthCredential): Promise { + const result = await this.prismaService.$transaction(async (tx) => { + const entity: UserEntity = await tx.userEntity + .create({ + data: { + ...user, + orgId + } + }) + .then((d) => decodeConstant(d, 'role', Object.values(UserRole))) + + if (credential) { + await tx.authCredentialEntity.create({ + data: { + orgId, + uid: credential.uid, + pubKey: credential.pubKey, + alg: credential.alg, + userId: user.uid + } + }) + } + + return entity + }) + + return result + } + + async delete(uid: string): Promise { + await this.prismaService.$transaction(async (tx) => { + await tx.userEntity.delete({ + where: { + uid + } + }) + + await tx.authCredentialEntity.deleteMany({ + where: { + userId: uid + } + }) + + await tx.userGroupMemberEntity.deleteMany({ + where: { + userId: uid + } + }) + }) + + return true + } + + async update(user: SetRequired, 'uid'>): Promise { + const entity = await this.prismaService.userEntity.update({ + where: { + uid: user.uid + }, + data: user + }) + + return this.decode(entity) + } + + async findById(uid: string): Promise { + const entity = await this.prismaService.userEntity.findUnique({ + where: { uid } + }) + + if (entity) { + return this.decode(entity) + } + + return null + } + + async findByOrgId(orgId: string): Promise { + const entities = await this.prismaService.userEntity.findMany({ where: { orgId } }) + + return entities.map(this.decode) + } + + private decode(model: UserModel): UserEntity { + return decodeConstant(omit(['orgId'], model), 'role', Object.values(UserRole)) + } +} diff --git a/apps/orchestration/src/store/entity/persistence/repository/wallet-group.repository.ts b/apps/orchestration/src/store/entity/persistence/repository/wallet-group.repository.ts new file mode 100644 index 000000000..e595a2b20 --- /dev/null +++ b/apps/orchestration/src/store/entity/persistence/repository/wallet-group.repository.ts @@ -0,0 +1,90 @@ +import { WalletGroupEntity } from '@narval/authz-shared' +import { Injectable } from '@nestjs/common' +import { WalletGroupEntity as GroupModel, WalletGroupMemberEntity as MemberModel } from '@prisma/client/orchestration' +import { map } from 'lodash/fp' +import { PrismaService } from '../../../../shared/module/persistence/service/prisma.service' + +type Model = GroupModel & { + members: MemberModel[] +} + +@Injectable() +export class WalletGroupRepository { + constructor(private prismaService: PrismaService) {} + + async create(orgId: string, walletGroup: WalletGroupEntity): Promise { + const group = await this.prismaService.walletGroupEntity.findUnique({ + where: { uid: walletGroup.uid } + }) + + if (!group) { + await this.prismaService.walletGroupEntity.create({ + data: { + orgId, + uid: walletGroup.uid + } + }) + } + + if (walletGroup.wallets.length) { + await this.enroll(walletGroup.uid, walletGroup.wallets) + } + + return walletGroup + } + + async update(walletGroup: WalletGroupEntity): Promise { + if (walletGroup.wallets.length) { + await this.enroll(walletGroup.uid, walletGroup.wallets) + } + + return walletGroup + } + + private async enroll(groupId: string, walletIds: string[]): Promise { + const members = walletIds.map((walletId) => ({ + walletId, + groupId + })) + + await this.prismaService.walletGroupMemberEntity.createMany({ + data: members, + skipDuplicates: true + }) + + return true + } + + async findById(uid: string): Promise { + const model = await this.prismaService.walletGroupEntity.findUnique({ + where: { uid }, + include: { + members: true + } + }) + + if (model) { + return this.decode(model) + } + + return null + } + + async findByOrgId(orgId: string): Promise { + const models = await this.prismaService.walletGroupEntity.findMany({ + where: { orgId }, + include: { + members: true + } + }) + + return models.map(this.decode) + } + + private decode(model: Model): WalletGroupEntity { + return { + uid: model.uid, + wallets: map('walletId', model.members) + } + } +} diff --git a/apps/orchestration/src/store/entity/persistence/repository/wallet.repository.ts b/apps/orchestration/src/store/entity/persistence/repository/wallet.repository.ts new file mode 100644 index 000000000..39b336697 --- /dev/null +++ b/apps/orchestration/src/store/entity/persistence/repository/wallet.repository.ts @@ -0,0 +1,55 @@ +import { AccountType, WalletEntity, getAddress } from '@narval/authz-shared' +import { Injectable } from '@nestjs/common' +import { WalletEntity as WalletModel } from '@prisma/client/orchestration' +import { PrismaService } from '../../../../shared/module/persistence/service/prisma.service' +import { decodeConstant } from '../decode.util' + +@Injectable() +export class WalletRepository { + constructor(private prismaService: PrismaService) {} + + async create(orgId: string, wallet: WalletEntity): Promise { + await this.prismaService.walletEntity.create({ + data: { + orgId, + uid: wallet.uid, + address: wallet.address, + accountType: wallet.accountType, + chainId: wallet.chainId + } + }) + + return wallet + } + + async findById(uid: string): Promise { + const model = await this.prismaService.walletEntity.findUnique({ where: { uid } }) + + if (model) { + return this.decode(model) + } + + return null + } + + async findByOrgId(orgId: string): Promise { + const models = await this.prismaService.walletEntity.findMany({ + where: { orgId } + }) + + return models.map(this.decode) + } + + private decode({ uid, address, accountType, chainId }: WalletModel): WalletEntity { + return decodeConstant( + { + uid, + address: getAddress(address), + accountType, + chainId: chainId || undefined + }, + 'accountType', + Object.values(AccountType) + ) + } +} diff --git a/apps/orchestration/src/store/store.module.ts b/apps/orchestration/src/store/store.module.ts new file mode 100644 index 000000000..9d26ffef5 --- /dev/null +++ b/apps/orchestration/src/store/store.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common' +import { EntityStoreModule } from './entity/entity-store.module' + +@Module({ + imports: [EntityStoreModule] +}) +export class StoreModule {} diff --git a/packages/authz-shared/src/index.ts b/packages/authz-shared/src/index.ts index 757ff9c1b..65a21435d 100644 --- a/packages/authz-shared/src/index.ts +++ b/packages/authz-shared/src/index.ts @@ -3,10 +3,14 @@ export * from './lib/decorators/is-asset-id.decorator' export * from './lib/decorators/is-hex-string.decorator' export * from './lib/decorators/is-not-empty-array-enum.decorator' export * from './lib/decorators/is-not-empty-array-string.decorator' +export * as FIXTURE from './lib/dev.fixture' +export * from './lib/dto' export * from './lib/type/action.type' export * from './lib/type/domain.type' +export * from './lib/type/entity.type' export * from './lib/util/caip.util' export * from './lib/util/enum.util' export * from './lib/util/evm.util' export * from './lib/util/hash-request.util' export * from './lib/util/json.util' +export * from './lib/util/typeguards' diff --git a/packages/authz-shared/src/lib/dev.fixture.ts b/packages/authz-shared/src/lib/dev.fixture.ts new file mode 100644 index 000000000..c67e18357 --- /dev/null +++ b/packages/authz-shared/src/lib/dev.fixture.ts @@ -0,0 +1,208 @@ +import { PrivateKeyAccount, sha256 } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { AccountClassification, AccountType, Alg, AuthCredential, UserRole } from './type/action.type' +import { + AddressBookAccountEntity, + Entities, + OrganizationEntity, + TokenEntity, + UserEntity, + UserGroupEntity, + WalletEntity, + WalletGroupEntity +} from './type/entity.type' + +const PERSONA = ['Root', 'Alice', 'Bob', 'Carol', 'Dave'] as const + +type Persona = (typeof PERSONA)[number] + +export const ORGANIZATION: OrganizationEntity = { + uid: '7d704a62-d15e-4382-a826-1eb41563043b' +} + +export const UNSAFE_PRIVATE_KEY: Record = { + // 0x000966c8bf232032cd23f9002c4513dfea2531be + Root: '0x4d377dba5424a7c1545a3c7b0522592927d49d2600a66f12e07a3977bafd79ab', + Alice: '0x454c9f13f6591f6482b17bdb6a671a7294500c7dd126111ce1643b03b6aeb354', + Bob: '0x569a6614716a76fdb9cf21b842d012add85e680b51fd4fb773109a93c6c4f307', + Carol: '0x33be709d0e3ffcd9ffa3d983d3fe3a55c34ab4eb4db2577847667262094f1786', + Dave: '0x82a0cf4f0fdfd42d93ff328b73bfdbc9c8b4f95f5aedfae82059753fc08a180f' +} + +export const ACCOUNT: Record = { + Root: privateKeyToAccount(UNSAFE_PRIVATE_KEY.Root), + Alice: privateKeyToAccount(UNSAFE_PRIVATE_KEY.Alice), + Bob: privateKeyToAccount(UNSAFE_PRIVATE_KEY.Bob), + Carol: privateKeyToAccount(UNSAFE_PRIVATE_KEY.Carol), + Dave: privateKeyToAccount(UNSAFE_PRIVATE_KEY.Dave) +} + +export const USER: Record = { + Root: { + uid: 'root:608bi1ef7-0efc-4a40-8739-0178a993b77c', + role: UserRole.ROOT + }, + Alice: { + uid: 'alice:0c6111fb-96ef-4177-8510-8cd994cc17ba', + role: UserRole.ADMIN + }, + Bob: { + uid: 'bob:3761b384-b5d3-4d29-9ed8-4b615fa1bcb3', + role: UserRole.ADMIN + }, + Carol: { + uid: 'carol:422dfe0b-0de1-44de-aaee-5262d6ebfb64', + role: UserRole.MANAGER + }, + Dave: { + uid: 'dave:4e7f31ad-a8e9-4a07-a19b-91c6883d7adb', + role: UserRole.MEMBER + } +} + +export const CREDENTIAL: Record = { + Root: { + uid: sha256(ACCOUNT.Root.address).toLowerCase(), + pubKey: ACCOUNT.Root.address, + alg: Alg.ES256K, + userId: USER.Root.uid + }, + Alice: { + uid: sha256(ACCOUNT.Alice.address).toLowerCase(), + pubKey: ACCOUNT.Alice.address, + alg: Alg.ES256K, + userId: USER.Alice.uid + }, + Bob: { + uid: sha256(ACCOUNT.Bob.address).toLowerCase(), + pubKey: ACCOUNT.Bob.address, + alg: Alg.ES256K, + userId: USER.Bob.uid + }, + Carol: { + uid: sha256(ACCOUNT.Carol.address).toLowerCase(), + pubKey: ACCOUNT.Carol.address, + alg: Alg.ES256K, + userId: USER.Carol.uid + }, + Dave: { + uid: sha256(ACCOUNT.Dave.address).toLowerCase(), + pubKey: ACCOUNT.Dave.address, + alg: Alg.ES256K, + userId: USER.Dave.uid + } +} + +export const USER_GROUP: Record = { + engineering: { + uid: 'ug:4735e190-6985-4f58-a723-c1a3aeec8b8c', + users: [USER.Alice.uid, USER.Carol.uid] + }, + treasury: { + uid: 'ug:08319ee9-c4f1-458f-b88c-e501ac575957', + users: [USER.Bob.uid, USER.Dave.uid] + } +} + +export const WALLET: Record = { + engineering1: { + uid: 'eip155:eoa:0xddcf208f219a6e6af072f2cfdc615b2c1805f98e', + address: '0xddcf208f219a6e6af072f2cfdc615b2c1805f98e', + accountType: AccountType.EOA, + assignees: [USER.Alice.uid] + }, + engineering2: { + uid: 'eip155:eoa:0x22228d0504d4f3363a5b7fda1f5fff1c7bca8ad4', + address: '0x22228d0504d4f3363a5b7fda1f5fff1c7bca8ad4', + accountType: AccountType.EOA + }, + treasury: { + uid: 'eip155:eoa:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b', // Prod guild 58 - treasury wallet + address: '0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b', + accountType: AccountType.EOA, + assignees: [USER.Alice.uid] + }, + operations: { + uid: 'eip155:eoa:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4', + address: '0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4', + accountType: AccountType.EOA, + assignees: [USER.Alice.uid] + } +} + +export const WALLET_GROUP: Record = { + engineering: { + uid: 'wg:9e60a686-ffbb-44fb-8bae-742fe1dedefb', + wallets: [WALLET.engineering1.uid, WALLET.engineering2.uid] + }, + treasury: { + uid: 'wg:df5db763-a3e0-4e19-848c-214e527e47cc', + wallets: [WALLET.treasury.uid] + } +} + +export const ADDRESS_BOOK: AddressBookAccountEntity[] = [ + { + uid: `eip155:137:${WALLET.engineering1.address}`, + address: WALLET.engineering1.address, + chainId: 137, + classification: AccountClassification.WALLET + }, + { + uid: `eip155:1:${WALLET.engineering1.address}`, + address: WALLET.engineering1.address, + chainId: 1, + classification: AccountClassification.WALLET + }, + { + uid: `eip155:137:${WALLET.engineering2.address}`, + address: WALLET.engineering2.address, + chainId: 137, + classification: AccountClassification.WALLET + }, + { + uid: `eip155:137:${WALLET.treasury.address}`, + address: WALLET.treasury.address, + chainId: 137, + classification: AccountClassification.WALLET + }, + { + uid: `eip155:1:${WALLET.treasury.address}`, + address: WALLET.treasury.address, + chainId: 1, + classification: AccountClassification.WALLET + }, + { + uid: `eip155:137:${WALLET.operations.address}`, + address: WALLET.operations.address, + chainId: 137, + classification: AccountClassification.WALLET + } +] + +export const TOKEN: Record = { + usdc1: { + uid: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + chainId: 1, + symbol: 'USDC', + decimals: 6 + }, + usdc137: { + uid: 'eip155:137/erc20:0x2791bca1f2de4661ed88a30c99a7a9449aa84174', + address: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', + chainId: 137, + symbol: 'USDC', + decimals: 6 + } +} + +export const ENTITIES: Entities = { + addressBook: ADDRESS_BOOK, + credentials: Object.values(CREDENTIAL), + tokens: Object.values(TOKEN), + userGroups: Object.values(USER_GROUP), + users: Object.values(USER), + walletGroups: Object.values(WALLET_GROUP), + wallets: Object.values(WALLET) +} diff --git a/packages/authz-shared/src/lib/dto/base-action-request.dto.ts b/packages/authz-shared/src/lib/dto/base-action-request.dto.ts new file mode 100644 index 000000000..89a09c538 --- /dev/null +++ b/packages/authz-shared/src/lib/dto/base-action-request.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger' +import { ArrayNotEmpty, IsDefined, ValidateNested } from 'class-validator' +import { SignatureDto } from './signature.dto' + +export class BaseActionRequestDto { + @IsDefined() + @ApiProperty({ + type: () => SignatureDto + }) + authentication: SignatureDto + + @IsDefined() + @ArrayNotEmpty() + @ValidateNested() + @ApiProperty({ + type: () => SignatureDto, + isArray: true + }) + approvals: SignatureDto[] +} diff --git a/apps/authz/src/app/http/rest/dto/base-action.dto.ts b/packages/authz-shared/src/lib/dto/base-action.dto.ts similarity index 87% rename from apps/authz/src/app/http/rest/dto/base-action.dto.ts rename to packages/authz-shared/src/lib/dto/base-action.dto.ts index fad7c89c2..fff06b3c8 100644 --- a/apps/authz/src/app/http/rest/dto/base-action.dto.ts +++ b/packages/authz-shared/src/lib/dto/base-action.dto.ts @@ -1,6 +1,6 @@ -import { Action } from '@narval/authz-shared' import { ApiProperty } from '@nestjs/swagger' import { IsDefined, IsIn, IsString } from 'class-validator' +import { Action } from '../type/action.type' export class BaseActionDto { @IsIn(Object.values(Action)) diff --git a/packages/authz-shared/src/lib/dto/index.ts b/packages/authz-shared/src/lib/dto/index.ts new file mode 100644 index 000000000..5d6b07444 --- /dev/null +++ b/packages/authz-shared/src/lib/dto/index.ts @@ -0,0 +1,3 @@ +export * from './base-action-request.dto' +export * from './base-action.dto' +export * from './signature.dto' diff --git a/apps/authz/src/app/http/rest/dto/request-signature.dto.ts b/packages/authz-shared/src/lib/dto/signature.dto.ts similarity index 81% rename from apps/authz/src/app/http/rest/dto/request-signature.dto.ts rename to packages/authz-shared/src/lib/dto/signature.dto.ts index 61b7d3865..6b2bcdf74 100644 --- a/apps/authz/src/app/http/rest/dto/request-signature.dto.ts +++ b/packages/authz-shared/src/lib/dto/signature.dto.ts @@ -1,8 +1,8 @@ -import { Alg } from '@narval/authz-shared' import { ApiProperty } from '@nestjs/swagger' import { IsDefined, IsIn, IsString } from 'class-validator' +import { Alg } from '../type/action.type' -export class RequestSignatureDto { +export class SignatureDto { @IsString() @IsDefined() @ApiProperty() diff --git a/packages/authz-shared/src/lib/type/action.type.ts b/packages/authz-shared/src/lib/type/action.type.ts index 6f221e0fa..a27792dab 100644 --- a/packages/authz-shared/src/lib/type/action.type.ts +++ b/packages/authz-shared/src/lib/type/action.type.ts @@ -1,3 +1,5 @@ +import { UserWalletEntity } from './entity.type' + export const Action = { CREATE_ORGANIZATION: 'CREATE_ORGANIZATION', @@ -29,8 +31,10 @@ export const Action = { SIGN_MESSAGE: 'signMessage', SIGN_TYPED_DATA: 'signTypedData' } as const + export type Action = (typeof Action)[keyof typeof Action] +// ENTITY export type AuthCredential = { uid: string // sha256 of the pubKey, used as the short identifier pubKey: string @@ -38,43 +42,50 @@ export type AuthCredential = { userId: string } +// ENTITY export const UserRole = { ROOT: 'root', ADMIN: 'admin', MEMBER: 'member', MANAGER: 'manager' } as const + +// ENTITY export type UserRole = (typeof UserRole)[keyof typeof UserRole] +// ENTITY export const AccountType = { EOA: 'eoa', AA: '4337' } as const + +// ENTITY export type AccountType = (typeof AccountType)[keyof typeof AccountType] +// ENTITY export const AccountClassification = { EXTERNAL: 'external', COUNTERPARTY: 'counterparty', INTERNAL: 'internal', WALLET: 'wallet' } as const + +// ENTITY export type AccountClassification = (typeof AccountClassification)[keyof typeof AccountClassification] +// ENTITY export type UserGroupMembership = { userId: string groupId: string } +// ENTITY export type WalletGroupMembership = { walletId: string groupId: string } -export type UserWallet = { - userId: string - walletId: string -} - +// DOMAIN export type Signature = { sig: string alg: Alg @@ -85,23 +96,29 @@ export type Signature = { pubKey: string } +// SIGNATURE LIB export const Alg = { ES256K: 'ES256K', // secp256k1, an Ethereum EOA ES256: 'ES256', // secp256r1, ecdsa but not ethereum RS256: 'RS256' } as const +// SIGNATURE LIB export type Alg = (typeof Alg)[keyof typeof Alg] +// DOMAIN export type Hex = `0x${string}` +// DOMAIN export type Address = `0x${string}` +// DOMAIN export type AccessList = { address: Address storageKeys: Hex[] }[] +// DOMAIN export type TransactionRequest = { chainId: number from: Address @@ -229,7 +246,7 @@ export type AssignWalletGroupRequest = BaseAdminRequest & { export type AssignUserWalletAction = BaseAction & { action: typeof Action.ASSIGN_USER_WALLET - data: UserWallet + data: UserWalletEntity } export type AssignUserWalletRequest = BaseAdminRequest & { diff --git a/packages/authz-shared/src/lib/type/entity.type.ts b/packages/authz-shared/src/lib/type/entity.type.ts new file mode 100644 index 000000000..10b04c50e --- /dev/null +++ b/packages/authz-shared/src/lib/type/entity.type.ts @@ -0,0 +1,58 @@ +import { AccountClassification, AccountType, Address, AuthCredential, UserRole } from './action.type' + +export type OrganizationEntity = { + uid: string +} + +export type UserEntity = { + uid: string + role: UserRole +} + +export type UserGroupEntity = { + uid: string + users: string[] +} + +export type UserWalletEntity = { + userId: string + walletId: string +} + +export type WalletEntity = { + uid: string + address: Address + accountType: AccountType + chainId?: number + assignees?: string[] +} + +export type WalletGroupEntity = { + uid: string + wallets: string[] +} + +export type AddressBookAccountEntity = { + uid: string + address: Address + chainId: number + classification: AccountClassification +} + +export type TokenEntity = { + uid: string + address: Address + symbol: string + chainId: number + decimals: number +} + +export type Entities = { + addressBook: AddressBookAccountEntity[] + credentials: AuthCredential[] + tokens: TokenEntity[] + userGroups: UserGroupEntity[] + users: UserEntity[] + walletGroups: WalletGroupEntity[] + wallets: WalletEntity[] +} diff --git a/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/UserOperationDecoder.ts b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/UserOperationDecoder.ts index 48af478b3..9f47a39aa 100644 --- a/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/UserOperationDecoder.ts +++ b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/UserOperationDecoder.ts @@ -1,5 +1,4 @@ -import { Address } from '@narval/authz-shared' -import { assertAddress, assertHexString } from 'packages/authz-shared/src/lib/util/typeguards' +import { Address, assertAddress, assertHexString } from '@narval/authz-shared' import { Hex, toHex } from 'viem' import { ContractCallInput, InputType, Intents } from '../../../domain' import { DecoderError } from '../../../error' diff --git a/packages/transaction-request-intent/src/lib/extraction/transformers.ts b/packages/transaction-request-intent/src/lib/extraction/transformers.ts index f7fe251ed..ac800f925 100644 --- a/packages/transaction-request-intent/src/lib/extraction/transformers.ts +++ b/packages/transaction-request-intent/src/lib/extraction/transformers.ts @@ -1,9 +1,4 @@ -import { - assertAddress, - assertArray, - assertBigInt, - assertHexString -} from 'packages/authz-shared/src/lib/util/typeguards' +import { assertAddress, assertArray, assertBigInt, assertHexString } from '@narval/authz-shared' import { DecoderError } from '../error' import { ApproveAllowanceParams, diff --git a/packages/transaction-request-intent/src/lib/utils.ts b/packages/transaction-request-intent/src/lib/utils.ts index 0fb0e3b0d..f3dd819cf 100644 --- a/packages/transaction-request-intent/src/lib/utils.ts +++ b/packages/transaction-request-intent/src/lib/utils.ts @@ -7,12 +7,13 @@ import { Hex, Namespace, TransactionRequest, + assertHexString, isAccountId, isAddress, + isString, toAccountId, toAssetId } from '@narval/authz-shared' -import { assertHexString, isString } from 'packages/authz-shared/src/lib/util/typeguards' import { SetOptional } from 'type-fest' import { Address, fromHex, presignMessagePrefix } from 'viem' import { diff --git a/packages/transaction-request-intent/tsconfig.json b/packages/transaction-request-intent/tsconfig.json index e258886ff..ad8d5c66e 100644 --- a/packages/transaction-request-intent/tsconfig.json +++ b/packages/transaction-request-intent/tsconfig.json @@ -14,6 +14,7 @@ "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "strictPropertyInitialization": false } }