diff --git a/apps/authz/Makefile b/apps/authz/Makefile index bcb302ccf..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 \ diff --git a/apps/authz/src/app/__test__/e2e/admin.spec.ts b/apps/authz/src/app/__test__/e2e/admin.spec.ts index d5e9b0c58..1d737e7ba 100644 --- a/apps/authz/src/app/__test__/e2e/admin.spec.ts +++ b/apps/authz/src/app/__test__/e2e/admin.spec.ts @@ -1,9 +1,10 @@ -import { 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 { PersistenceModule } from '../../../shared/module/persistence/persistence.module' @@ -11,6 +12,7 @@ import { TestPrismaService } from '../../../shared/module/persistence/service/te 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', () => { @@ -43,6 +45,9 @@ describe('Admin Endpoints', () => { } beforeAll(async () => { + const entityRepositoryMock = mock() + entityRepositoryMock.fetch.mockResolvedValue(FIXTURE.ENTITIES) + module = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ @@ -52,9 +57,13 @@ describe('Admin Endpoints', () => { PersistenceModule, AppModule ] - }).compile() + }) + .overrideProvider(EntityRepository) + .useValue(entityRepositoryMock) + .compile() testPrismaService = module.get(TestPrismaService) + app = module.createNestApplication() await app.init() diff --git a/apps/authz/src/app/app.module.ts b/apps/authz/src/app/app.module.ts index e9f57b35e..18d5c76af 100644 --- a/apps/authz/src/app/app.module.ts +++ b/apps/authz/src/app/app.module.ts @@ -9,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: [ @@ -24,8 +24,8 @@ import { AdminRepository } from './persistence/repository/admin.repository' 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 2ec280498..7d637749d 100644 --- a/apps/authz/src/app/core/admin.service.ts +++ b/apps/authz/src/app/core/admin.service.ts @@ -1,11 +1,9 @@ -import { Injectable, Logger } from '@nestjs/common' +import { Injectable } from '@nestjs/common' import { Policy, SetPolicyRulesRequest } from '../../shared/types/policy.type' import { OpaService } from '../opa/opa.service' @Injectable() export class AdminService { - private logger = new Logger(AdminService.name) - constructor(private opaService: OpaService) {} async setPolicyRules(payload: SetPolicyRulesRequest): Promise<{ fileId: string; policies: Policy[] }> { diff --git a/apps/authz/src/app/opa/opa.service.ts b/apps/authz/src/app/opa/opa.service.ts index 8a01c5afb..cfe11d1ca 100644 --- a/apps/authz/src/app/opa/opa.service.ts +++ b/apps/authz/src/app/opa/opa.service.ts @@ -1,19 +1,17 @@ -import { Entities } from '@narval/authz-shared' -import { HttpService } from '@nestjs/axios' import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common' 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 { lastValueFrom, map, tap } from 'rxjs' -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> @@ -25,7 +23,7 @@ export class OpaService implements OnApplicationBootstrap { private logger = new Logger(OpaService.name) private opaEngine: OpaEngine | undefined - constructor(private adminRepository: AdminRepository, private httpService: HttpService) {} + constructor(private entityRepository: EntityRepository) {} async onApplicationBootstrap(): Promise { this.logger.log('OPA Service boot') @@ -61,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') @@ -78,90 +76,23 @@ export class OpaService implements OnApplicationBootstrap { return { fileId, policies } } - private getEntities(): Promise { - return lastValueFrom( - this.httpService - .get('http://localhost:3005/store/entities', { - headers: { - 'x-org-id': '7d704a62-d15e-4382-a826-1eb41563043b' - } - }) - .pipe( - map((response) => response.data), - tap((entities) => { - this.logger.log('Received entities snapshot', entities) - }) - ) - ) - } - 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 entities = await this.getEntities() - - console.log('####', entities) - - 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 entities = await this.entityRepository.fetch(ORGANIZATION.uid) - 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 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/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/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/seed.ts b/apps/orchestration/src/shared/module/persistence/seed.ts index f369c3f2a..5cdf6539f 100644 --- a/apps/orchestration/src/shared/module/persistence/seed.ts +++ b/apps/orchestration/src/shared/module/persistence/seed.ts @@ -41,12 +41,8 @@ async function main() { await germinateTransferTrackingModule(prisma) logger.log('Orchestration database germinated 🌱') - - process.exit(0) - } catch (error) { - logger.error('Seed failed', error) - - process.exit(1) + } finally { + await application.close() } } diff --git a/apps/orchestration/src/store/entity/core/service/entity.service.ts b/apps/orchestration/src/store/entity/core/service/entity.service.ts index b29360cf4..abd2102d2 100644 --- a/apps/orchestration/src/store/entity/core/service/entity.service.ts +++ b/apps/orchestration/src/store/entity/core/service/entity.service.ts @@ -1,9 +1,9 @@ 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 { 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' @@ -12,30 +12,30 @@ import { WalletRepository } from '../../persistence/repository/wallet.repository export class EntityService { 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 getEntities(orgId: string): Promise { - const [addressBook, tokens, userGroups, users, userWallets, walletGroups, wallets] = await Promise.all([ + 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.userWalletRepository.findByOrgId(orgId), this.walletGroupRepository.findByOrgId(orgId), this.walletRepository.findByOrgId(orgId) ]) return { addressBook, + credentials, tokens, userGroups, - userWallets, users, walletGroups, wallets 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 index 0e30a7a75..ca0a9aa94 100644 --- a/apps/orchestration/src/store/entity/http/rest/dto/entities.dto.ts +++ b/apps/orchestration/src/store/entity/http/rest/dto/entities.dto.ts @@ -1,6 +1,7 @@ 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' @@ -12,6 +13,10 @@ export class EntitiesDto { @ValidateNested({ each: true }) addressBook: AddressBookAccountDto[] + @Type(() => AuthCredentialDto) + @ValidateNested({ each: true }) + credentials: AuthCredentialDto[] + @Type(() => TokenDto) @ValidateNested({ each: true }) tokens: TokenDto[] diff --git a/apps/orchestration/src/store/entity/persistence/entity-store.seed.ts b/apps/orchestration/src/store/entity/persistence/entity-store.seed.ts index 11fe52f49..0c9fd9cd5 100644 --- a/apps/orchestration/src/store/entity/persistence/entity-store.seed.ts +++ b/apps/orchestration/src/store/entity/persistence/entity-store.seed.ts @@ -3,7 +3,6 @@ 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 { EntityService } from '../core/service/entity.service' import { AddressBookRepository } from './repository/address-book.repository' import { CredentialRepository } from './repository/credential.repository' import { TokenRepository } from './repository/token.repository' @@ -25,8 +24,7 @@ export class EntityStoreSeed implements Seeder { private userRepository: UserRepository, private userWalletRepository: UserWalletRepository, private walletGroupRepository: WalletGroupRepository, - private walletRepository: WalletRepository, - private entityService: EntityService + private walletRepository: WalletRepository ) {} async germinate(): Promise { @@ -70,7 +68,5 @@ export class EntityStoreSeed implements Seeder { await Promise.all(FIXTURE.ADDRESS_BOOK.map((entity) => this.addressBookRepository.create(ORGANIZATION.uid, entity))) await this.tokenRepository.create(ORGANIZATION.uid, Object.values(FIXTURE.TOKEN)) - - this.logger.log('Entities seeded', await this.entityService.getEntities(ORGANIZATION.uid)) } } diff --git a/apps/orchestration/src/store/entity/persistence/repository/credential.repository.ts b/apps/orchestration/src/store/entity/persistence/repository/credential.repository.ts index c8c6122e0..6edab990e 100644 --- a/apps/orchestration/src/store/entity/persistence/repository/credential.repository.ts +++ b/apps/orchestration/src/store/entity/persistence/repository/credential.repository.ts @@ -1,5 +1,6 @@ import { Alg, AuthCredential } from '@narval/authz-shared' import { Injectable } from '@nestjs/common' +import { AuthCredentialEntity as Model } from '@prisma/client/orchestration' import { PrismaService } from '../../../../shared/module/persistence/service/prisma.service' import { decodeConstant } from '../decode.util' @@ -22,14 +23,26 @@ export class CredentialRepository { } async findById(uid: string): Promise { - const entity = await this.prismaService.authCredentialEntity.findUnique({ + const model = await this.prismaService.authCredentialEntity.findUnique({ where: { uid } }) - if (entity) { - return decodeConstant(entity, 'alg', Object.values(Alg)) + 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(model, 'alg', Object.values(Alg)) + } } diff --git a/package-lock.json b/package-lock.json index 0a6fe0076..6d6015300 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@bull-board/nestjs": "^5.11.0", "@docusaurus/core": "3.1.1", "@docusaurus/preset-classic": "3.1.0", + "@golevelup/nestjs-discovery": "^4.0.0", "@mdx-js/react": "^3.0.0", "@nestjs/axios": "^3.0.1", "@nestjs/bull": "^10.0.1", @@ -5340,6 +5341,18 @@ "npm": ">=6.14.13" } }, + "node_modules/@golevelup/nestjs-discovery": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-4.0.0.tgz", + "integrity": "sha512-iyZLYip9rhVMR0C93vo860xmboRrD5g5F5iEOfpeblGvYSz8ymQrL9RAST7x/Fp3n+TAXSeOLzDIASt+rak68g==", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^10.x", + "@nestjs/core": "^10.x" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", diff --git a/package.json b/package.json index 434050ed8..6e8c07946 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@bull-board/nestjs": "^5.11.0", "@docusaurus/core": "3.1.1", "@docusaurus/preset-classic": "3.1.0", + "@golevelup/nestjs-discovery": "^4.0.0", "@mdx-js/react": "^3.0.0", "@nestjs/axios": "^3.0.1", "@nestjs/bull": "^10.0.1", diff --git a/packages/authz-shared/src/lib/dev.fixture.ts b/packages/authz-shared/src/lib/dev.fixture.ts index 62753b3cb..c67e18357 100644 --- a/packages/authz-shared/src/lib/dev.fixture.ts +++ b/packages/authz-shared/src/lib/dev.fixture.ts @@ -1,8 +1,9 @@ -import { Account, sha256 } from 'viem' +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, @@ -28,7 +29,7 @@ export const UNSAFE_PRIVATE_KEY: Record = { Dave: '0x82a0cf4f0fdfd42d93ff328b73bfdbc9c8b4f95f5aedfae82059753fc08a180f' } -export const ACCOUNT: Record = { +export const ACCOUNT: Record = { Root: privateKeyToAccount(UNSAFE_PRIVATE_KEY.Root), Alice: privateKeyToAccount(UNSAFE_PRIVATE_KEY.Alice), Bob: privateKeyToAccount(UNSAFE_PRIVATE_KEY.Bob), @@ -195,3 +196,13 @@ export const TOKEN: Record = { 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/type/entity.type.ts b/packages/authz-shared/src/lib/type/entity.type.ts index 8a4dbf9c6..10b04c50e 100644 --- a/packages/authz-shared/src/lib/type/entity.type.ts +++ b/packages/authz-shared/src/lib/type/entity.type.ts @@ -1,4 +1,4 @@ -import { AccountClassification, AccountType, Address, UserRole } from './action.type' +import { AccountClassification, AccountType, Address, AuthCredential, UserRole } from './action.type' export type OrganizationEntity = { uid: string @@ -49,10 +49,10 @@ export type TokenEntity = { export type Entities = { addressBook: AddressBookAccountEntity[] - users: UserEntity[] - userWallets: UserWalletEntity[] + credentials: AuthCredential[] + tokens: TokenEntity[] userGroups: UserGroupEntity[] - wallets: WalletEntity[] + users: UserEntity[] walletGroups: WalletGroupEntity[] - tokens: TokenEntity[] + wallets: WalletEntity[] }