diff --git a/apps/policy-engine/src/app/app.module.ts b/apps/policy-engine/src/app/app.module.ts index 5f6adf991..840ca0085 100644 --- a/apps/policy-engine/src/app/app.module.ts +++ b/apps/policy-engine/src/app/app.module.ts @@ -1,5 +1,5 @@ import { HttpModule } from '@nestjs/axios' -import { Module, ValidationPipe } from '@nestjs/common' +import { Module, OnApplicationBootstrap, ValidationPipe } from '@nestjs/common' import { ConfigModule } from '@nestjs/config' import { APP_PIPE } from '@nestjs/core' import { EncryptionModule } from '../encryption/encryption.module' @@ -7,12 +7,17 @@ import { load } from '../policy-engine.config' import { KeyValueModule } from '../shared/module/key-value/key-value.module' import { AppController } from './app.controller' import { AppService } from './app.service' +import { DataStoreRepositoryFactory } from './core/factory/data-store-repository.factory' +import { BootstrapService } from './core/service/bootstrap.service' +import { DataStoreService } from './core/service/data-store.service' import { EngineService } from './core/service/engine.service' import { TenantService } from './core/service/tenant.service' import { TenantController } from './http/rest/controller/tenant.controller' import { OpaService } from './opa/opa.service' import { EngineRepository } from './persistence/repository/engine.repository' import { EntityRepository } from './persistence/repository/entity.repository' +import { FileSystemDataStoreRepository } from './persistence/repository/file-system-data-store.repository' +import { HttpDataStoreRepository } from './persistence/repository/http-data-store.repository' import { TenantRepository } from './persistence/repository/tenant.repository' @Module({ @@ -28,10 +33,15 @@ import { TenantRepository } from './persistence/repository/tenant.repository' controllers: [AppController, TenantController], providers: [ AppService, - OpaService, + BootstrapService, + DataStoreRepositoryFactory, + DataStoreService, EngineRepository, EngineService, EntityRepository, + FileSystemDataStoreRepository, + HttpDataStoreRepository, + OpaService, TenantRepository, TenantService, { @@ -40,4 +50,10 @@ import { TenantRepository } from './persistence/repository/tenant.repository' } ] }) -export class AppModule {} +export class AppModule implements OnApplicationBootstrap { + constructor(private bootstrapService: BootstrapService) {} + + async onApplicationBootstrap() { + await this.bootstrapService.boot() + } +} diff --git a/apps/policy-engine/src/app/core/service/__test__/unit/bootstrap.service.spec.ts b/apps/policy-engine/src/app/core/service/__test__/unit/bootstrap.service.spec.ts new file mode 100644 index 000000000..8fbf87f52 --- /dev/null +++ b/apps/policy-engine/src/app/core/service/__test__/unit/bootstrap.service.spec.ts @@ -0,0 +1,64 @@ +import { Test } from '@nestjs/testing' +import { MockProxy, mock } from 'jest-mock-extended' +import { BootstrapService } from '../../bootstrap.service' +import { TenantService } from '../../tenant.service' + +describe(BootstrapService.name, () => { + let bootstrapService: BootstrapService + let tenantServiceMock: MockProxy + + const dataStore = { + entity: { + dataUrl: 'http://9.9.9.9:90', + signatureUrl: 'http://9.9.9.9:90', + keys: [] + }, + policy: { + dataUrl: 'http://9.9.9.9:90', + signatureUrl: 'http://9.9.9.9:90', + keys: [] + } + } + + const tenantOne = { + dataStore, + clientId: 'test-tenant-one-id', + clientSecret: 'unsafe-client-secret', + createdAt: new Date(), + updatedAt: new Date() + } + + const tenantTwo = { + dataStore, + clientId: 'test-tenant-two-id', + clientSecret: 'unsafe-client-secret', + createdAt: new Date(), + updatedAt: new Date() + } + + beforeEach(async () => { + tenantServiceMock = mock() + tenantServiceMock.findAll.mockResolvedValue([tenantOne, tenantTwo]) + + const module = await Test.createTestingModule({ + providers: [ + BootstrapService, + { + provide: TenantService, + useValue: tenantServiceMock + } + ] + }).compile() + + bootstrapService = module.get(BootstrapService) + }) + + describe('boot', () => { + it('syncs tenants data stores', async () => { + await bootstrapService.boot() + + expect(tenantServiceMock.syncDataStore).toHaveBeenNthCalledWith(1, tenantOne.clientId) + expect(tenantServiceMock.syncDataStore).toHaveBeenNthCalledWith(2, tenantTwo.clientId) + }) + }) +}) diff --git a/apps/policy-engine/src/app/core/service/__test__/unit/tenant.service.spec.ts b/apps/policy-engine/src/app/core/service/__test__/unit/tenant.service.spec.ts new file mode 100644 index 000000000..8d26e1b62 --- /dev/null +++ b/apps/policy-engine/src/app/core/service/__test__/unit/tenant.service.spec.ts @@ -0,0 +1,91 @@ +import { DataStoreConfiguration, FIXTURE } from '@narval/policy-engine-shared' +import { Test } from '@nestjs/testing' +import { MockProxy, mock } from 'jest-mock-extended' +import { KeyValueService } from '../../../../../shared/module/key-value/core/service/key-value.service' +import { InMemoryKeyValueRepository } from '../../../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository' +import { Tenant } from '../../../../../shared/type/domain.type' +import { TenantRepository } from '../../../../persistence/repository/tenant.repository' +import { DataStoreService } from '../../data-store.service' +import { TenantService } from '../../tenant.service' + +describe(TenantService.name, () => { + let tenantService: TenantService + let tenantRepository: TenantRepository + let dataStoreServiceMock: MockProxy + + const clientId = 'test-client-id' + + const dataStoreConfiguration: DataStoreConfiguration = { + dataUrl: 'a-url-that-doesnt-need-to-exist-for-the-purpose-of-this-test', + signatureUrl: 'a-url-that-doesnt-need-to-exist-for-the-purpose-of-this-test', + keys: [] + } + + const tenant: Tenant = { + clientId, + clientSecret: 'test-client-secret', + dataStore: { + entity: dataStoreConfiguration, + policy: dataStoreConfiguration + }, + createdAt: new Date(), + updatedAt: new Date() + } + + const stores = { + entity: { + data: FIXTURE.ENTITIES, + signature: 'test-signature' + }, + policy: { + data: FIXTURE.POLICIES, + signature: 'test-signature' + } + } + + beforeEach(async () => { + dataStoreServiceMock = mock() + dataStoreServiceMock.fetch.mockResolvedValue(stores) + + const module = await Test.createTestingModule({ + providers: [ + TenantService, + TenantRepository, + { + provide: DataStoreService, + useValue: dataStoreServiceMock + }, + { + provide: KeyValueService, + useClass: InMemoryKeyValueRepository + } + ] + }).compile() + + tenantService = module.get(TenantService) + tenantRepository = module.get(TenantRepository) + }) + + describe('syncDataStore', () => { + beforeEach(async () => { + await tenantRepository.save(tenant) + }) + + it('saves entity and policy stores', async () => { + expect(await tenantRepository.findEntityStore(clientId)).toEqual(null) + expect(await tenantRepository.findPolicyStore(clientId)).toEqual(null) + + await tenantService.syncDataStore(clientId) + + expect(await tenantRepository.findEntityStore(clientId)).toEqual(stores.entity) + expect(await tenantRepository.findPolicyStore(clientId)).toEqual(stores.policy) + }) + + it('fetches the data stores once', async () => { + await tenantService.syncDataStore(clientId) + + expect(dataStoreServiceMock.fetch).toHaveBeenCalledTimes(1) + expect(dataStoreServiceMock.fetch).toHaveBeenCalledWith(tenant.dataStore) + }) + }) +}) diff --git a/apps/policy-engine/src/app/core/service/bootstrap.service.ts b/apps/policy-engine/src/app/core/service/bootstrap.service.ts new file mode 100644 index 000000000..c361a40a4 --- /dev/null +++ b/apps/policy-engine/src/app/core/service/bootstrap.service.ts @@ -0,0 +1,51 @@ +import { Injectable, Logger } from '@nestjs/common' +import { TenantService } from './tenant.service' + +@Injectable() +export class BootstrapService { + private logger = new Logger(BootstrapService.name) + + constructor(private tenantService: TenantService) {} + + async boot(): Promise { + this.logger.log('Start application bootstrap procedure') + + await this.tenantService.onboard( + { + clientId: '012553b0-34e9-4b48-b217-ced3c906cd39', + clientSecret: 'unsafe-dev-secret', + dataStore: { + entity: { + dataUrl: 'http://127.0.0.1:4200/api/data-store', + signatureUrl: 'http://127.0.0.1:4200/api/data-store', + keys: [] + }, + policy: { + dataUrl: 'http://127.0.0.1:4200/api/data-store', + signatureUrl: 'http://127.0.0.1:4200/api/data-store', + keys: [] + } + }, + createdAt: new Date(), + updatedAt: new Date() + }, + // Disable sync after the onboard because we'll sync it as part of the boot. + { syncAfter: false } + ) + + await this.syncTenants() + } + + private async syncTenants(): Promise { + const tenants = await this.tenantService.findAll() + + this.logger.log('Start syncing tenants data stores', { + tenantsCount: tenants.length + }) + + // TODO: (@wcalderipe, 07/03/24) maybe change the execution to parallel? + for (const tenant of tenants) { + await this.tenantService.syncDataStore(tenant.clientId) + } + } +} diff --git a/apps/policy-engine/src/app/core/service/data-store.service.ts b/apps/policy-engine/src/app/core/service/data-store.service.ts index 5fc846d72..3f5832875 100644 --- a/apps/policy-engine/src/app/core/service/data-store.service.ts +++ b/apps/policy-engine/src/app/core/service/data-store.service.ts @@ -1,7 +1,7 @@ import { DataStoreConfiguration, - Entities, - Policy, + EntityStore, + PolicyStore, entityDataSchema, entitySignatureSchema, policyDataSchema, @@ -17,14 +17,8 @@ export class DataStoreService { constructor(private dataStoreRepositoryFactory: DataStoreRepositoryFactory) {} async fetch(store: { entity: DataStoreConfiguration; policy: DataStoreConfiguration }): Promise<{ - entity: { - data: Entities - signature: string - } - policy: { - data: Policy[] - signature: string - } + entity: EntityStore + policy: PolicyStore }> { const [entityData, entitySignature, policyData, policySignature] = await Promise.all([ this.fetchByUrl(store.entity.dataUrl, entityDataSchema), diff --git a/apps/policy-engine/src/app/core/service/tenant.service.ts b/apps/policy-engine/src/app/core/service/tenant.service.ts index 7163fd0b6..7dcf38027 100644 --- a/apps/policy-engine/src/app/core/service/tenant.service.ts +++ b/apps/policy-engine/src/app/core/service/tenant.service.ts @@ -1,18 +1,29 @@ -import { HttpStatus, Injectable } from '@nestjs/common' +import { EntityStore, PolicyStore } from '@narval/policy-engine-shared' +import { HttpStatus, Injectable, Logger } from '@nestjs/common' import { ApplicationException } from '../../../shared/exception/application.exception' import { Tenant } from '../../../shared/type/domain.type' import { TenantRepository } from '../../persistence/repository/tenant.repository' +import { DataStoreService } from './data-store.service' @Injectable() export class TenantService { - constructor(private tenantRepository: TenantRepository) {} + private logger = new Logger(TenantService.name) + + constructor( + private tenantRepository: TenantRepository, + private dataStoreService: DataStoreService + ) {} async findByClientId(clientId: string): Promise { return this.tenantRepository.findByClientId(clientId) } - async create(tenant: Tenant): Promise { - if (await this.tenantRepository.findByClientId(tenant.clientId)) { + async onboard(tenant: Tenant, options?: { syncAfter?: boolean }): Promise { + const syncAfter = options?.syncAfter ?? true + + const exists = await this.tenantRepository.findByClientId(tenant.clientId) + + if (exists) { throw new ApplicationException({ message: 'Tenant already exist', suggestedHttpStatusCode: HttpStatus.BAD_REQUEST, @@ -20,6 +31,71 @@ export class TenantService { }) } - return this.tenantRepository.create(tenant) + try { + await this.tenantRepository.save(tenant) + + if (syncAfter) { + const hasSynced = await this.syncDataStore(tenant.clientId) + + if (!hasSynced) { + this.logger.warn('Failed to sync new tenant data store during the onboard') + } + } + + return tenant + } catch (error) { + throw new ApplicationException({ + message: 'Failed to onboard new tenant', + suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + origin: error, + context: { tenant } + }) + } + } + + async syncDataStore(clientId: string): Promise { + this.logger.log('Start syncing tenant data stores', { clientId }) + + try { + const tenant = await this.findByClientId(clientId) + + if (tenant) { + this.logger.log('Sync tenant data stores', { + dataStore: tenant.dataStore + }) + + const stores = await this.dataStoreService.fetch(tenant.dataStore) + + await Promise.all([ + this.tenantRepository.saveEntityStore(clientId, stores.entity), + this.tenantRepository.savePolicyStore(clientId, stores.policy) + ]) + + this.logger.log('Tenant data stores synced', { clientId, stores }) + + return true + } + + return false + } catch (error) { + this.logger.error('Failed to sync tenant data store', { + message: error.message, + stack: error.stack + }) + + return false + } + } + + async findEntityStore(clientId: string): Promise { + return this.tenantRepository.findEntityStore(clientId) + } + + async findPolicyStore(clientId: string): Promise { + return this.tenantRepository.findPolicyStore(clientId) + } + + async findAll(): Promise { + return this.tenantRepository.findAll() } } diff --git a/apps/policy-engine/src/app/http/rest/controller/tenant.controller.ts b/apps/policy-engine/src/app/http/rest/controller/tenant.controller.ts index 5a43596c6..dc96ba555 100644 --- a/apps/policy-engine/src/app/http/rest/controller/tenant.controller.ts +++ b/apps/policy-engine/src/app/http/rest/controller/tenant.controller.ts @@ -14,7 +14,7 @@ export class TenantController { async create(@Body() body: CreateTenantDto) { const now = new Date() - const tenant = await this.tenantService.create({ + const tenant = await this.tenantService.onboard({ clientId: body.clientId || uuid(), clientSecret: randomBytes(42).toString('hex'), dataStore: { diff --git a/apps/policy-engine/src/app/opa/opa.service.ts b/apps/policy-engine/src/app/opa/opa.service.ts index d39436cd2..338a7ddb1 100644 --- a/apps/policy-engine/src/app/opa/opa.service.ts +++ b/apps/policy-engine/src/app/opa/opa.service.ts @@ -113,8 +113,6 @@ export class OpaService implements OnApplicationBootstrap { } } - this.logger.log('Fetched OPA Engine data', data) - return data } diff --git a/apps/policy-engine/src/app/persistence/repository/__test__/unit/tenant.repository.spec.ts b/apps/policy-engine/src/app/persistence/repository/__test__/unit/tenant.repository.spec.ts index c0e7b2602..b14cccdd8 100644 --- a/apps/policy-engine/src/app/persistence/repository/__test__/unit/tenant.repository.spec.ts +++ b/apps/policy-engine/src/app/persistence/repository/__test__/unit/tenant.repository.spec.ts @@ -1,4 +1,12 @@ -import { DataStoreConfiguration } from '@narval/policy-engine-shared' +import { + Action, + Criterion, + DataStoreConfiguration, + EntityStore, + FIXTURE, + PolicyStore, + Then +} from '@narval/policy-engine-shared' import { Test } from '@nestjs/testing' import { mock } from 'jest-mock-extended' import { EncryptionService } from '../../../../../encryption/core/encryption.service' @@ -14,6 +22,8 @@ describe(TenantRepository.name, () => { let repository: TenantRepository let inMemoryKeyValueRepository: InMemoryKeyValueRepository + const clientId = 'test-client-id' + beforeEach(async () => { inMemoryKeyValueRepository = new InMemoryKeyValueRepository() @@ -50,7 +60,7 @@ describe(TenantRepository.name, () => { repository = module.get(TenantRepository) }) - describe('create', () => { + describe('save', () => { const now = new Date() const dataStoreConfiguration: DataStoreConfiguration = { @@ -60,7 +70,7 @@ describe(TenantRepository.name, () => { } const tenant: Tenant = { - clientId: 'test-client-id', + clientId, clientSecret: 'test-client-secret', dataStore: { entity: dataStoreConfiguration, @@ -70,8 +80,8 @@ describe(TenantRepository.name, () => { updatedAt: now } - it('creates a new tenant', async () => { - await repository.create(tenant) + it('saves a new tenant', async () => { + await repository.save(tenant) const value = await inMemoryKeyValueRepository.get(repository.getKey(tenant.clientId)) const actualTenant = await repository.findByClientId(tenant.clientId) @@ -81,9 +91,64 @@ describe(TenantRepository.name, () => { }) it('indexes the new tenant', async () => { - await repository.create(tenant) + await repository.save(tenant) expect(await repository.getTenantIndex()).toEqual([tenant.clientId]) }) }) + + describe('saveEntityStore', () => { + const store: EntityStore = { + data: FIXTURE.ENTITIES, + signature: 'test-fake-signature' + } + + it('saves the entity store', async () => { + await repository.saveEntityStore(clientId, store) + + expect(await repository.findEntityStore(clientId)).toEqual(store) + }) + + it('overwrites existing entity store', async () => { + const storeTwo = { ...store, signature: 'another-test-signature' } + + await repository.saveEntityStore(clientId, store) + await repository.saveEntityStore(clientId, storeTwo) + + expect(await repository.findEntityStore(clientId)).toEqual(storeTwo) + }) + }) + + describe('savePolicyStore', () => { + const store: PolicyStore = { + data: [ + { + then: Then.PERMIT, + name: 'test-policy', + when: [ + { + criterion: Criterion.CHECK_ACTION, + args: [Action.SIGN_TRANSACTION] + } + ] + } + ], + signature: 'test-fake-signature' + } + + it('saves the policy store', async () => { + await repository.savePolicyStore(clientId, store) + + expect(await repository.findPolicyStore(clientId)).toEqual(store) + }) + + it('overwrites existing policy store', async () => { + const storeTwo = { ...store, signature: 'another-test-signature' } + + await repository.savePolicyStore(clientId, store) + await repository.savePolicyStore(clientId, storeTwo) + + expect(await repository.findPolicyStore(clientId)).toEqual(storeTwo) + }) + }) }) diff --git a/apps/policy-engine/src/app/persistence/repository/entity.repository.ts b/apps/policy-engine/src/app/persistence/repository/entity.repository.ts index 6c5729fb7..b48079999 100644 --- a/apps/policy-engine/src/app/persistence/repository/entity.repository.ts +++ b/apps/policy-engine/src/app/persistence/repository/entity.repository.ts @@ -1,55 +1,20 @@ import { CredentialEntity, Entities, FIXTURE } from '@narval/policy-engine-shared' import { HttpService } from '@nestjs/axios' -import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common' -import { lastValueFrom, map, tap } from 'rxjs' +import { Injectable, Logger } from '@nestjs/common' @Injectable() -export class EntityRepository implements OnApplicationBootstrap { +export class EntityRepository { private logger = new Logger(EntityRepository.name) - private entities?: Entities - constructor(private httpService: HttpService) {} - fetch(orgId: string): Promise { + async 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) - }) - ) - ) + return FIXTURE.ENTITIES } getCredentialForPubKey(pubKey: string): CredentialEntity | 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. - try { - if (!this.entities) { - const entities = await this.fetch(FIXTURE.ORGANIZATION.id) - - this.entities = entities - } - } catch (error) { - this.logger.error('Failed to bootstrap entities', error) - } + return FIXTURE.ENTITIES.credentials.find((cred) => cred.pubKey === pubKey) || null } } diff --git a/apps/policy-engine/src/app/persistence/repository/tenant.repository.ts b/apps/policy-engine/src/app/persistence/repository/tenant.repository.ts index 7063e45f5..da36c809a 100644 --- a/apps/policy-engine/src/app/persistence/repository/tenant.repository.ts +++ b/apps/policy-engine/src/app/persistence/repository/tenant.repository.ts @@ -1,4 +1,6 @@ +import { EntityStore, PolicyStore, entityStoreSchema, policyStoreSchema } from '@narval/policy-engine-shared' import { Injectable } from '@nestjs/common' +import { compact } from 'lodash/fp' import { KeyValueService } from '../../../shared/module/key-value/core/service/key-value.service' import { tenantIndexSchema, tenantSchema } from '../../../shared/schema/tenant.schema' import { Tenant } from '../../../shared/type/domain.type' @@ -17,21 +19,13 @@ export class TenantRepository { return null } - async create(tenant: Tenant): Promise { + async save(tenant: Tenant): Promise { await this.keyValueService.set(this.getKey(tenant.clientId), this.encode(tenant)) await this.index(tenant) return tenant } - private async index(tenant: Tenant): Promise { - const currentIndex = await this.getTenantIndex() - - await this.keyValueService.set(this.getIndexKey(), this.encodeIndex([...currentIndex, tenant.clientId])) - - return true - } - async getTenantIndex(): Promise { const index = await this.keyValueService.get(this.getIndexKey()) @@ -42,12 +36,70 @@ export class TenantRepository { return [] } + async saveEntityStore(clientId: string, store: EntityStore): Promise { + return this.keyValueService.set(this.getEntityStoreKey(clientId), this.encodeEntityStore(store)) + } + + async findEntityStore(clientId: string): Promise { + const value = await this.keyValueService.get(this.getEntityStoreKey(clientId)) + + if (value) { + return this.decodeEntityStore(value) + } + + return null + } + + async savePolicyStore(clientId: string, store: PolicyStore): Promise { + return this.keyValueService.set(this.getPolicyStoreKey(clientId), this.encodePolicyStore(store)) + } + + async findPolicyStore(clientId: string): Promise { + const value = await this.keyValueService.get(this.getPolicyStoreKey(clientId)) + + if (value) { + return this.decodePolicyStore(value) + } + + return null + } + + // TODO: (@wcalderipe, 07/03/24) we need to rethink this strategy. If we use a + // SQL database, this could generate a massive amount of queries; thus, + // degrading the performance. + // + // An option is to move these general queries `findBy`, findAll`, etc to the + // KeyValeuRepository implementation letting each implementation pick the best + // strategy to solve the problem (e.g. where query in SQL) + async findAll(): Promise { + const ids = await this.getTenantIndex() + const tenants = await Promise.all(ids.map((id) => this.findByClientId(id))) + + return compact(tenants) + } + getKey(clientId: string): string { return `tenant:${clientId}` } getIndexKey(): string { - return `tenants` + return 'tenant:index' + } + + getEntityStoreKey(clientId: string): string { + return `tenant:${clientId}:entity-store` + } + + getPolicyStoreKey(clientId: string): string { + return `tenant:${clientId}:policy-store` + } + + private async index(tenant: Tenant): Promise { + const currentIndex = await this.getTenantIndex() + + await this.keyValueService.set(this.getIndexKey(), this.encodeIndex([...currentIndex, tenant.clientId])) + + return true } private encode(tenant: Tenant): string { @@ -65,4 +117,20 @@ export class TenantRepository { private decodeIndex(value: string): string[] { return tenantIndexSchema.parse(JSON.parse(value)) } + + private encodeEntityStore(value: EntityStore): string { + return KeyValueService.encode(entityStoreSchema.parse(value)) + } + + private decodeEntityStore(value: string): EntityStore { + return entityStoreSchema.parse(JSON.parse(value)) + } + + private encodePolicyStore(value: PolicyStore): string { + return KeyValueService.encode(policyStoreSchema.parse(value)) + } + + private decodePolicyStore(value: string): PolicyStore { + return policyStoreSchema.parse(JSON.parse(value)) + } } diff --git a/apps/policy-engine/src/opa/template/mockData.ts b/apps/policy-engine/src/opa/template/mockData.ts deleted file mode 100644 index 852e744ad..000000000 --- a/apps/policy-engine/src/opa/template/mockData.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { - Action, - Criterion, - EntityType, - FIXTURE, - Policy, - Then, - UserRole, - ValueOperators -} from '@narval/policy-engine-shared' -import { Intents } from '@narval/transaction-request-intent' - -export const examplePermitPolicy: Policy = { - then: Then.PERMIT, - name: 'examplePermitPolicy', - when: [ - { - criterion: Criterion.CHECK_RESOURCE_INTEGRITY, - args: null - }, - { - criterion: Criterion.CHECK_NONCE_EXISTS, - args: null - }, - { - criterion: Criterion.CHECK_ACTION, - args: [Action.SIGN_TRANSACTION] - }, - { - criterion: Criterion.CHECK_PRINCIPAL_ID, - args: [FIXTURE.USER.Alice.role] - }, - { - criterion: Criterion.CHECK_WALLET_ID, - args: [FIXTURE.WALLET.Engineering.address] - }, - { - criterion: Criterion.CHECK_INTENT_TYPE, - args: [Intents.TRANSFER_NATIVE] - }, - { - criterion: Criterion.CHECK_INTENT_TOKEN, - args: ['eip155:137/slip44:966'] - }, - { - criterion: Criterion.CHECK_INTENT_AMOUNT, - args: { - currency: '*', - operator: ValueOperators.LESS_THAN_OR_EQUAL, - value: '1000000000000000000' - } - }, - { - criterion: Criterion.CHECK_APPROVALS, - args: [ - { - approvalCount: 2, - countPrincipal: false, - approvalEntityType: EntityType.User, - entityIds: [FIXTURE.USER.Bob.id, FIXTURE.USER.Carol.id] - }, - { - approvalCount: 1, - countPrincipal: false, - approvalEntityType: EntityType.UserRole, - entityIds: [UserRole.ADMIN] - } - ] - } - ] -} - -export const exampleForbidPolicy: Policy = { - then: Then.FORBID, - name: 'exampleForbidPolicy', - when: [ - { - criterion: Criterion.CHECK_RESOURCE_INTEGRITY, - args: null - }, - { - criterion: Criterion.CHECK_NONCE_EXISTS, - args: null - }, - { - criterion: Criterion.CHECK_ACTION, - args: [Action.SIGN_TRANSACTION] - }, - { - criterion: Criterion.CHECK_PRINCIPAL_ID, - args: [FIXTURE.USER.Alice.id] - }, - { - criterion: Criterion.CHECK_WALLET_ID, - args: [FIXTURE.WALLET.Engineering.address] - }, - { - criterion: Criterion.CHECK_INTENT_TYPE, - args: [Intents.TRANSFER_NATIVE] - }, - { - criterion: Criterion.CHECK_INTENT_TOKEN, - args: ['eip155:137/slip44:966'] - }, - { - criterion: Criterion.CHECK_SPENDING_LIMIT, - args: { - limit: '1000000000000000000', - operator: ValueOperators.GREATER_THAN, - timeWindow: { - type: 'rolling', - value: 12 * 60 * 60 - }, - filters: { - tokens: ['eip155:137/slip44:966'], - users: ['matt@narval.xyz'] - } - } - } - ] -} - -export const policies = { - policies: [examplePermitPolicy, exampleForbidPolicy] -} diff --git a/apps/policy-engine/src/shared/module/key-value/core/service/key-value.service.ts b/apps/policy-engine/src/shared/module/key-value/core/service/key-value.service.ts index eca17dc9d..96b88008e 100644 --- a/apps/policy-engine/src/shared/module/key-value/core/service/key-value.service.ts +++ b/apps/policy-engine/src/shared/module/key-value/core/service/key-value.service.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common' -import { EncryptionService } from '../../../../../encryption/core/encryption.service' import { KeyValueRepository } from '../repository/key-value.repository' /** @@ -13,10 +12,7 @@ import { KeyValueRepository } from '../repository/key-value.repository' */ @Injectable() export class KeyValueService { - constructor( - @Inject(KeyValueRepository) private keyValueRepository: KeyValueRepository, - private encryptionService: EncryptionService - ) {} + constructor(@Inject(KeyValueRepository) private keyValueRepository: KeyValueRepository) {} async get(key: string): Promise { // const encryptedValue = await this.keyValueRepository.get(key) diff --git a/packages/policy-engine-shared/src/lib/dev.fixture.ts b/packages/policy-engine-shared/src/lib/dev.fixture.ts index 5971b56c9..81876996a 100644 --- a/packages/policy-engine-shared/src/lib/dev.fixture.ts +++ b/packages/policy-engine-shared/src/lib/dev.fixture.ts @@ -1,6 +1,8 @@ import { Alg } from '@narval/signature' import { PrivateKeyAccount, sha256 } from 'viem' import { privateKeyToAccount } from 'viem/accounts' +import { Action } from './type/action.type' +import { EntityType, ValueOperators } from './type/domain.type' import { AccountClassification, AccountType, @@ -18,6 +20,7 @@ import { WalletGroupEntity, WalletGroupMemberEntity } from './type/entity.type' +import { Criterion, Policy, Then } from './type/policy.type' const PERSONAS = ['Root', 'Alice', 'Bob', 'Carol', 'Dave'] as const const GROUPS = ['Engineering', 'Treasury'] as const @@ -261,3 +264,114 @@ export const ENTITIES: Entities = { walletGroups: Object.values(WALLET_GROUP), wallets: Object.values(WALLET) } + +export const POLICIES: Policy[] = [ + { + then: Then.PERMIT, + name: 'Example of permit policy', + when: [ + { + criterion: Criterion.CHECK_RESOURCE_INTEGRITY, + args: null + }, + { + criterion: Criterion.CHECK_NONCE_EXISTS, + args: null + }, + { + criterion: Criterion.CHECK_ACTION, + args: [Action.SIGN_TRANSACTION] + }, + { + criterion: Criterion.CHECK_PRINCIPAL_ID, + args: [USER.Alice.role] + }, + { + criterion: Criterion.CHECK_WALLET_ID, + args: [WALLET.Engineering.address] + }, + { + criterion: Criterion.CHECK_INTENT_TYPE, + args: ['transferNative'] + }, + { + criterion: Criterion.CHECK_INTENT_TOKEN, + args: ['eip155:137/slip44:966'] + }, + { + criterion: Criterion.CHECK_INTENT_AMOUNT, + args: { + currency: '*', + operator: ValueOperators.LESS_THAN_OR_EQUAL, + value: '1000000000000000000' + } + }, + { + criterion: Criterion.CHECK_APPROVALS, + args: [ + { + approvalCount: 2, + countPrincipal: false, + approvalEntityType: EntityType.User, + entityIds: [USER.Bob.id, USER.Carol.id] + }, + { + approvalCount: 1, + countPrincipal: false, + approvalEntityType: EntityType.UserRole, + entityIds: [UserRole.ADMIN] + } + ] + } + ] + }, + { + then: Then.FORBID, + name: 'Example of forbid policy', + when: [ + { + criterion: Criterion.CHECK_RESOURCE_INTEGRITY, + args: null + }, + { + criterion: Criterion.CHECK_NONCE_EXISTS, + args: null + }, + { + criterion: Criterion.CHECK_ACTION, + args: [Action.SIGN_TRANSACTION] + }, + { + criterion: Criterion.CHECK_PRINCIPAL_ID, + args: [USER.Alice.id] + }, + { + criterion: Criterion.CHECK_WALLET_ID, + args: [WALLET.Engineering.address] + }, + { + criterion: Criterion.CHECK_INTENT_TYPE, + args: ['transferNative'] + }, + { + criterion: Criterion.CHECK_INTENT_TOKEN, + args: ['eip155:137/slip44:966'] + }, + { + criterion: Criterion.CHECK_SPENDING_LIMIT, + args: { + limit: '1000000000000000000', + operator: ValueOperators.GREATER_THAN, + timeWindow: { + type: 'rolling', + value: 12 * 60 * 60 + }, + filters: { + tokens: ['eip155:137/slip44:966'], + users: ['matt@narval.xyz'] + } + } + } + ] + } +] diff --git a/packages/policy-engine-shared/src/lib/schema/data-store.schema.ts b/packages/policy-engine-shared/src/lib/schema/data-store.schema.ts index 5fcb70f70..0e35e8787 100644 --- a/packages/policy-engine-shared/src/lib/schema/data-store.schema.ts +++ b/packages/policy-engine-shared/src/lib/schema/data-store.schema.ts @@ -15,11 +15,9 @@ export const jsonWebKeySchema = z.object({ d: z.string().optional().describe('(EC) Private Key') }) -export const dataStoreProtocolSchema = z.enum(['file']) - export const dataStoreConfigurationSchema = z.object({ - dataUrl: z.string(), - signatureUrl: z.string(), + dataUrl: z.string().min(1), + signatureUrl: z.string().min(1), keys: z.array(jsonWebKeySchema) }) @@ -36,7 +34,7 @@ export const entityDataSchema = z.object({ export const entitySignatureSchema = z.object({ entity: z.object({ - signature: z.string() + signature: z.string().min(1) }) }) @@ -46,6 +44,11 @@ export const entityJsonWebKeysSchema = z.object({ }) }) +export const entityStoreSchema = z.object({ + data: entitiesSchema, + signature: z.string().min(1) +}) + export const policyDataSchema = z.object({ policy: z.object({ data: z.array(policySchema) @@ -54,7 +57,7 @@ export const policyDataSchema = z.object({ export const policySignatureSchema = z.object({ policy: z.object({ - signature: z.string() + signature: z.string().min(1) }) }) @@ -63,3 +66,8 @@ export const policyJsonWebKeysSchema = z.object({ keys: z.array(jsonWebKeySchema) }) }) + +export const policyStoreSchema = z.object({ + data: z.array(policySchema), + signature: z.string().min(1) +}) diff --git a/packages/policy-engine-shared/src/lib/type/data-store.type.ts b/packages/policy-engine-shared/src/lib/type/data-store.type.ts index fb07af95a..8d37fe2e3 100644 --- a/packages/policy-engine-shared/src/lib/type/data-store.type.ts +++ b/packages/policy-engine-shared/src/lib/type/data-store.type.ts @@ -1,27 +1,27 @@ import { z } from 'zod' import { dataStoreConfigurationSchema, - dataStoreProtocolSchema, entityDataSchema, entityJsonWebKeysSchema, entitySignatureSchema, + entityStoreSchema, jsonWebKeySchema, policyDataSchema, policyJsonWebKeysSchema, - policySignatureSchema + policySignatureSchema, + policyStoreSchema } from '../schema/data-store.schema' export type JsonWebKey = z.infer -export type DataStoreProtocol = z.infer -export const DataStoreProtocol = dataStoreProtocolSchema.Enum - export type DataStoreConfiguration = z.infer export type EntityData = z.infer export type EntitySignature = z.infer export type EntityJsonWebKeySet = z.infer +export type EntityStore = z.infer export type PolicyData = z.infer export type PolicySignature = z.infer export type PolicyJsonWebKeySet = z.infer +export type PolicyStore = z.infer