From 3cbce5f7cc72f0b3708a2c07af12957a1ca8b74a Mon Sep 17 00:00:00 2001 From: Pierre Troger Date: Tue, 5 Nov 2024 16:46:51 +0100 Subject: [PATCH] versionning v1 lol --- .../engine/__test__/e2e/evaluation.spec.ts | 14 +- .../integration/data-store.service.spec.ts | 12 +- .../__test__/unit/client.service.spec.ts | 6 +- .../__test__/unit/evaluation.service.spec.ts | 4 +- .../file-system-data-store.repository.spec.ts | 4 +- .../http-data-store.repository.spec.ts | 4 +- .../__test__/unit/client.repository.spec.ts | 4 +- .../unit/open-policy-agent.engine.spec.ts | 94 ++-- .../core/open-policy-agent.engine.ts | 6 +- .../core/type/open-policy-agent.type.ts | 83 +-- .../core/type/open-policy-agent.type.v1.ts | 72 +++ .../core/type/open-policy-agent.type.v2.ts | 23 + .../__test__/unit/data-preparation.v1.spec.ts | 62 +++ .../__test__/unit/data-preparation.v2.spec.ts | 57 ++ .../__test__/unit/evaluation.util.spec.ts | 115 +--- .../unit/rego-transpiler.util.spec.ts | 4 +- .../__test__/unit/wasm-build.util.spec.ts | 6 +- .../core/util/data-preparation.v1.ts | 62 +++ .../core/util/data-preparation.v2.ts | 63 +++ .../core/util/evaluation.util.ts | 77 +-- .../src/shared/testing/evaluation.testing.ts | 36 +- packages/policy-engine-shared/src/index.ts | 12 +- .../src/lib/__test__/unit/dev.fixture.spec.ts | 10 +- .../src/lib/dev.fixture.v1.ts | 516 ++++++++++++++++++ .../lib/{dev.fixture.ts => dev.fixture.v2.ts} | 18 +- .../src/lib/schema/credential.schema.ts | 8 + .../src/lib/schema/entity.schema.shared.ts | 22 + .../{entity.schema.ts => entity.schema.v1.ts} | 48 +- .../src/lib/schema/entity.schema.v2.ts | 16 + .../{policy.schema.ts => policy.schema.v1.ts} | 2 +- .../src/lib/type/data-store.type.ts | 4 +- .../src/lib/type/domain.type.ts | 2 +- .../src/lib/type/engine.type.ts | 2 +- .../{entity.type.ts => entity.type.v1.ts} | 17 +- .../src/lib/type/entity.type.v2.ts | 6 + .../src/lib/type/policy.type.ts | 2 +- .../util/__test__/unit/entity.util.spec.ts | 288 +++++++++- .../src/lib/util/entity.util.ts | 26 +- .../src/lib/util/validation.types.ts | 10 +- .../src/lib/util/validators.v1.ts | 2 +- .../src/lib/util/validators.v2.ts | 2 +- 41 files changed, 1405 insertions(+), 416 deletions(-) create mode 100644 apps/policy-engine/src/open-policy-agent/core/type/open-policy-agent.type.v1.ts create mode 100644 apps/policy-engine/src/open-policy-agent/core/type/open-policy-agent.type.v2.ts create mode 100644 apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/data-preparation.v1.spec.ts create mode 100644 apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/data-preparation.v2.spec.ts create mode 100644 apps/policy-engine/src/open-policy-agent/core/util/data-preparation.v1.ts create mode 100644 apps/policy-engine/src/open-policy-agent/core/util/data-preparation.v2.ts create mode 100644 packages/policy-engine-shared/src/lib/dev.fixture.v1.ts rename packages/policy-engine-shared/src/lib/{dev.fixture.ts => dev.fixture.v2.ts} (97%) create mode 100644 packages/policy-engine-shared/src/lib/schema/credential.schema.ts create mode 100644 packages/policy-engine-shared/src/lib/schema/entity.schema.shared.ts rename packages/policy-engine-shared/src/lib/schema/{entity.schema.ts => entity.schema.v1.ts} (67%) create mode 100644 packages/policy-engine-shared/src/lib/schema/entity.schema.v2.ts rename packages/policy-engine-shared/src/lib/schema/{policy.schema.ts => policy.schema.v1.ts} (99%) rename packages/policy-engine-shared/src/lib/type/{entity.type.ts => entity.type.v1.ts} (79%) create mode 100644 packages/policy-engine-shared/src/lib/type/entity.type.v2.ts diff --git a/apps/policy-engine/src/engine/__test__/e2e/evaluation.spec.ts b/apps/policy-engine/src/engine/__test__/e2e/evaluation.spec.ts index 2e6da3413..81cd3c62c 100644 --- a/apps/policy-engine/src/engine/__test__/e2e/evaluation.spec.ts +++ b/apps/policy-engine/src/engine/__test__/e2e/evaluation.spec.ts @@ -7,7 +7,7 @@ import { DataStoreConfiguration, Decision, EvaluationResponse, - FIXTURE, + FIXTURE_V2, HttpSource, SerializedEvaluationRequest, SourceType, @@ -122,7 +122,7 @@ describe('Evaluation', () => { ) await clientService.savePolicyStore(client.clientId, await getPolicyStore([], privateKey)) - await clientService.saveEntityStore(client.clientId, await getEntityStore(FIXTURE.ENTITIES, privateKey)) + await clientService.saveEntityStore(client.clientId, await getEntityStore(FIXTURE_V2.ENTITIES, privateKey)) await app.init() }) @@ -231,11 +231,11 @@ describe('Evaluation', () => { message: { from: { name: 'Alice', - account: FIXTURE.VIEM_ACCOUNT.Alice.address + account: FIXTURE_V2.VIEM_ACCOUNT.Alice.address }, to: { name: 'Bob', - account: FIXTURE.VIEM_ACCOUNT.Bob.address + account: FIXTURE_V2.VIEM_ACCOUNT.Bob.address }, contents: "Dear Bob, today we're going to the moon" } @@ -247,12 +247,12 @@ describe('Evaluation', () => { transactionRequestIntent: { type: 'userOperation', entrypoint: `eip155:${sepolia.id}:${ENTRYPOINT_ADDRESS_V06.toLowerCase()}`, - from: `eip155:${sepolia.id}:${FIXTURE.VIEM_ACCOUNT.Alice.address.toLowerCase()}`, + from: `eip155:${sepolia.id}:${FIXTURE_V2.VIEM_ACCOUNT.Alice.address.toLowerCase()}`, operationIntents: [ { amount: '1', - from: `eip155:${sepolia.id}:${FIXTURE.VIEM_ACCOUNT.Alice.address.toLowerCase()}`, - to: `eip155:${sepolia.id}:${FIXTURE.VIEM_ACCOUNT.Bob.address.toLowerCase()}`, + from: `eip155:${sepolia.id}:${FIXTURE_V2.VIEM_ACCOUNT.Alice.address.toLowerCase()}`, + to: `eip155:${sepolia.id}:${FIXTURE_V2.VIEM_ACCOUNT.Bob.address.toLowerCase()}`, token: `eip155:${sepolia.id}/slip44:60`, type: 'transferNative' } diff --git a/apps/policy-engine/src/engine/core/service/__test__/integration/data-store.service.spec.ts b/apps/policy-engine/src/engine/core/service/__test__/integration/data-store.service.spec.ts index 265e2b7c5..be8e2245f 100644 --- a/apps/policy-engine/src/engine/core/service/__test__/integration/data-store.service.spec.ts +++ b/apps/policy-engine/src/engine/core/service/__test__/integration/data-store.service.spec.ts @@ -5,7 +5,7 @@ import { EntitySignature, EntityStore, EntityUtil, - FIXTURE, + FIXTURE_V2, PolicyData, PolicySignature, PolicyStore, @@ -50,8 +50,8 @@ describe(DataStoreService.name, () => { publicKey = getPublicKey(privateKey) - entityStore = await getEntityStore(FIXTURE.ENTITIES, privateKey) - policyStore = await getPolicyStore(FIXTURE.POLICIES, privateKey) + entityStore = await getEntityStore(FIXTURE_V2.ENTITIES, privateKey) + policyStore = await getPolicyStore(FIXTURE_V2.POLICIES, privateKey) entityData = { entity: { @@ -232,7 +232,7 @@ describe(DataStoreService.name, () => { }) it('throws DataStoreException when entity signature is invalid', async () => { - const entityStoreOne = await getEntityStore(FIXTURE.ENTITIES, privateKey) + const entityStoreOne = await getEntityStore(FIXTURE_V2.ENTITIES, privateKey) const entityStoreTwo = await getEntityStore(EntityUtil.emptyV2(), privateKey) await testThrowDataStoreException({ @@ -251,7 +251,7 @@ describe(DataStoreService.name, () => { }) it('throws DataStoreException when policy signature is invalid', async () => { - const policyStoreOne = await getPolicyStore(FIXTURE.POLICIES, privateKey) + const policyStoreOne = await getPolicyStore(FIXTURE_V2.POLICIES, privateKey) const policyStoreTwo = await getPolicyStore([], privateKey) await testThrowDataStoreException({ @@ -291,7 +291,7 @@ describe(DataStoreService.name, () => { }) it('returns error when signature mismatch', async () => { - const entityStoreOne = await getEntityStore(FIXTURE.ENTITIES, privateKey) + const entityStoreOne = await getEntityStore(FIXTURE_V2.ENTITIES, privateKey) const entityStoreTwo = await getEntityStore(EntityUtil.emptyV2(), privateKey) const verification = await service.verifySignature({ diff --git a/apps/policy-engine/src/engine/core/service/__test__/unit/client.service.spec.ts b/apps/policy-engine/src/engine/core/service/__test__/unit/client.service.spec.ts index 2d2557b19..5c974419b 100644 --- a/apps/policy-engine/src/engine/core/service/__test__/unit/client.service.spec.ts +++ b/apps/policy-engine/src/engine/core/service/__test__/unit/client.service.spec.ts @@ -1,6 +1,6 @@ import { EncryptionModule } from '@narval/encryption-module' import { LoggerModule, secret } from '@narval/nestjs-shared' -import { DataStoreConfiguration, FIXTURE, HttpSource, SourceType } from '@narval/policy-engine-shared' +import { DataStoreConfiguration, FIXTURE_V2, HttpSource, SourceType } from '@narval/policy-engine-shared' import { Alg, getPublicKey, privateKeyToJwk } from '@narval/signature' import { Test } from '@nestjs/testing' import { MockProxy, mock } from 'jest-mock-extended' @@ -49,11 +49,11 @@ describe(ClientService.name, () => { const stores = { entity: { - data: FIXTURE.ENTITIES, + data: FIXTURE_V2.ENTITIES, signature: 'test-signature' }, policy: { - data: FIXTURE.POLICIES, + data: FIXTURE_V2.POLICIES, signature: 'test-signature' } } diff --git a/apps/policy-engine/src/engine/core/service/__test__/unit/evaluation.service.spec.ts b/apps/policy-engine/src/engine/core/service/__test__/unit/evaluation.service.spec.ts index 92ba59653..da65dcb64 100644 --- a/apps/policy-engine/src/engine/core/service/__test__/unit/evaluation.service.spec.ts +++ b/apps/policy-engine/src/engine/core/service/__test__/unit/evaluation.service.spec.ts @@ -1,7 +1,7 @@ import { Decision, EvaluationResponse, - FIXTURE, + FIXTURE_V2, GrantPermissionAction, Request, SignTransactionAction @@ -23,7 +23,7 @@ const cnf = { const baseResponse: EvaluationResponse = { decision: Decision.PERMIT, - principal: FIXTURE.CREDENTIAL.Alice, + principal: FIXTURE_V2.CREDENTIAL.Alice, request: {} as Request } diff --git a/apps/policy-engine/src/engine/persistence/repository/__test__/integration/file-system-data-store.repository.spec.ts b/apps/policy-engine/src/engine/persistence/repository/__test__/integration/file-system-data-store.repository.spec.ts index 3d199d4c1..04f782c8c 100644 --- a/apps/policy-engine/src/engine/persistence/repository/__test__/integration/file-system-data-store.repository.spec.ts +++ b/apps/policy-engine/src/engine/persistence/repository/__test__/integration/file-system-data-store.repository.spec.ts @@ -1,4 +1,4 @@ -import { EntityData, FIXTURE, FileSource, SourceType } from '@narval/policy-engine-shared' +import { EntityData, FIXTURE_V2, FileSource, SourceType } from '@narval/policy-engine-shared' import { Test } from '@nestjs/testing' import { withTempJsonFile } from '../../../../../shared/testing/with-temp-json-file.testing' import { DataStoreException } from '../../../../core/exception/data-store.exception' @@ -9,7 +9,7 @@ describe(FileSystemDataStoreRepository.name, () => { const entityData: EntityData = { entity: { - data: FIXTURE.ENTITIES + data: FIXTURE_V2.ENTITIES } } diff --git a/apps/policy-engine/src/engine/persistence/repository/__test__/integration/http-data-store.repository.spec.ts b/apps/policy-engine/src/engine/persistence/repository/__test__/integration/http-data-store.repository.spec.ts index 2cb5d08a3..5c85a58d8 100644 --- a/apps/policy-engine/src/engine/persistence/repository/__test__/integration/http-data-store.repository.spec.ts +++ b/apps/policy-engine/src/engine/persistence/repository/__test__/integration/http-data-store.repository.spec.ts @@ -1,5 +1,5 @@ import { HttpModule, LoggerModule } from '@narval/nestjs-shared' -import { EntityData, FIXTURE, HttpSource, SourceType } from '@narval/policy-engine-shared' +import { EntityData, FIXTURE_V2, HttpSource, SourceType } from '@narval/policy-engine-shared' import { HttpStatus } from '@nestjs/common' import { Test } from '@nestjs/testing' import nock from 'nock' @@ -18,7 +18,7 @@ describe(HttpDataStoreRepository.name, () => { const entityData: EntityData = { entity: { - data: FIXTURE.ENTITIES + data: FIXTURE_V2.ENTITIES } } diff --git a/apps/policy-engine/src/engine/persistence/repository/__test__/unit/client.repository.spec.ts b/apps/policy-engine/src/engine/persistence/repository/__test__/unit/client.repository.spec.ts index 71aeea7d8..48ea8a41f 100644 --- a/apps/policy-engine/src/engine/persistence/repository/__test__/unit/client.repository.spec.ts +++ b/apps/policy-engine/src/engine/persistence/repository/__test__/unit/client.repository.spec.ts @@ -4,7 +4,7 @@ import { Criterion, DataStoreConfiguration, EntityStore, - FIXTURE, + FIXTURE_V2, HttpSource, PolicyStore, SourceType, @@ -97,7 +97,7 @@ describe(ClientRepository.name, () => { describe('saveEntityStore', () => { const store: EntityStore = { - data: FIXTURE.ENTITIES, + data: FIXTURE_V2.ENTITIES, signature: 'test-fake-signature' } diff --git a/apps/policy-engine/src/open-policy-agent/core/__test__/unit/open-policy-agent.engine.spec.ts b/apps/policy-engine/src/open-policy-agent/core/__test__/unit/open-policy-agent.engine.spec.ts index 222ae1ad7..e4924e6e1 100644 --- a/apps/policy-engine/src/open-policy-agent/core/__test__/unit/open-policy-agent.engine.spec.ts +++ b/apps/policy-engine/src/open-policy-agent/core/__test__/unit/open-policy-agent.engine.spec.ts @@ -7,7 +7,7 @@ import { Entities, EntityType, EvaluationRequest, - FIXTURE, + FIXTURE_V2, Hex, JwtString, Policy, @@ -23,7 +23,7 @@ import { Test, TestingModule } from '@nestjs/testing' import { Config, load } from '../../../../policy-engine.config' import { OpenPolicyAgentException } from '../../exception/open-policy-agent.exception' import { OpenPolicyAgentEngine } from '../../open-policy-agent.engine' -import { Result } from '../../type/open-policy-agent.type' +import { Result } from '../../type/open-policy-agent.type.v2' const ONE_ETH = toHex(BigInt('1000000000000000000')) @@ -85,19 +85,19 @@ describe('OpenPolicyAgentEngine', () => { describe('setPolicies', () => { it('sets policies', () => { - expect(engine.setPolicies(FIXTURE.POLICIES).getPolicies()).toEqual(FIXTURE.POLICIES) + expect(engine.setPolicies(FIXTURE_V2.POLICIES).getPolicies()).toEqual(FIXTURE_V2.POLICIES) }) }) describe('setEntities', () => { it('sets entities', () => { - expect(engine.setEntities(FIXTURE.ENTITIES).getEntities()).toEqual(FIXTURE.ENTITIES) + expect(engine.setEntities(FIXTURE_V2.ENTITIES).getEntities()).toEqual(FIXTURE_V2.ENTITIES) }) }) describe('load', () => { it('sets opa engine', async () => { - const e = await engine.setPolicies(FIXTURE.POLICIES).load() + const e = await engine.setPolicies(FIXTURE_V2.POLICIES).load() expect(e.getOpenPolicyAgentInstance()).toBeDefined() }) @@ -114,8 +114,8 @@ describe('OpenPolicyAgentEngine', () => { const evaluation = { request, authentication: await getJwt({ - privateKey: FIXTURE.UNSAFE_PRIVATE_KEY.Alice, - sub: FIXTURE.USER.Alice.id, + privateKey: FIXTURE_V2.UNSAFE_PRIVATE_KEY.Alice, + sub: FIXTURE_V2.USER.Alice.id, request: request as SignMessageAction }) } @@ -140,7 +140,7 @@ describe('OpenPolicyAgentEngine', () => { const e = await new OpenPolicyAgentEngine({ policies, - entities: FIXTURE.ENTITIES, + entities: FIXTURE_V2.ENTITIES, resourcePath: await getConfig('resourcePath') }).load() @@ -148,19 +148,19 @@ describe('OpenPolicyAgentEngine', () => { action: Action.SIGN_TRANSACTION, nonce: 'test-nonce', transactionRequest: { - from: FIXTURE.ACCOUNT.Engineering.address, - to: FIXTURE.ACCOUNT.Testing.address, + from: FIXTURE_V2.ACCOUNT.Engineering.address, + to: FIXTURE_V2.ACCOUNT.Testing.address, value: ONE_ETH, chainId: 1 }, - resourceId: FIXTURE.ACCOUNT.Engineering.id + resourceId: FIXTURE_V2.ACCOUNT.Engineering.id } const evaluation: EvaluationRequest = { request, authentication: await getJwt({ - privateKey: FIXTURE.UNSAFE_PRIVATE_KEY.Alice, - sub: FIXTURE.USER.Alice.id, + privateKey: FIXTURE_V2.UNSAFE_PRIVATE_KEY.Alice, + sub: FIXTURE_V2.USER.Alice.id, request }) } @@ -197,7 +197,7 @@ describe('OpenPolicyAgentEngine', () => { const e = await new OpenPolicyAgentEngine({ policies, - entities: FIXTURE.ENTITIES, + entities: FIXTURE_V2.ENTITIES, resourcePath: await getConfig('resourcePath') }).load() @@ -205,18 +205,18 @@ describe('OpenPolicyAgentEngine', () => { action: Action.SIGN_TRANSACTION, nonce: 'test-nonce', transactionRequest: { - from: FIXTURE.ACCOUNT.Engineering.address, - to: FIXTURE.ACCOUNT.Testing.address, + from: FIXTURE_V2.ACCOUNT.Engineering.address, + to: FIXTURE_V2.ACCOUNT.Testing.address, value: ONE_ETH, chainId: 1 }, - resourceId: FIXTURE.ACCOUNT.Engineering.id + resourceId: FIXTURE_V2.ACCOUNT.Engineering.id } const evaluation: EvaluationRequest = { authentication: await getJwt({ - privateKey: FIXTURE.UNSAFE_PRIVATE_KEY.Alice, - sub: FIXTURE.USER.Alice.id, + privateKey: FIXTURE_V2.UNSAFE_PRIVATE_KEY.Alice, + sub: FIXTURE_V2.USER.Alice.id, request }), request @@ -225,7 +225,7 @@ describe('OpenPolicyAgentEngine', () => { const response = await e.evaluate(evaluation) expect(response.decision).toEqual(Decision.PERMIT) - expect(response.principal).toEqual(FIXTURE.CREDENTIAL.Alice) + expect(response.principal).toEqual(FIXTURE_V2.CREDENTIAL.Alice) }) }) @@ -485,8 +485,8 @@ describe('OpenPolicyAgentEngine', () => { const evaluation: EvaluationRequest = { authentication: await getJwt({ - privateKey: FIXTURE.UNSAFE_PRIVATE_KEY.Alice, // My Credential in Entities above is Alice; - sub: FIXTURE.USER.Alice.id, + privateKey: FIXTURE_V2.UNSAFE_PRIVATE_KEY.Alice, // My Credential in Entities above is Alice; + sub: FIXTURE_V2.USER.Alice.id, request }), request @@ -518,8 +518,8 @@ describe('OpenPolicyAgentEngine', () => { const evaluation: EvaluationRequest = { authentication: await getJwt({ - privateKey: FIXTURE.UNSAFE_PRIVATE_KEY.Alice, // My Credential in Entities above is Alice; - sub: FIXTURE.USER.Alice.id, + privateKey: FIXTURE_V2.UNSAFE_PRIVATE_KEY.Alice, // My Credential in Entities above is Alice; + sub: FIXTURE_V2.USER.Alice.id, request }), request @@ -551,8 +551,8 @@ describe('OpenPolicyAgentEngine', () => { const evaluation: EvaluationRequest = { authentication: await getJwt({ - privateKey: FIXTURE.UNSAFE_PRIVATE_KEY.Alice, // My Credential in Entities above is Alice; - sub: FIXTURE.USER.Alice.id, + privateKey: FIXTURE_V2.UNSAFE_PRIVATE_KEY.Alice, // My Credential in Entities above is Alice; + sub: FIXTURE_V2.USER.Alice.id, request }), request @@ -629,7 +629,7 @@ describe('OpenPolicyAgentEngine', () => { } ] - const e = await engine.setPolicies(immutablePolicy).setEntities(FIXTURE.ENTITIES).load() + const e = await engine.setPolicies(immutablePolicy).setEntities(FIXTURE_V2.ENTITIES).load() const request = { action: Action.SIGN_TYPED_DATA, @@ -640,8 +640,8 @@ describe('OpenPolicyAgentEngine', () => { const evaluation: EvaluationRequest = { authentication: await getJwt({ - privateKey: FIXTURE.UNSAFE_PRIVATE_KEY.Alice, - sub: FIXTURE.USER.Alice.id, + privateKey: FIXTURE_V2.UNSAFE_PRIVATE_KEY.Alice, + sub: FIXTURE_V2.USER.Alice.id, request }), request @@ -649,7 +649,7 @@ describe('OpenPolicyAgentEngine', () => { const response = await e.evaluate(evaluation) expect(response.decision).toEqual(Decision.PERMIT) - expect(response.principal).toEqual(FIXTURE.CREDENTIAL.Alice) + expect(response.principal).toEqual(FIXTURE_V2.CREDENTIAL.Alice) }) }) @@ -671,7 +671,7 @@ describe('OpenPolicyAgentEngine', () => { } ] - const entities = FIXTURE.ENTITIES + const entities = FIXTURE_V2.ENTITIES it('permits a transfer of 1 wei', async () => { const e = await new OpenPolicyAgentEngine({ @@ -694,8 +694,8 @@ describe('OpenPolicyAgentEngine', () => { const evaluation: EvaluationRequest = { authentication: await getJwt({ - privateKey: FIXTURE.UNSAFE_PRIVATE_KEY.Bob, - sub: FIXTURE.USER.Bob.id, + privateKey: FIXTURE_V2.UNSAFE_PRIVATE_KEY.Bob, + sub: FIXTURE_V2.USER.Bob.id, request }), request @@ -727,8 +727,8 @@ describe('OpenPolicyAgentEngine', () => { const evaluation: EvaluationRequest = { authentication: await getJwt({ - privateKey: FIXTURE.UNSAFE_PRIVATE_KEY.Bob, - sub: FIXTURE.USER.Bob.id, + privateKey: FIXTURE_V2.UNSAFE_PRIVATE_KEY.Bob, + sub: FIXTURE_V2.USER.Bob.id, request }), request @@ -760,8 +760,8 @@ describe('OpenPolicyAgentEngine', () => { const evaluation: EvaluationRequest = { authentication: await getJwt({ - privateKey: FIXTURE.UNSAFE_PRIVATE_KEY.Bob, - sub: FIXTURE.USER.Bob.id, + privateKey: FIXTURE_V2.UNSAFE_PRIVATE_KEY.Bob, + sub: FIXTURE_V2.USER.Bob.id, request }), request @@ -793,8 +793,8 @@ describe('OpenPolicyAgentEngine', () => { const evaluation: EvaluationRequest = { authentication: await getJwt({ - privateKey: FIXTURE.UNSAFE_PRIVATE_KEY.Bob, - sub: FIXTURE.USER.Bob.id, + privateKey: FIXTURE_V2.UNSAFE_PRIVATE_KEY.Bob, + sub: FIXTURE_V2.USER.Bob.id, request }), request @@ -826,8 +826,8 @@ describe('OpenPolicyAgentEngine', () => { const evaluation: EvaluationRequest = { authentication: await getJwt({ - privateKey: FIXTURE.UNSAFE_PRIVATE_KEY.Bob, - sub: FIXTURE.USER.Bob.id, + privateKey: FIXTURE_V2.UNSAFE_PRIVATE_KEY.Bob, + sub: FIXTURE_V2.USER.Bob.id, request }), request @@ -858,8 +858,8 @@ describe('OpenPolicyAgentEngine', () => { const evaluation: EvaluationRequest = { authentication: await getJwt({ - privateKey: FIXTURE.UNSAFE_PRIVATE_KEY.Bob, - sub: FIXTURE.USER.Bob.id, + privateKey: FIXTURE_V2.UNSAFE_PRIVATE_KEY.Bob, + sub: FIXTURE_V2.USER.Bob.id, request }), request @@ -890,8 +890,8 @@ describe('OpenPolicyAgentEngine', () => { const evaluation: EvaluationRequest = { authentication: await getJwt({ - privateKey: FIXTURE.UNSAFE_PRIVATE_KEY.Bob, - sub: FIXTURE.USER.Bob.id, + privateKey: FIXTURE_V2.UNSAFE_PRIVATE_KEY.Bob, + sub: FIXTURE_V2.USER.Bob.id, request }), request @@ -924,8 +924,8 @@ describe('OpenPolicyAgentEngine', () => { const evaluation: EvaluationRequest = { authentication: await getJwt({ - privateKey: FIXTURE.UNSAFE_PRIVATE_KEY.Bob, - sub: FIXTURE.USER.Bob.id, + privateKey: FIXTURE_V2.UNSAFE_PRIVATE_KEY.Bob, + sub: FIXTURE_V2.USER.Bob.id, request }), request diff --git a/apps/policy-engine/src/open-policy-agent/core/open-policy-agent.engine.ts b/apps/policy-engine/src/open-policy-agent/core/open-policy-agent.engine.ts index 350037357..d03527350 100644 --- a/apps/policy-engine/src/open-policy-agent/core/open-policy-agent.engine.ts +++ b/apps/policy-engine/src/open-policy-agent/core/open-policy-agent.engine.ts @@ -3,7 +3,6 @@ import { CredentialEntity, Decision, Engine, - Entities, EntityUtil, EvaluationRequest, EvaluationResponse, @@ -20,10 +19,11 @@ import { z } from 'zod' import { POLICY_ENTRYPOINT } from '../open-policy-agent.constant' import { OpenPolicyAgentException } from './exception/open-policy-agent.exception' import { resultSchema } from './schema/open-policy-agent.schema' -import { OpenPolicyAgentInstance, Result } from './type/open-policy-agent.type' -import { toData, toInput } from './util/evaluation.util' +import { toInput } from './util/evaluation.util' import { getRegoRuleTemplatePath } from './util/rego-transpiler.util' import { build, getRegoCorePath } from './util/wasm-build.util' +import { Entities } from 'packages/policy-engine-shared/src/lib/schema/entity.schema.shared' +import { OpenPolicyAgentInstance } from './type/open-policy-agent.type.v1' export class OpenPolicyAgentEngine implements Engine { private policies: Policy[] diff --git a/apps/policy-engine/src/open-policy-agent/core/type/open-policy-agent.type.ts b/apps/policy-engine/src/open-policy-agent/core/type/open-policy-agent.type.ts index f712a349d..b6a3ca9ab 100644 --- a/apps/policy-engine/src/open-policy-agent/core/type/open-policy-agent.type.ts +++ b/apps/policy-engine/src/open-policy-agent/core/type/open-policy-agent.type.ts @@ -1,65 +1,32 @@ -import { - Action, - CredentialEntity, - Feed, - SerializedTransactionRequest, - SerializedUserOperationV6, - accountEntitySchema, - addressBookAccountEntitySchema, - groupEntitySchema, - tokenEntitySchema, - userEntitySchema -} from '@narval/policy-engine-shared' -import { Intent } from '@narval/transaction-request-intent' -import { loadPolicy } from '@open-policy-agent/opa-wasm' -import { z } from 'zod' -import { resultSchema } from '../schema/open-policy-agent.schema' - -type PromiseType> = T extends Promise ? U : never - -export type OpenPolicyAgentInstance = PromiseType> - -export type Input = { - action: Action - principal: CredentialEntity - resource?: { uid: string } - intent?: Intent - transactionRequest?: SerializedTransactionRequest - userOperation?: SerializedUserOperationV6 - permissions?: string[] - approvals?: CredentialEntity[] - feeds?: Feed[] +import { EntitiesV, EntityVersion } from 'packages/policy-engine-shared/src/lib/schema/entity.schema.shared'; +import { DataV1 } from './open-policy-agent.type.v1'; +import { DataV2 } from './open-policy-agent.type.v2'; +import { z } from 'zod'; + +type DataVersionMap = { + '1': DataV1; + '2': DataV2; } +type DataMapType = { + [V in EntityVersion]: z.ZodType +} + +export const dataMap: DataMapType = { + '1': DataV1, + '2': DataV2 +} as const; -// TODO: (@wcalderipe, 18/03/24) Check with @samteb how can we replace these -// types by entities defined at @narval/policy-engine-shared. -// IMPORTANT: Index entities by lower case ID is an important invariant for -// many Rego rules performing a look up on the dataset. -const Id = z.string().toLowerCase() +export const getDataSchema = (version: Version): z.ZodType => + dataMap[version]; -export const Account = accountEntitySchema.extend({ - id: Id, - assignees: z.array(Id) -}) -export type Account = z.infer +export const Data = z.union([DataV1, DataV2]); +export type Data = DataV1 | DataV2; -export const Group = groupEntitySchema.extend({ - id: Id, - users: z.array(Id), - accounts: z.array(Id) -}) -export type Group = z.infer +type DataV = DataVersionMap[Version]; -export const Data = z.object({ - entities: z.object({ - addressBook: z.record(Id, addressBookAccountEntitySchema.extend({ id: Id })), - tokens: z.record(Id, tokenEntitySchema.extend({ id: Id })), - users: z.record(Id, userEntitySchema.extend({ id: Id })), - groups: z.record(Id, Group), - accounts: z.record(Id, Account) - }) -}) -export type Data = z.infer +export type DataTransformer = (entities: EntitiesV) => DataV; -export type Result = z.infer +export type RequiredDataTransformer = Required<{ + [V in EntityVersion]: DataTransformer +}>; diff --git a/apps/policy-engine/src/open-policy-agent/core/type/open-policy-agent.type.v1.ts b/apps/policy-engine/src/open-policy-agent/core/type/open-policy-agent.type.v1.ts new file mode 100644 index 000000000..eeab94fe3 --- /dev/null +++ b/apps/policy-engine/src/open-policy-agent/core/type/open-policy-agent.type.v1.ts @@ -0,0 +1,72 @@ +import { + Action, + CredentialEntity, + Feed, + SerializedTransactionRequest, + SerializedUserOperationV6, + accountEntitySchema, + accountGroupEntitySchema, + addressBookAccountEntitySchema, + tokenEntitySchema, + userEntitySchema, + userGroupEntitySchema +} from '@narval/policy-engine-shared' +import { Intent } from '@narval/transaction-request-intent' +import { loadPolicy } from '@open-policy-agent/opa-wasm' +import { z } from 'zod' +import { resultSchema } from '../schema/open-policy-agent.schema' + +type PromiseType> = T extends Promise ? U : never + +export type OpenPolicyAgentInstance = PromiseType> + +export type Input = { + action: Action + principal: CredentialEntity + resource?: { uid: string } + intent?: Intent + transactionRequest?: SerializedTransactionRequest + userOperation?: SerializedUserOperationV6 + permissions?: string[] + approvals?: CredentialEntity[] + feeds?: Feed[] +} + +// TODO: (@wcalderipe, 18/03/24) Check with @samteb how can we replace these +// types by entities defined at @narval/policy-engine-shared. + +// IMPORTANT: Index entities by lower case ID is an important invariant for +// many Rego rules performing a look up on the dataset. +export const Id = z.string().toLowerCase() + +export const UserGroup = userGroupEntitySchema.extend({ + id: Id, + users: z.array(Id) +}) +export type UserGroup = z.infer + +export const Account = accountEntitySchema.extend({ + id: Id, + assignees: z.array(Id) +}) +export type Account = z.infer + +export const AccountGroup = accountGroupEntitySchema.extend({ + id: Id, + accounts: z.array(Id) +}) +export type AccountGroup = z.infer + +export const DataV1 = z.object({ + entities: z.object({ + addressBook: z.record(Id, addressBookAccountEntitySchema.extend({ id: Id })), + tokens: z.record(Id, tokenEntitySchema.extend({ id: Id })), + users: z.record(Id, userEntitySchema.extend({ id: Id })), + accountGroups: z.record(Id, AccountGroup), + userGroups: z.record(Id, UserGroup), + accounts: z.record(Id, Account) + }) +}) +export type DataV1 = z.infer + +export type Result = z.infer \ No newline at end of file diff --git a/apps/policy-engine/src/open-policy-agent/core/type/open-policy-agent.type.v2.ts b/apps/policy-engine/src/open-policy-agent/core/type/open-policy-agent.type.v2.ts new file mode 100644 index 000000000..2c20d21cf --- /dev/null +++ b/apps/policy-engine/src/open-policy-agent/core/type/open-policy-agent.type.v2.ts @@ -0,0 +1,23 @@ +import { + groupEntitySchema, +} from '@narval/policy-engine-shared' +import { z } from 'zod' +import { DataV1, Id } from './open-policy-agent.type.v1' + + +export const Group = groupEntitySchema.extend({ + id: Id, + users: z.array(Id), + accounts: z.array(Id) +}) +export type Group = z.infer + +export const DataV2 = DataV1.extend({ + entities: DataV1.shape.entities.omit({ + userGroups: true, + accountGroups: true + }).extend({ + groups: z.record(Group) + }) +}) +export type DataV2 = z.infer \ No newline at end of file diff --git a/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/data-preparation.v1.spec.ts b/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/data-preparation.v1.spec.ts new file mode 100644 index 000000000..75df8a834 --- /dev/null +++ b/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/data-preparation.v1.spec.ts @@ -0,0 +1,62 @@ +import { FIXTURE_V1 } from '@narval/policy-engine-shared'; +import { toData } from '../../data-preparation.v1'; +describe('toData', () => { + describe('entities', () => { + const lowerCaseId = (value: T) => ({ ...value, id: value.id.toLowerCase() }) + + it('indexes address book accounts by lower case id', () => { + const { entities } = toData(FIXTURE_V1.ENTITIES) + const firstAccount = FIXTURE_V1.ADDRESS_BOOK[0] + + expect(entities.addressBook[firstAccount.id.toLowerCase()]).toEqual(lowerCaseId(firstAccount)) + }) + + it('indexes tokens by lower case id', () => { + const { entities } = toData(FIXTURE_V1.ENTITIES) + const usdc = FIXTURE_V1.TOKEN.usdc1 + + expect(entities.tokens[usdc.id.toLowerCase()]).toEqual(usdc) + }) + + it('indexes users by lower case id', () => { + const { entities } = toData(FIXTURE_V1.ENTITIES) + const alice = FIXTURE_V1.USER.Alice + + expect(entities.users[alice.id.toLowerCase()]).toEqual(alice) + }) + + it('indexes accounts by lower case id', () => { + const { entities } = toData(FIXTURE_V1.ENTITIES) + const account = FIXTURE_V1.ACCOUNT.Testing + + expect(entities.accounts[account.id.toLowerCase()]).toEqual({ + ...lowerCaseId(account), + assignees: ['test-alice-user-uid'] + }) + }) + + it('indexes user groups with members by lower case id', () => { + const { entities } = toData(FIXTURE_V1.ENTITIES) + const group = FIXTURE_V1.USER_GROUP.Engineering + + expect(entities.userGroups[group.id.toLowerCase()]).toEqual({ + id: group.id.toLowerCase(), + users: FIXTURE_V1.USER_GROUP_MEMBER.filter(({ groupId }) => groupId === group.id).map(({ userId }) => + userId.toLowerCase() + ) + }) + }) + + it('indexes account groups with members by lower case id', () => { + const { entities } = toData(FIXTURE_V1.ENTITIES) + const group = FIXTURE_V1.ACCOUNT_GROUP.Treasury + + expect(entities.accountGroups[group.id.toLowerCase()]).toEqual({ + id: group.id.toLowerCase(), + accounts: FIXTURE_V1.ACCOUNT_GROUP_MEMBER.filter(({ groupId }) => groupId === group.id).map(({ accountId }) => + accountId.toLowerCase() + ) + }) + }) + }) +}) \ No newline at end of file diff --git a/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/data-preparation.v2.spec.ts b/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/data-preparation.v2.spec.ts new file mode 100644 index 000000000..8e8c29e7a --- /dev/null +++ b/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/data-preparation.v2.spec.ts @@ -0,0 +1,57 @@ +import { + FIXTURE_V2, +} from '@narval/policy-engine-shared' +import { EntitiesV } from 'packages/policy-engine-shared/src/lib/schema/entity.schema.shared' +import { toData } from '../../data-preparation.v2'; + +describe('toData', () => { + describe('entities', () => { + const lowerCaseId = (value: T) => ({ ...value, id: value.id.toLowerCase() }) + + it('indexes address book accounts by lower case id', () => { + const { entities } = toData(FIXTURE_V2.ENTITIES) + const firstAccount = FIXTURE_V2.ADDRESS_BOOK[0] + + expect(entities.addressBook[firstAccount.id.toLowerCase()]).toEqual(lowerCaseId(firstAccount)) + }) + + it('indexes tokens by lower case id', () => { + const { entities } = toData(FIXTURE_V2.ENTITIES) + const usdc = FIXTURE_V2.TOKEN.usdc1 + + expect(entities.tokens[usdc.id.toLowerCase()]).toEqual(usdc) + }) + + it('indexes users by lower case id', () => { + const { entities } = toData(FIXTURE_V2.ENTITIES) + const alice = FIXTURE_V2.USER.Alice + + expect(entities.users[alice.id.toLowerCase()]).toEqual(alice) + }) + + it('indexes accounts by lower case id', () => { + const { entities } = toData(FIXTURE_V2.ENTITIES) + const account = FIXTURE_V2.ACCOUNT.Testing + + expect(entities.accounts[account.id.toLowerCase()]).toEqual({ + ...lowerCaseId(account), + assignees: ['test-alice-user-uid'] + }) + }) + + it('indexes groups with members by lower case id', () => { + const { entities } = toData(FIXTURE_V2.ENTITIES) + const group = FIXTURE_V2.GROUP.Engineering + + expect(entities.groups[group.id.toLowerCase()]).toEqual({ + id: group.id.toLowerCase(), + users: FIXTURE_V2.USER_GROUP_MEMBER.filter(({ groupId }) => groupId === group.id).map(({ userId }) => + userId.toLowerCase() + ), + accounts: FIXTURE_V2.ACCOUNT_GROUP_MEMBER.filter(({ groupId }) => groupId === group.id).map(({ accountId }) => + accountId.toLowerCase() + ) + }) + }) + }) +}) \ No newline at end of file diff --git a/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/evaluation.util.spec.ts b/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/evaluation.util.spec.ts index 9db43960e..5c9498d94 100644 --- a/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/evaluation.util.spec.ts +++ b/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/evaluation.util.spec.ts @@ -1,8 +1,7 @@ import { Action, - EntitiesV, EvaluationRequest, - FIXTURE, + FIXTURE_V2, GrantPermissionAction, SerializedUserOperationV6, SignMessageAction, @@ -21,11 +20,11 @@ import { generateSignUserOperationRequest } from '../../../../../shared/testing/evaluation.testing' import { OpenPolicyAgentException } from '../../../exception/open-policy-agent.exception' -import { toData, toInput } from '../../evaluation.util' +import { toInput } from '../../evaluation.util' describe('toInput', () => { - const principal = FIXTURE.CREDENTIAL.Alice - const approvals = [FIXTURE.CREDENTIAL.Alice, FIXTURE.CREDENTIAL.Bob, FIXTURE.CREDENTIAL.Carol] + const principal = FIXTURE_V2.CREDENTIAL.Alice + const approvals = [FIXTURE_V2.CREDENTIAL.Alice, FIXTURE_V2.CREDENTIAL.Bob, FIXTURE_V2.CREDENTIAL.Carol] it('throws OpenPolicyAgentException when action is unsupported', async () => { const evaluation = await generateSignTransactionRequest() @@ -92,7 +91,7 @@ describe('toInput', () => { const principal = { id: 'NoTLowerCasedId', userId: 'NotLOWECasedId', - key: FIXTURE.CREDENTIAL.Alice.key + key: FIXTURE_V2.CREDENTIAL.Alice.key } const input = toInput({ evaluation, principal, approvals }) @@ -312,107 +311,3 @@ describe('toInput', () => { }) }) -describe('toData', () => { - describe('entities', () => { - const lowerCaseId = (value: T) => ({ ...value, id: value.id.toLowerCase() }) - - it('indexes address book accounts by lower case id', () => { - const { entities } = toData(FIXTURE.ENTITIES) - const firstAccount = FIXTURE.ADDRESS_BOOK[0] - - expect(entities.addressBook[firstAccount.id.toLowerCase()]).toEqual(lowerCaseId(firstAccount)) - }) - - it('indexes tokens by lower case id', () => { - const { entities } = toData(FIXTURE.ENTITIES) - const usdc = FIXTURE.TOKEN.usdc1 - - expect(entities.tokens[usdc.id.toLowerCase()]).toEqual(usdc) - }) - - it('indexes users by lower case id', () => { - const { entities } = toData(FIXTURE.ENTITIES) - const alice = FIXTURE.USER.Alice - - expect(entities.users[alice.id.toLowerCase()]).toEqual(alice) - }) - - it('indexes accounts by lower case id', () => { - const { entities } = toData(FIXTURE.ENTITIES) - const account = FIXTURE.ACCOUNT.Testing - - expect(entities.accounts[account.id.toLowerCase()]).toEqual({ - ...lowerCaseId(account), - assignees: ['test-alice-user-uid'] - }) - }) - - it('indexes groups with members by lower case id', () => { - const { entities } = toData(FIXTURE.ENTITIES) - const group = FIXTURE.GROUP.Engineering - - expect(entities.groups[group.id.toLowerCase()]).toEqual({ - id: group.id.toLowerCase(), - users: FIXTURE.USER_GROUP_MEMBER.filter(({ groupId }) => groupId === group.id).map(({ userId }) => - userId.toLowerCase() - ), - accounts: FIXTURE.ACCOUNT_GROUP_MEMBER.filter(({ groupId }) => groupId === group.id).map(({ accountId }) => - accountId.toLowerCase() - ) - }) - }) - - it('indexes groups with members from deprecated schema by lower case id', () => { - const { groups: _groups, version: _version, ...otherEntities } = FIXTURE.ENTITIES - const deprecatedEntities: EntitiesV<'1'> = { - ...otherEntities, - accountGroups: [{ id: 'test-engineering-account-group-uid' }, { id: 'test-treasury-account-group-uid' }], - userGroups: [{ id: 'test-engineering-user-group-uid' }, { id: 'test-treasury-user-group-uid' }], - accountGroupMembers: [ - { - accountId: FIXTURE.ACCOUNT.Engineering.id, - groupId: 'test-engineering-account-group-uid' - }, - { - accountId: FIXTURE.ACCOUNT.Treasury.id, - groupId: 'test-treasury-account-group-uid' - } - ], - userGroupMembers: [ - { - userId: FIXTURE.USER.Alice.id, - groupId: 'test-engineering-user-group-uid' - }, - { - userId: FIXTURE.USER.Bob.id, - groupId: 'test-treasury-user-group-uid' - } - ] - } - - const { entities } = toData(deprecatedEntities) - expect(entities.groups).toEqual({ - 'test-engineering-user-group-uid': { - id: 'test-engineering-user-group-uid', - users: [FIXTURE.USER.Alice.id], - accounts: [] - }, - 'test-treasury-user-group-uid': { - id: 'test-treasury-user-group-uid', - users: [FIXTURE.USER.Bob.id], - accounts: [] - }, - 'test-engineering-account-group-uid': { - id: 'test-engineering-account-group-uid', - users: [], - accounts: [FIXTURE.ACCOUNT.Engineering.id.toLowerCase()] - }, - 'test-treasury-account-group-uid': { - id: 'test-treasury-account-group-uid', - users: [], - accounts: [FIXTURE.ACCOUNT.Treasury.id.toLowerCase()] - } - }) - }) - }) -}) diff --git a/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/rego-transpiler.util.spec.ts b/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/rego-transpiler.util.spec.ts index 281778dbd..18ad27cdb 100644 --- a/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/rego-transpiler.util.spec.ts +++ b/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/rego-transpiler.util.spec.ts @@ -5,7 +5,7 @@ import { Criterion, ERC1155TransfersCriterion, EntityType, - FIXTURE, + FIXTURE_V2, IntentAmountCriterion, NonceRequiredCriterion, Then, @@ -30,7 +30,7 @@ const getTemplatePath = async () => getRegoRuleTemplatePath(await getConfig('res describe('transpile', () => { it('transpiles rego rules based on the given policies', async () => { - const rules = await transpile(FIXTURE.POLICIES, await getTemplatePath()) + const rules = await transpile(FIXTURE_V2.POLICIES, await getTemplatePath()) expect(rules).toContain('permit') }) diff --git a/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/wasm-build.util.spec.ts b/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/wasm-build.util.spec.ts index 5421c606f..11619a32b 100644 --- a/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/wasm-build.util.spec.ts +++ b/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/wasm-build.util.spec.ts @@ -1,5 +1,5 @@ import { ConfigModule, ConfigService } from '@narval/config-module' -import { FIXTURE } from '@narval/policy-engine-shared' +import { FIXTURE_V2 } from '@narval/policy-engine-shared' import { Path, PathValue } from '@nestjs/config' import { Test, TestingModule } from '@nestjs/testing' import { loadPolicy } from '@open-policy-agent/opa-wasm' @@ -48,7 +48,7 @@ describe('writeRegoPolicies', () => { it('writes a file with transpiled rego policies', async () => { await withTempDirectory(async (path) => { const { file } = await writeRegoPolicies({ - policies: FIXTURE.POLICIES, + policies: FIXTURE_V2.POLICIES, filename: 'policies.rego', path, regoRuleTemplatePath: await getTemplatePath() @@ -140,7 +140,7 @@ describe('build', () => { path, regoCorePath: await getCorePath(), regoRuleTemplatePath: await getTemplatePath(), - policies: FIXTURE.POLICIES, + policies: FIXTURE_V2.POLICIES, cleanAfter: false }) diff --git a/apps/policy-engine/src/open-policy-agent/core/util/data-preparation.v1.ts b/apps/policy-engine/src/open-policy-agent/core/util/data-preparation.v1.ts new file mode 100644 index 000000000..5bdc2f43c --- /dev/null +++ b/apps/policy-engine/src/open-policy-agent/core/util/data-preparation.v1.ts @@ -0,0 +1,62 @@ +import { indexBy } from 'lodash/fp' +import { Account, AccountGroup, DataV1, UserGroup, } from '../type/open-policy-agent.type.v1' +import { Entities } from 'packages/policy-engine-shared/src/lib/schema/entity.schema.shared' + +export const toDataV1 = (entities: Entities): DataV1 => { + const userGroups = entities.userGroupMembers.reduce((groups, { userId, groupId }) => { + const id = groupId.toLowerCase() + const group = groups.get(id) + + if (group) { + return groups.set(id, { + id: groupId, + users: group.users.concat(userId) + }) + } else { + return groups.set(groupId, { id: groupId, users: [userId] }) + } + }, new Map()) + + const accountAssignees = entities.userAccounts.reduce((assignees, { userId, accountId }) => { + const account = assignees.get(accountId) + + if (account) { + return assignees.set(accountId, account.concat(userId)) + } else { + return assignees.set(accountId, [userId]) + } + }, new Map()) + + const accounts: Account[] = entities.accounts.map((account) => ({ + ...account, + assignees: accountAssignees.get(account.id) || [] + })) + + const accountGroups = entities.accountGroupMembers.reduce((groups, { accountId, groupId }) => { + const group = groups.get(groupId) + + if (group) { + return groups.set(groupId, { + id: groupId, + accounts: group.accounts.concat(accountId) + }) + } else { + return groups.set(groupId, { id: groupId, accounts: [accountId] }) + } + }, new Map()) + + const data: DataV1 = { + entities: { + addressBook: indexBy('id', entities.addressBook), + tokens: indexBy('id', entities.tokens), + users: indexBy('id', entities.users), + userGroups: Object.fromEntries(userGroups), + accounts: indexBy('id', accounts), + accountGroups: Object.fromEntries(accountGroups) + } + } + + // IMPORTANT: The Data schema converts IDs to lower case because we don't + // want to be doing defensive programming in Rego. + return DataV1.parse(data) +} \ No newline at end of file diff --git a/apps/policy-engine/src/open-policy-agent/core/util/data-preparation.v2.ts b/apps/policy-engine/src/open-policy-agent/core/util/data-preparation.v2.ts new file mode 100644 index 000000000..c86c090f5 --- /dev/null +++ b/apps/policy-engine/src/open-policy-agent/core/util/data-preparation.v2.ts @@ -0,0 +1,63 @@ +import { indexBy } from 'lodash/fp' +import { DataV2, Group } from '../type/open-policy-agent.type.v2' +import { Entities } from 'packages/policy-engine-shared/src/lib/schema/entity.schema.shared' +import { Account } from '../type/open-policy-agent.type.v1' + +export const toDataV2 = (entities: Entities): DataV2 => { + const groups = new Map() + + // Process user group members + entities.userGroupMembers.forEach(({ userId, groupId }) => { + const id = groupId.toLowerCase() + const group = groups.get(id) || { + id: groupId, + users: [], + accounts: [] + } + + group.users.push(userId) + groups.set(id, group) + }) + + // Process account group members + entities.accountGroupMembers.forEach(({ accountId, groupId }) => { + const id = groupId.toLowerCase() + const group = groups.get(id) || { + id: groupId, + users: [], + accounts: [] + } + + group.accounts.push(accountId) + groups.set(id, group) + }) + + const accountAssignees = entities.userAccounts.reduce((assignees, { userId, accountId }) => { + const account = assignees.get(accountId) + + if (account) { + return assignees.set(accountId, account.concat(userId)) + } else { + return assignees.set(accountId, [userId]) + } + }, new Map()) + + const accounts: Account[] = entities.accounts.map((account) => ({ + ...account, + assignees: accountAssignees.get(account.id) || [] + })) + + const data: DataV2 = { + entities: { + addressBook: indexBy('id', entities.addressBook), + tokens: indexBy('id', entities.tokens), + users: indexBy('id', entities.users), + groups: Object.fromEntries(groups), + accounts: indexBy('id', accounts) + } + } + + // IMPORTANT: The Data schema converts IDs to lower case because we don't + // want to be doing defensive programming in Rego. + return DataV2.parse(data) +} \ No newline at end of file diff --git a/apps/policy-engine/src/open-policy-agent/core/util/evaluation.util.ts b/apps/policy-engine/src/open-policy-agent/core/util/evaluation.util.ts index 72c6a2f13..5b36f4f92 100644 --- a/apps/policy-engine/src/open-policy-agent/core/util/evaluation.util.ts +++ b/apps/policy-engine/src/open-policy-agent/core/util/evaluation.util.ts @@ -1,7 +1,6 @@ import { Action, CredentialEntity, - Entities, EvaluationRequest, Feed, GrantPermissionAction, @@ -16,9 +15,13 @@ import { } from '@narval/policy-engine-shared' import { InputType, safeDecode } from '@narval/transaction-request-intent' import { HttpStatus } from '@nestjs/common' -import { indexBy } from 'lodash/fp' import { OpenPolicyAgentException } from '../exception/open-policy-agent.exception' -import { Account, Data, Group, Input } from '../type/open-policy-agent.type' +import { Input } from '../type/open-policy-agent.type.v1' +import { Entities, EntityVersion } from 'packages/policy-engine-shared/src/lib/schema/entity.schema.shared' +import { Data, RequiredDataTransformer } from '../type/open-policy-agent.type' +import { findEntityVersion } from 'packages/policy-engine-shared/src/lib/util/entity.util' +import { toDataV1 } from './data-preparation.v1' +import { toDataV2 } from './data-preparation.v2' type Mapping = ( request: R, @@ -191,61 +194,17 @@ export const toInput = (params: { }) } -export const toData = (entities: Entities): Data => { - const groups = new Map() - - // Process user group members - entities.userGroupMembers.forEach(({ userId, groupId }) => { - const id = groupId.toLowerCase() - const group = groups.get(id) || { - id: groupId, - users: [], - accounts: [] - } - - group.users.push(userId) - groups.set(id, group) - }) - - // Process account group members - entities.accountGroupMembers.forEach(({ accountId, groupId }) => { - const id = groupId.toLowerCase() - const group = groups.get(id) || { - id: groupId, - users: [], - accounts: [] - } - - group.accounts.push(accountId) - groups.set(id, group) - }) - - const accountAssignees = entities.userAccounts.reduce((assignees, { userId, accountId }) => { - const account = assignees.get(accountId) +export const DATA_TRANSFORMER: RequiredDataTransformer = { + '1': toDataV1, + '2': toDataV2 +} - if (account) { - return assignees.set(accountId, account.concat(userId)) - } else { - return assignees.set(accountId, [userId]) - } - }, new Map()) - - const accounts: Account[] = entities.accounts.map((account) => ({ - ...account, - assignees: accountAssignees.get(account.id) || [] - })) - - const data: Data = { - entities: { - addressBook: indexBy('id', entities.addressBook), - tokens: indexBy('id', entities.tokens), - users: indexBy('id', entities.users), - groups: Object.fromEntries(groups), - accounts: indexBy('id', accounts) - } +export const toData = (entities: Entities): Data => { + if (entities.version === '1') { + return DATA_TRANSFORMER['1'](entities) + } else if (entities.version === '2') { + return DATA_TRANSFORMER['2'](entities) + } else { + return DATA_TRANSFORMER['1'](entities) } - - // IMPORTANT: The Data schema converts IDs to lower case because we don't - // want to be doing defensive programming in Rego. - return Data.parse(data) -} +} \ No newline at end of file diff --git a/apps/policy-engine/src/shared/testing/evaluation.testing.ts b/apps/policy-engine/src/shared/testing/evaluation.testing.ts index b0e21de2d..a27fd247c 100644 --- a/apps/policy-engine/src/shared/testing/evaluation.testing.ts +++ b/apps/policy-engine/src/shared/testing/evaluation.testing.ts @@ -1,5 +1,5 @@ import { Permission, Resource, resourceId } from '@narval/armory-sdk' -import { Action, EvaluationRequest, FIXTURE, Request, TransactionRequest } from '@narval/policy-engine-shared' +import { Action, EvaluationRequest, FIXTURE_V2, Request, TransactionRequest } from '@narval/policy-engine-shared' import { Alg, Payload, hash, privateKeyToJwk, signJwt } from '@narval/signature' import { randomBytes } from 'crypto' import { ENTRYPOINT_ADDRESS_V06 } from 'permissionless' @@ -15,17 +15,17 @@ const sign = async (request: Request) => { requestHash: message } - const aliceSignature = await signJwt(payload, privateKeyToJwk(FIXTURE.UNSAFE_PRIVATE_KEY.Alice, Alg.ES256K)) - const bobSignature = await signJwt(payload, privateKeyToJwk(FIXTURE.UNSAFE_PRIVATE_KEY.Bob, Alg.ES256K)) - const carolSignature = await signJwt(payload, privateKeyToJwk(FIXTURE.UNSAFE_PRIVATE_KEY.Carol, Alg.ES256K)) + const aliceSignature = await signJwt(payload, privateKeyToJwk(FIXTURE_V2.UNSAFE_PRIVATE_KEY.Alice, Alg.ES256K)) + const bobSignature = await signJwt(payload, privateKeyToJwk(FIXTURE_V2.UNSAFE_PRIVATE_KEY.Bob, Alg.ES256K)) + const carolSignature = await signJwt(payload, privateKeyToJwk(FIXTURE_V2.UNSAFE_PRIVATE_KEY.Carol, Alg.ES256K)) return { aliceSignature, bobSignature, carolSignature } } export const generateSignTransactionRequestWithGas = async (): Promise => { const txRequest: TransactionRequest = { - from: FIXTURE.ACCOUNT.Engineering.address, - to: FIXTURE.ACCOUNT.Treasury.address, + from: FIXTURE_V2.ACCOUNT.Engineering.address, + to: FIXTURE_V2.ACCOUNT.Treasury.address, chainId: 137, value: toHex(ONE_ETH), data: '0x00000000', @@ -38,7 +38,7 @@ export const generateSignTransactionRequestWithGas = async (): Promise => { const txRequest: TransactionRequest = { - from: FIXTURE.ACCOUNT.Engineering.address, - to: FIXTURE.ACCOUNT.Treasury.address, + from: FIXTURE_V2.ACCOUNT.Engineering.address, + to: FIXTURE_V2.ACCOUNT.Treasury.address, chainId: 137, value: toHex(ONE_ETH), data: '0x00000000', @@ -65,7 +65,7 @@ export const generateSignTransactionRequest = async (): Promise = const request: Request = { action: Action.SIGN_MESSAGE, nonce: uuid(), - resourceId: FIXTURE.ACCOUNT.Engineering.id, + resourceId: FIXTURE_V2.ACCOUNT.Engineering.id, message: 'generated sign message request' } @@ -98,7 +98,7 @@ export const generateSignRawRequest = async (): Promise => { const request: Request = { action: Action.SIGN_RAW, nonce: uuid(), - resourceId: FIXTURE.ACCOUNT.Engineering.id, + resourceId: FIXTURE_V2.ACCOUNT.Engineering.id, rawMessage: toHex(randomBytes(42)) } @@ -115,7 +115,7 @@ export const generateSignTypedDataRequest = async (): Promise const request: Request = { action: Action.SIGN_TYPED_DATA, nonce: uuid(), - resourceId: FIXTURE.ACCOUNT.Engineering.id, + resourceId: FIXTURE_V2.ACCOUNT.Engineering.id, typedData: { domain: { name: 'Ether Mail', @@ -138,11 +138,11 @@ export const generateSignTypedDataRequest = async (): Promise message: { from: { name: 'Alice', - account: FIXTURE.VIEM_ACCOUNT.Alice.address + account: FIXTURE_V2.VIEM_ACCOUNT.Alice.address }, to: { name: 'Bob', - account: FIXTURE.VIEM_ACCOUNT.Bob.address + account: FIXTURE_V2.VIEM_ACCOUNT.Bob.address }, contents: "Dear Bob, today we're going to the moon" } @@ -179,13 +179,13 @@ export const generateSignUserOperationRequest = async (): Promise { - it('defines valid entities', () => { - expect(validate(ENTITIES)).toEqual({ success: true }) + it('defines valid entities for v1', () => { + expect(validate(ENTITIESV2)).toEqual({ success: true }) + }) + + it('defines valid entities for v2', () => { + expect(validate(ENTITIESV2)).toEqual({ success: true }) }) }) diff --git a/packages/policy-engine-shared/src/lib/dev.fixture.v1.ts b/packages/policy-engine-shared/src/lib/dev.fixture.v1.ts new file mode 100644 index 000000000..4a9df223b --- /dev/null +++ b/packages/policy-engine-shared/src/lib/dev.fixture.v1.ts @@ -0,0 +1,516 @@ +import { + Alg, + Curves, + Hex, + KeyTypes, + Secp256k1PublicKey, + SigningAlg, + privateKeyToJwk, + secp256k1PublicKeySchema +} from '@narval/signature' +import { PrivateKeyAccount } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { Action } from './type/action.type' +import { EntityType, ValueOperators } from './type/domain.type' +import { + AccountClassification, + AccountEntity, + AccountGroupEntity, + AccountGroupMemberEntity, + AccountType, + AddressBookAccountEntity, + ClientEntity, + CredentialEntity, + TokenEntity, + UserAccountEntity, + UserEntity, + UserGroupEntity, + UserGroupMemberEntity, + UserRole +} from './type/entity.type.v1' +import { Criterion, Policy, Then } from './type/policy.type' +import { EntitiesV } from './schema/entity.schema.shared' + +const PERSONAS = ['Root', 'Alice', 'Bob', 'Carol', 'Dave', 'Eric', 'SystemManager'] as const +const GROUPS = ['Engineering', 'Treasury'] as const +const ACCOUNTS_NAME = ['Engineering', 'Testing', 'Treasury', 'Operation'] as const + +type Personas = (typeof PERSONAS)[number] +type Groups = (typeof GROUPS)[number] +type Accounts = (typeof ACCOUNTS_NAME)[number] + +export const CLIENT: ClientEntity = { + id: '7d704a62-d15e-4382-a826-1eb41563043b' +} + +export const AddressBookAddresses = { + External: '0x1118ee1cbaa1856f4550c6fc24abb16c5c9b2a43' as Hex, + Internal: '0x2227be636c3ad8cf9d08ba8bdba4abd2ef29bd23' as Hex, + CounterParty: '0x3331472fce4ec74a1e3f9653776acfc790cd0743' as Hex +} + +export const UNSAFE_PRIVATE_KEY: Record = { + // 0x000c0d191308a336356bee3813cc17f6868972c4 + Root: '0xa95b097938cc1d1a800d2b10d2a175f979613c940868460fd66830059fc1e418', + // 0xaaa8ee1cbaa1856f4550c6fc24abb16c5c9b2a43 + Alice: '0x454c9f13f6591f6482b17bdb6a671a7294500c7dd126111ce1643b03b6aeb354', + // 0xbbb7be636c3ad8cf9d08ba8bdba4abd2ef29bd23 + Bob: '0x569a6614716a76fdb9cf21b842d012add85e680b51fd4fb773109a93c6c4f307', + // 0xccc1472fce4ec74a1e3f9653776acfc790cd0743 + Carol: '0x33be709d0e3ffcd9ffa3d983d3fe3a55c34ab4eb4db2577847667262094f1786', + // 0xddd26a02e7c54e8dc373b9d2dcb309ecdeca815d + Dave: '0x82a0cf4f0fdfd42d93ff328b73bfdbc9c8b4f95f5aedfae82059753fc08a180f', + // 0xeee3b0b3b4b7b8b9babbbcbdbebfc0c1c2c3c4c5 + Eric: '0x3e4989d1d83959d9dbec1f14bfb0685cfd15f4fd5037dc6e37e88e01838aef65', + // 0x0xfffFA973C351Df1BE703dA81b0C6BE08Abc51500 + SystemManager: '0xa05bf30ec3423e414b16ba737cab1f17b2bf938133007ceb5b4cf55a20eea36a' +} + +export const PUBLIC_KEYS_JWK: Record = { + Root: secp256k1PublicKeySchema.parse(privateKeyToJwk(UNSAFE_PRIVATE_KEY.Root, Alg.ES256K)), + Alice: secp256k1PublicKeySchema.parse(privateKeyToJwk(UNSAFE_PRIVATE_KEY.Alice, Alg.ES256K)), + Bob: secp256k1PublicKeySchema.parse(privateKeyToJwk(UNSAFE_PRIVATE_KEY.Bob, Alg.ES256K)), + Carol: secp256k1PublicKeySchema.parse(privateKeyToJwk(UNSAFE_PRIVATE_KEY.Carol, Alg.ES256K)), + Dave: secp256k1PublicKeySchema.parse(privateKeyToJwk(UNSAFE_PRIVATE_KEY.Dave, Alg.ES256K)), + Eric: secp256k1PublicKeySchema.parse(privateKeyToJwk(UNSAFE_PRIVATE_KEY.Eric, Alg.ES256K)), + SystemManager: secp256k1PublicKeySchema.parse(privateKeyToJwk(UNSAFE_PRIVATE_KEY.SystemManager, Alg.ES256K)) +} + +export const VIEM_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), + Eric: privateKeyToAccount(UNSAFE_PRIVATE_KEY.Eric), + SystemManager: privateKeyToAccount(UNSAFE_PRIVATE_KEY.SystemManager) +} + +export const USER: Record = { + Root: { + id: 'test-root-user-uid', + role: UserRole.ROOT + }, + Alice: { + id: 'test-alice-user-uid', + role: UserRole.ADMIN + }, + Bob: { + id: 'test-bob-user-uid', + role: UserRole.ADMIN + }, + Carol: { + id: 'test-carol-user-uid', + role: UserRole.MANAGER + }, + Dave: { + id: 'test-dave-user-uid', + role: UserRole.MEMBER + }, + Eric: { + id: 'test-eric-user-uid', + role: UserRole.MEMBER + }, + SystemManager: { + id: 'test-system-manager-user-uid', + role: UserRole.MANAGER + } +} + +export const CREDENTIAL: Record = { + Root: { + id: PUBLIC_KEYS_JWK.Root.kid, + userId: USER.Root.id, + key: PUBLIC_KEYS_JWK.Root + }, + Alice: { + userId: USER.Alice.id, + id: PUBLIC_KEYS_JWK.Alice.kid, + key: PUBLIC_KEYS_JWK.Alice + }, + Bob: { + userId: USER.Bob.id, + id: PUBLIC_KEYS_JWK.Bob.kid, + key: PUBLIC_KEYS_JWK.Bob + }, + Carol: { + userId: USER.Carol.id, + id: PUBLIC_KEYS_JWK.Carol.kid, + key: PUBLIC_KEYS_JWK.Carol + }, + Dave: { + userId: USER.Dave.id, + id: PUBLIC_KEYS_JWK.Dave.kid, + key: PUBLIC_KEYS_JWK.Dave + }, + Eric: { + userId: USER.Eric.id, + id: PUBLIC_KEYS_JWK.Eric.kid, + key: PUBLIC_KEYS_JWK.Eric + }, + SystemManager: { + userId: USER.SystemManager.id, + id: PUBLIC_KEYS_JWK.SystemManager.kid, + key: PUBLIC_KEYS_JWK.SystemManager + } +} + +export const EOA_CREDENTIAL: Record = { + Root: { + id: VIEM_ACCOUNT.Root.address, + userId: USER.Root.id, + key: { + kty: KeyTypes.EC, + crv: Curves.SECP256K1, + alg: SigningAlg.ES256K, + kid: VIEM_ACCOUNT.Root.address, + addr: VIEM_ACCOUNT.Root.address + } + }, + Alice: { + id: VIEM_ACCOUNT.Alice.address, + userId: USER.Alice.id, + key: { + kty: KeyTypes.EC, + crv: Curves.SECP256K1, + alg: SigningAlg.ES256K, + kid: VIEM_ACCOUNT.Alice.address, + addr: VIEM_ACCOUNT.Alice.address + } + }, + Bob: { + id: VIEM_ACCOUNT.Bob.address, + userId: USER.Bob.id, + key: { + kty: KeyTypes.EC, + crv: Curves.SECP256K1, + alg: SigningAlg.ES256K, + kid: VIEM_ACCOUNT.Bob.address, + addr: VIEM_ACCOUNT.Bob.address + } + }, + Carol: { + id: VIEM_ACCOUNT.Carol.address, + userId: USER.Carol.id, + key: { + kty: KeyTypes.EC, + crv: Curves.SECP256K1, + alg: SigningAlg.ES256K, + kid: VIEM_ACCOUNT.Carol.address, + addr: VIEM_ACCOUNT.Carol.address + } + }, + Dave: { + id: VIEM_ACCOUNT.Dave.address, + userId: USER.Dave.id, + key: { + kty: KeyTypes.EC, + crv: Curves.SECP256K1, + alg: SigningAlg.ES256K, + kid: VIEM_ACCOUNT.Dave.address, + addr: VIEM_ACCOUNT.Dave.address + } + }, + Eric: { + id: VIEM_ACCOUNT.Eric.address, + userId: USER.Eric.id, + key: { + kty: KeyTypes.EC, + crv: Curves.SECP256K1, + alg: SigningAlg.ES256K, + kid: VIEM_ACCOUNT.Eric.address, + addr: VIEM_ACCOUNT.Eric.address + } + }, + SystemManager: { + id: VIEM_ACCOUNT.SystemManager.address, + userId: USER.SystemManager.id, + key: { + kty: KeyTypes.EC, + crv: Curves.SECP256K1, + alg: SigningAlg.ES256K, + kid: VIEM_ACCOUNT.SystemManager.address, + addr: VIEM_ACCOUNT.SystemManager.address + } + } +} + +export const USER_GROUP: Record = { + Engineering: { + id: 'test-engineering-user-group-uid' + }, + Treasury: { + id: 'test-treasury-user-group-uid' + } +} + +export const USER_GROUP_MEMBER: UserGroupMemberEntity[] = [ + { + groupId: USER_GROUP.Engineering.id, + userId: USER.Alice.id + }, + { + groupId: USER_GROUP.Engineering.id, + userId: USER.Carol.id + }, + { + groupId: USER_GROUP.Treasury.id, + userId: USER.Bob.id + }, + { + groupId: USER_GROUP.Treasury.id, + userId: USER.Eric.id + }, + { + groupId: USER_GROUP.Treasury.id, + userId: USER.Dave.id + } +] + +export const UNSAFE_ACCOUNT_PRIVATE_KEY: Record = { + Engineering: '0x1c2813a646825e89229434ad424c973e0fd043e4e99976abf6c7938419ca70b2', + Testing: '0x84daac66f32f715deded36d3cd22cd35a2f2b286d1205886899fff827dc1f3f2', + Treasury: '0x136f85910606e14fc69ffad7f1d77efc0f08284d1d9ac3369e51aeef81c8316e', + Operation: '0x2743f953c8912cbfec84702744b43937cfcb71b3d1fba7a1e6c08a2d6d726991' +} + +export const ACCOUNTSNAME_VIEMACCOUNT: Record = { + Engineering: privateKeyToAccount(UNSAFE_ACCOUNT_PRIVATE_KEY.Engineering), + Testing: privateKeyToAccount(UNSAFE_ACCOUNT_PRIVATE_KEY.Testing), + Treasury: privateKeyToAccount(UNSAFE_ACCOUNT_PRIVATE_KEY.Treasury), + Operation: privateKeyToAccount(UNSAFE_ACCOUNT_PRIVATE_KEY.Operation) +} + +export const ACCOUNT: Record = { + Testing: { + id: `eip155:eoa:${ACCOUNTSNAME_VIEMACCOUNT.Testing.address}`, + address: ACCOUNTSNAME_VIEMACCOUNT.Testing.address, + accountType: AccountType.EOA + }, + Engineering: { + id: `eip155:eoa:${ACCOUNTSNAME_VIEMACCOUNT.Engineering.address}`, + address: ACCOUNTSNAME_VIEMACCOUNT.Engineering.address, + accountType: AccountType.EOA + }, + Treasury: { + id: `eip155:eoa:${ACCOUNTSNAME_VIEMACCOUNT.Treasury.address}`, + address: ACCOUNTSNAME_VIEMACCOUNT.Treasury.address, + accountType: AccountType.EOA + }, + Operation: { + id: `eip155:eoa:${ACCOUNTSNAME_VIEMACCOUNT.Operation.address}`, + address: ACCOUNTSNAME_VIEMACCOUNT.Operation.address, + accountType: AccountType.EOA + } +} +/* +{ + Testing: { + id: 'eip155:eoa:0x0f610AC9F0091f8F573c33f15155afE8aD747495', + address: '0x0f610AC9F0091f8F573c33f15155afE8aD747495', + accountType: 'eoa' + }, + Engineering: { + id: 'eip155:eoa:0x9f38879167acCf7401351027EE3f9247A71cd0c5', + address: '0x9f38879167acCf7401351027EE3f9247A71cd0c5', + accountType: 'eoa' + }, + Treasury: { + id: 'eip155:eoa:0x0301e2724a40E934Cce3345928b88956901aA127', + address: '0x0301e2724a40E934Cce3345928b88956901aA127', + accountType: 'eoa' + }, + Operation: { + id: 'eip155:eoa:0x76d1b7f9b3F69C435eeF76a98A415332084A856F', + address: '0x76d1b7f9b3F69C435eeF76a98A415332084A856F', + accountType: 'eoa' + } +} +*/ + +export const ACCOUNT_GROUP: Record = { + Engineering: { + id: 'test-engineering-account-group-uid' + }, + Treasury: { + id: 'test-treasury-account-group-uid' + } +} + +export const ACCOUNT_GROUP_MEMBER: AccountGroupMemberEntity[] = [ + { + groupId: ACCOUNT_GROUP.Engineering.id, + accountId: ACCOUNT.Engineering.id + }, + { + groupId: ACCOUNT_GROUP.Engineering.id, + accountId: ACCOUNT.Testing.id + }, + { + groupId: ACCOUNT_GROUP.Treasury.id, + accountId: ACCOUNT.Treasury.id + }, + { + groupId: ACCOUNT_GROUP.Treasury.id, + accountId: ACCOUNT.Operation.id + } +] + +export const USER_ACCOUNT: UserAccountEntity[] = [ + { + accountId: ACCOUNT.Operation.id, + userId: USER.Alice.id + }, + { + accountId: ACCOUNT.Testing.id, + userId: USER.Alice.id + }, + { + accountId: ACCOUNT.Treasury.id, + userId: USER.Alice.id + } +] + +export const ADDRESS_BOOK: AddressBookAccountEntity[] = [ + { + id: `eip155:137:${ACCOUNT.Testing.address}`, + address: ACCOUNT.Testing.address, + chainId: 137, + classification: AccountClassification.MANAGED + }, + { + id: `eip155:1:${ACCOUNT.Engineering.address}`, + address: ACCOUNT.Engineering.address, + chainId: 1, + classification: AccountClassification.MANAGED + }, + { + id: `eip155:137:${ACCOUNT.Engineering.address}`, + address: ACCOUNT.Treasury.address, + chainId: 137, + classification: AccountClassification.MANAGED + }, + { + id: `eip155:1:${ACCOUNT.Treasury.address}`, + address: ACCOUNT.Treasury.address, + chainId: 1, + classification: AccountClassification.MANAGED + }, + { + id: `eip155:137:${ACCOUNT.Operation.address}`, + address: ACCOUNT.Operation.address, + chainId: 137, + classification: AccountClassification.MANAGED + }, + { + id: `eip155:1:${AddressBookAddresses.External}`, + address: AddressBookAddresses.External, + chainId: 1, + classification: AccountClassification.EXTERNAL + }, + { + id: `eip155:137:${AddressBookAddresses.Internal}`, + address: AddressBookAddresses.Internal, + chainId: 137, + classification: AccountClassification.INTERNAL + }, + { + id: `eip155:1:${AddressBookAddresses.CounterParty}`, + address: AddressBookAddresses.CounterParty, + chainId: 1, + classification: AccountClassification.COUNTERPARTY + } +] + +export const TOKEN: Record<`${string}1` | `${string}137`, TokenEntity> = { + usdc1: { + id: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + chainId: 1, + symbol: 'USDC', + decimals: 6 + }, + usdc137: { + id: 'eip155:137/erc20:0x2791bca1f2de4661ed88a30c99a7a9449aa84174', + address: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', + chainId: 137, + symbol: 'USDC', + decimals: 6 + } +} + +export const ENTITIES: EntitiesV<'1'> = { + addressBook: ADDRESS_BOOK, + credentials: [...Object.values(CREDENTIAL), ...Object.values(EOA_CREDENTIAL)], + tokens: Object.values(TOKEN), + userGroupMembers: USER_GROUP_MEMBER, + userGroups: Object.values(USER_GROUP), + userAccounts: USER_ACCOUNT, + users: Object.values(USER), + accountGroupMembers: ACCOUNT_GROUP_MEMBER, + accountGroups: Object.values(ACCOUNT_GROUP), + accounts: Object.values(ACCOUNT) +} + +export const POLICIES: Policy[] = [ + { + id: 'c13fe2c1-ecbe-43fe-9e0e-fae730fd5f50', + description: 'Required approval for an admin to transfer ERC-721 or ERC-1155 tokens', + when: [ + { + criterion: Criterion.CHECK_PRINCIPAL_ROLE, + args: [UserRole.ADMIN] + }, + { + criterion: Criterion.CHECK_ACTION, + args: [Action.SIGN_TRANSACTION] + }, + { + criterion: Criterion.CHECK_INTENT_TYPE, + args: ['transferErc721', 'transferErc1155'] + }, + { + criterion: Criterion.CHECK_APPROVALS, + args: [ + { + approvalCount: 2, + countPrincipal: false, + approvalEntityType: EntityType.User, + entityIds: [USER.Bob.id, USER.Carol.id] + } + ] + } + ], + then: Then.PERMIT + }, + { + id: 'f8ff8a65-a3ac-410f-800b-e345c49f9db9', + description: 'Authorize native transfers of up to 1 MATIC every 24 hours', + when: [ + { + criterion: Criterion.CHECK_ACTION, + args: [Action.SIGN_TRANSACTION] + }, + { + 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.LESS_THAN_OR_EQUAL, + timeWindow: { + type: 'rolling', + value: 43_200 + } + } + } + ], + then: Then.PERMIT + } +] \ No newline at end of file diff --git a/packages/policy-engine-shared/src/lib/dev.fixture.ts b/packages/policy-engine-shared/src/lib/dev.fixture.v2.ts similarity index 97% rename from packages/policy-engine-shared/src/lib/dev.fixture.ts rename to packages/policy-engine-shared/src/lib/dev.fixture.v2.ts index 76fdc114f..886951e16 100644 --- a/packages/policy-engine-shared/src/lib/dev.fixture.ts +++ b/packages/policy-engine-shared/src/lib/dev.fixture.v2.ts @@ -13,22 +13,12 @@ import { privateKeyToAccount } from 'viem/accounts' import { Action } from './type/action.type' import { EntityType, ValueOperators } from './type/domain.type' import { - AccountClassification, - AccountEntity, - AccountGroupMemberEntity, - AccountType, - AddressBookAccountEntity, - ClientEntity, - CredentialEntity, - EntitiesV, GroupEntity, - TokenEntity, - UserAccountEntity, - UserEntity, - UserGroupMemberEntity, - UserRole -} from './type/entity.type' +} from './type/entity.type.v2' import { Criterion, Policy, Then } from './type/policy.type' +import { EntitiesV } from './schema/entity.schema.shared' +import { AccountClassification, AccountEntity, AccountGroupMemberEntity, AddressBookAccountEntity, ClientEntity, CredentialEntity, TokenEntity, UserAccountEntity, UserEntity, UserGroupMemberEntity, UserRole } from './type/entity.type.v1' +import { AccountType } from '@narval/armory-sdk' const PERSONAS = ['Root', 'Alice', 'Bob', 'Carol', 'Dave', 'Eric', 'SystemManager'] as const const GROUPS_NAME = ['Engineering', 'Treasury'] as const diff --git a/packages/policy-engine-shared/src/lib/schema/credential.schema.ts b/packages/policy-engine-shared/src/lib/schema/credential.schema.ts new file mode 100644 index 000000000..553415643 --- /dev/null +++ b/packages/policy-engine-shared/src/lib/schema/credential.schema.ts @@ -0,0 +1,8 @@ +import { publicKeySchema } from '@narval/signature' +import { z } from 'zod' + +export const credentialEntitySchema = z.object({ + id: z.string(), + userId: z.string(), + key: publicKeySchema +}) diff --git a/packages/policy-engine-shared/src/lib/schema/entity.schema.shared.ts b/packages/policy-engine-shared/src/lib/schema/entity.schema.shared.ts new file mode 100644 index 000000000..475709692 --- /dev/null +++ b/packages/policy-engine-shared/src/lib/schema/entity.schema.shared.ts @@ -0,0 +1,22 @@ +import { z } from 'zod' +import { entitiesV2Schema } from './entity.schema.v2' +import { entitiesV1Schema } from './entity.schema.v1' + +export const entitiesSchema = z.discriminatedUnion('version', [entitiesV1Schema, entitiesV2Schema]) + +export const EntityVersion = { + '1': '1', + '2': '2' +} +export type EntityVersion = keyof typeof EntityVersion; + +const schemaMap = { + '1': entitiesV1Schema, + '2': entitiesV2Schema +} as const; + +export const getEntitySchema = (version?: EntityVersion) => version ? schemaMap[version] : entitiesV1Schema; + +export type EntitiesV = z.infer<(typeof schemaMap)[Version]> + +export type Entities = z.infer diff --git a/packages/policy-engine-shared/src/lib/schema/entity.schema.ts b/packages/policy-engine-shared/src/lib/schema/entity.schema.v1.ts similarity index 67% rename from packages/policy-engine-shared/src/lib/schema/entity.schema.ts rename to packages/policy-engine-shared/src/lib/schema/entity.schema.v1.ts index 60f054b53..04288239e 100644 --- a/packages/policy-engine-shared/src/lib/schema/entity.schema.ts +++ b/packages/policy-engine-shared/src/lib/schema/entity.schema.v1.ts @@ -2,6 +2,7 @@ import { publicKeySchema } from '@narval/signature' import { z } from 'zod' import { ChainAccountId } from '../util/caip.util' import { addressSchema } from './address.schema' +import { credentialEntitySchema } from './credential.schema' export const userRoleSchema = z.nativeEnum({ ROOT: 'root', @@ -22,13 +23,6 @@ export const accountClassificationSchema = z.nativeEnum({ MANAGED: 'managed' } as const) -export const credentialEntitySchema = z.object({ - id: z.string(), - userId: z.string(), - key: publicKeySchema - // TODO @ptroger: Should we be allowing a private key to be passed in entity data ? -}) - export const clientEntitySchema = z.object({ id: z.string() }) @@ -38,6 +32,10 @@ export const userEntitySchema = z.object({ role: userRoleSchema }) +export const userGroupEntitySchema = z.object({ + id: z.string() +}) + export const userAccountEntitySchema = z.object({ userId: z.string(), accountId: z.string() @@ -55,6 +53,10 @@ export const accountEntitySchema = z.object({ chainId: z.number().optional() }) +export const accountGroupEntitySchema = z.object({ + id: z.string() +}) + export const accountGroupMemberEntitySchema = z.object({ accountId: z.string(), groupId: z.string() @@ -81,44 +83,16 @@ export const tokenEntitySchema = z.object({ decimals: z.number() }) -export const groupEntitySchema = z.object({ - id: z.string() -}) - -export const entitiesV2Schema = z.object({ - version: z.literal('2'), - addressBook: z.array(addressBookAccountEntitySchema), - credentials: z.array(credentialEntitySchema), - tokens: z.array(tokenEntitySchema), - userGroupMembers: z.array(userGroupMemberEntitySchema), - userAccounts: z.array(userAccountEntitySchema), - users: z.array(userEntitySchema), - accountGroupMembers: z.array(accountGroupMemberEntitySchema), - groups: z.array(groupEntitySchema), - accounts: z.array(accountEntitySchema) -}) - export const entitiesV1Schema = z.object({ version: z.literal('1').optional(), addressBook: z.array(addressBookAccountEntitySchema), credentials: z.array(credentialEntitySchema), tokens: z.array(tokenEntitySchema), userGroupMembers: z.array(userGroupMemberEntitySchema), - userGroups: z.array(groupEntitySchema), + userGroups: z.array(userGroupEntitySchema), userAccounts: z.array(userAccountEntitySchema), users: z.array(userEntitySchema), accountGroupMembers: z.array(accountGroupMemberEntitySchema), - accountGroups: z.array(groupEntitySchema), + accountGroups: z.array(accountGroupEntitySchema), accounts: z.array(accountEntitySchema) }) - -export const entitiesSchema = z.discriminatedUnion('version', [entitiesV1Schema, entitiesV2Schema]) - -export const schemaMap = { - '1': entitiesV1Schema, - '2': entitiesV2Schema -} as const - -export type EntityVersion = keyof typeof schemaMap - -export const getEntitySchema = (version?: EntityVersion) => (version ? schemaMap[version] : entitiesV1Schema) diff --git a/packages/policy-engine-shared/src/lib/schema/entity.schema.v2.ts b/packages/policy-engine-shared/src/lib/schema/entity.schema.v2.ts new file mode 100644 index 000000000..be12bb754 --- /dev/null +++ b/packages/policy-engine-shared/src/lib/schema/entity.schema.v2.ts @@ -0,0 +1,16 @@ +import { z } from 'zod' +import { entitiesV1Schema } from './entity.schema.v1' + +export const groupEntitySchema = z.object({ + id: z.string() +}) + +export const entitiesV2Schema = entitiesV1Schema + .omit({ + accountGroups: true, + userGroups: true, + }) + .extend({ + version: z.literal('2'), + groups: z.array(groupEntitySchema), + }) diff --git a/packages/policy-engine-shared/src/lib/schema/policy.schema.ts b/packages/policy-engine-shared/src/lib/schema/policy.schema.v1.ts similarity index 99% rename from packages/policy-engine-shared/src/lib/schema/policy.schema.ts rename to packages/policy-engine-shared/src/lib/schema/policy.schema.v1.ts index ffa77d8a4..60872b155 100644 --- a/packages/policy-engine-shared/src/lib/schema/policy.schema.ts +++ b/packages/policy-engine-shared/src/lib/schema/policy.schema.v1.ts @@ -2,7 +2,7 @@ import { Alg } from '@narval/signature' import { z } from 'zod' import { Action } from '../type/action.type' import { EntityType, FiatCurrency, IdentityOperators, ValueOperators } from '../type/domain.type' -import { AccountType, UserRole } from '../type/entity.type' +import { AccountType, UserRole } from '../type/entity.type.v1' import { ChainAccountId } from '../util/caip.util' import { addressSchema } from './address.schema' import { hexSchema } from './hex.schema' 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 826f6cbaa..ec04a0b1a 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,7 +1,7 @@ import { jwkSchema } from '@narval/signature' import { z } from 'zod' -import { entitiesSchema } from '../schema/entity.schema' -import { policySchema } from '../schema/policy.schema' +import { entitiesSchema } from '../schema/entity.schema.shared' +import { policySchema } from '../schema/policy.schema.v1' export const SourceType = { HTTP: 'HTTP', diff --git a/packages/policy-engine-shared/src/lib/type/domain.type.ts b/packages/policy-engine-shared/src/lib/type/domain.type.ts index 510658deb..02d3c62a8 100644 --- a/packages/policy-engine-shared/src/lib/type/domain.type.ts +++ b/packages/policy-engine-shared/src/lib/type/domain.type.ts @@ -1,5 +1,4 @@ import { ZodTypeAny, z } from 'zod' -import { credentialEntitySchema } from '../schema/entity.schema' import { ChainAccountId } from '../util/caip.util' import { GrantPermissionAction, @@ -11,6 +10,7 @@ import { SignTypedDataAction, SignUserOperationAction } from './action.type' +import { credentialEntitySchema } from '../schema/credential.schema' export enum Decision { PERMIT = 'Permit', diff --git a/packages/policy-engine-shared/src/lib/type/engine.type.ts b/packages/policy-engine-shared/src/lib/type/engine.type.ts index 6867bcb1c..d9aebb1cd 100644 --- a/packages/policy-engine-shared/src/lib/type/engine.type.ts +++ b/packages/policy-engine-shared/src/lib/type/engine.type.ts @@ -1,6 +1,6 @@ import { EvaluationRequest, EvaluationResponse } from './domain.type' -import { Entities } from './entity.type' import { Policy } from './policy.type' +import { Entities } from '../schema/entity.schema.shared' export interface Engine { evaluate(request: EvaluationRequest): Promise diff --git a/packages/policy-engine-shared/src/lib/type/entity.type.ts b/packages/policy-engine-shared/src/lib/type/entity.type.v1.ts similarity index 79% rename from packages/policy-engine-shared/src/lib/type/entity.type.ts rename to packages/policy-engine-shared/src/lib/type/entity.type.v1.ts index 2c430b1f3..8456c82ce 100644 --- a/packages/policy-engine-shared/src/lib/type/entity.type.ts +++ b/packages/policy-engine-shared/src/lib/type/entity.type.v1.ts @@ -1,4 +1,5 @@ import { z } from 'zod' +import { accountGroupEntitySchema, userGroupEntitySchema } from '../schema/entity.schema.v1'; import { accountClassificationSchema, accountEntitySchema, @@ -6,17 +7,13 @@ import { accountTypeSchema, addressBookAccountEntitySchema, clientEntitySchema, - credentialEntitySchema, - entitiesSchema, - EntityVersion, - groupEntitySchema, - schemaMap, tokenEntitySchema, userAccountEntitySchema, userEntitySchema, userGroupMemberEntitySchema, userRoleSchema -} from '../schema/entity.schema' +} from '../schema/entity.schema.v1' +import { credentialEntitySchema } from '../schema/credential.schema'; export const UserRole = userRoleSchema.enum @@ -36,7 +33,9 @@ export type ClientEntity = z.infer export type UserEntity = z.infer -export type GroupEntity = z.infer +export type AccountGroupEntity = z.infer + +export type UserGroupEntity = z.infer export type UserAccountEntity = z.infer @@ -49,7 +48,3 @@ export type AccountGroupMemberEntity = z.infer export type TokenEntity = z.infer - -export type EntitiesV = z.infer<(typeof schemaMap)[Version]> - -export type Entities = z.infer diff --git a/packages/policy-engine-shared/src/lib/type/entity.type.v2.ts b/packages/policy-engine-shared/src/lib/type/entity.type.v2.ts new file mode 100644 index 000000000..f88209833 --- /dev/null +++ b/packages/policy-engine-shared/src/lib/type/entity.type.v2.ts @@ -0,0 +1,6 @@ +import { z } from 'zod' +import { + groupEntitySchema, +} from '../schema/entity.schema.v2' + +export type GroupEntity = z.infer diff --git a/packages/policy-engine-shared/src/lib/type/policy.type.ts b/packages/policy-engine-shared/src/lib/type/policy.type.ts index 7634099d2..097f013e2 100644 --- a/packages/policy-engine-shared/src/lib/type/policy.type.ts +++ b/packages/policy-engine-shared/src/lib/type/policy.type.ts @@ -62,7 +62,7 @@ import { userOperationIntentsConditionSchema, userOperationIntentsCriterionSchema, userOperationTransfersConditionSchema -} from '../schema/policy.schema' +} from '../schema/policy.schema.v1' export const Then = thenSchema.enum export type Then = z.infer diff --git a/packages/policy-engine-shared/src/lib/util/__test__/unit/entity.util.spec.ts b/packages/policy-engine-shared/src/lib/util/__test__/unit/entity.util.spec.ts index 728ca172d..290026474 100644 --- a/packages/policy-engine-shared/src/lib/util/__test__/unit/entity.util.spec.ts +++ b/packages/policy-engine-shared/src/lib/util/__test__/unit/entity.util.spec.ts @@ -1,8 +1,290 @@ -import { ACCOUNT, ADDRESS_BOOK, CREDENTIAL, GROUP, TOKEN, USER } from '../../../dev.fixture' -import { Entities, UserEntity, UserRole } from '../../../type/entity.type' +import { ACCOUNT, ADDRESS_BOOK, CREDENTIAL, GROUP, TOKEN, USER } from '../../../dev.fixture.v2' +import { Entities } from '../../../schema/entity.schema.shared' +import { UserEntity, UserRole } from '../../../type/entity.type.v1' import { populate, updateUserAccounts, validate } from '../../entity.util' +import { ACCOUNT_GROUP, USER_GROUP } from '../../../dev.fixture.v1'; -describe('validate', () => { +describe(`validate<'1'>`, () => { + const emptyEntities: Entities = { + addressBook: [], + credentials: [], + tokens: [], + userGroupMembers: [], + userGroups: [], + userAccounts: [], + users: [], + accountGroupMembers: [], + accountGroups: [], + accounts: [] + } + + describe('association integrity', () => { + it('fails when group from user group member does not exist', () => { + const result = validate({ + ...emptyEntities, + userGroupMembers: [ + { + groupId: USER_GROUP.Engineering.id, + userId: USER.Alice.id + } + ], + users: [USER.Alice] + }) + + expect(result).toEqual({ + success: false, + issues: [ + { + code: 'ENTITY_NOT_FOUND', + message: + "couldn't create the user group member because the group test-engineering-user-group-uid is undefined" + } + ] + }) + }) + + it('fails when user from user group member does not exist', () => { + const result = validate({ + ...emptyEntities, + userGroups: [USER_GROUP.Engineering], + userGroupMembers: [ + { + groupId: USER_GROUP.Engineering.id, + userId: USER.Alice.id + } + ] + }) + + expect(result).toEqual({ + success: false, + issues: [ + { + code: 'ENTITY_NOT_FOUND', + message: + "couldn't create the user group member for group test-engineering-user-group-uid because the user test-alice-user-uid is undefined" + } + ] + }) + }) + + it('fails when group from account group member does not exist', () => { + const result = validate({ + ...emptyEntities, + accounts: [ACCOUNT.Engineering], + accountGroupMembers: [ + { + accountId: ACCOUNT.Engineering.id, + groupId: ACCOUNT_GROUP.Engineering.id + } + ] + }) + + expect(result).toEqual({ + success: false, + issues: [ + { + code: 'ENTITY_NOT_FOUND', + message: + "couldn't create the account group member because the group test-engineering-account-group-uid is undefined" + } + ] + }) + }) + + it('fails when account from account group member does not exist', () => { + const result = validate({ + ...emptyEntities, + accountGroups: [ACCOUNT_GROUP.Engineering], + accountGroupMembers: [ + { + accountId: ACCOUNT.Engineering.id, + groupId: ACCOUNT_GROUP.Engineering.id + } + ] + }) + + expect(result).toEqual({ + success: false, + issues: [ + { + code: 'ENTITY_NOT_FOUND', + message: + "couldn't create the account group member for group test-engineering-account-group-uid because the account eip155:eoa:0x9f38879167acCf7401351027EE3f9247A71cd0c5 is undefined" + } + ] + }) + }) + + it('fails when user from user account does not exist', () => { + const result = validate({ + ...emptyEntities, + accounts: [ACCOUNT.Engineering], + userAccounts: [ + { + userId: USER.Alice.id, + accountId: ACCOUNT.Engineering.id + } + ] + }) + + expect(result).toEqual({ + success: false, + issues: [ + { + code: 'ENTITY_NOT_FOUND', + message: `couldn't assign the account ${ACCOUNT.Engineering.id} because the user ${USER.Alice.id} is undefined` + } + ] + }) + }) + + it('fails when account from user account does not exist', () => { + const result = validate({ + ...emptyEntities, + users: [USER.Alice], + userAccounts: [ + { + userId: USER.Alice.id, + accountId: ACCOUNT.Engineering.id + } + ] + }) + + expect(result).toEqual({ + success: false, + issues: [ + { + code: 'ENTITY_NOT_FOUND', + message: `couldn't assign the account ${ACCOUNT.Engineering.id} because it's undefined` + } + ] + }) + }) + }) + + describe('uids duplication', () => { + it('fails when address book uids are not unique', () => { + const result = validate({ + ...emptyEntities, + addressBook: [ADDRESS_BOOK[0], ADDRESS_BOOK[0], ADDRESS_BOOK[1]] + }) + + expect(result).toEqual({ + success: false, + issues: [ + { + code: 'UNIQUE_IDENTIFIER_DUPLICATION', + message: `the address book account ${ADDRESS_BOOK[0].id} is duplicated` + } + ] + }) + }) + + it('fails when credential uids are not unique', () => { + const result = validate({ + ...emptyEntities, + credentials: [CREDENTIAL.Alice, CREDENTIAL.Alice, CREDENTIAL.Bob] + }) + + expect(result).toEqual({ + success: false, + issues: [ + { + code: 'UNIQUE_IDENTIFIER_DUPLICATION', + message: `the credential ${CREDENTIAL.Alice.id} is duplicated` + } + ] + }) + }) + + it('fails when token uids are not unique', () => { + const result = validate({ + ...emptyEntities, + tokens: [TOKEN.usdc1, TOKEN.usdc1, TOKEN.usdc137] + }) + + expect(result).toEqual({ + success: false, + issues: [ + { + code: 'UNIQUE_IDENTIFIER_DUPLICATION', + message: `the token ${TOKEN.usdc1.id} is duplicated` + } + ] + }) + }) + + it('fails when user group uids are not unique', () => { + const result = validate({ + ...emptyEntities, + userGroups: [USER_GROUP.Engineering, USER_GROUP.Engineering, USER_GROUP.Treasury] + }) + + expect(result).toEqual({ + success: false, + issues: [ + { + code: 'UNIQUE_IDENTIFIER_DUPLICATION', + message: `the user group ${USER_GROUP.Engineering.id} is duplicated` + } + ] + }) + }) + + it('fails when users uids are not unique', () => { + const result = validate({ + ...emptyEntities, + users: [USER.Alice, USER.Alice, USER.Bob] + }) + + expect(result).toEqual({ + success: false, + issues: [ + { + code: 'UNIQUE_IDENTIFIER_DUPLICATION', + message: `the user ${USER.Alice.id} is duplicated` + } + ] + }) + }) + + it('fails when account group uids are not unique', () => { + const result = validate({ + ...emptyEntities, + accountGroups: [ACCOUNT_GROUP.Engineering, ACCOUNT_GROUP.Engineering, ACCOUNT_GROUP.Treasury] + }) + + expect(result).toEqual({ + success: false, + issues: [ + { + code: 'UNIQUE_IDENTIFIER_DUPLICATION', + message: `the account group ${ACCOUNT_GROUP.Engineering.id} is duplicated` + } + ] + }) + }) + + it('fails when accounts uids are not unique', () => { + const result = validate({ + ...emptyEntities, + accounts: [ACCOUNT.Engineering, ACCOUNT.Engineering, ACCOUNT.Treasury] + }) + + expect(result).toEqual({ + success: false, + issues: [ + { + code: 'UNIQUE_IDENTIFIER_DUPLICATION', + message: `the account ${ACCOUNT.Engineering.id} is duplicated` + } + ] + }) + }) + }) +}) + +describe(`validate<'2'>`, () => { const emptyEntities: Entities = { version: '2', addressBook: [], diff --git a/packages/policy-engine-shared/src/lib/util/entity.util.ts b/packages/policy-engine-shared/src/lib/util/entity.util.ts index b5d36753b..35c7ead75 100644 --- a/packages/policy-engine-shared/src/lib/util/entity.util.ts +++ b/packages/policy-engine-shared/src/lib/util/entity.util.ts @@ -1,13 +1,13 @@ -import { entitiesV1Schema, entitiesV2Schema, EntityVersion, getEntitySchema } from '../schema/entity.schema' +import { Entities, EntitiesV, EntityVersion, getEntitySchema } from '../schema/entity.schema.shared'; +import { entitiesV1Schema } from '../schema/entity.schema.v1' +import { entitiesV2Schema } from '../schema/entity.schema.v2' import { AccountEntity, CredentialEntity, - Entities, - EntitiesV, UserAccountEntity, UserEntity -} from '../type/entity.type' -import { isV1, Validation, Validator } from './validation.types' +} from '../type/entity.type.v1' +import { isV1, RequiredValidators, Validation, ValidationOption, Validator } from './validation.types' import { V1_VALIDATORS } from './validators.v1' import { V2_VALIDATORS } from './validators.v2' @@ -20,9 +20,7 @@ export const isVersion = (entities: Entities): entities is EntitiesV => getEntitySchema(version).safeParse(entities).success -export const VALIDATORS: { - [V in EntityVersion]: Validator[] -} = { +export const VALIDATORS: RequiredValidators = { '1': V1_VALIDATORS, '2': V2_VALIDATORS } @@ -30,6 +28,16 @@ export const VALIDATORS: { export const findEntityVersion = (entities: Entities): { version: EntityVersion } => !entities.version ? { version: '1' } : { version: entities.version } + +const getValidators = (version: EntityVersion): Validator[] => { + switch (version) { + case '1': + return VALIDATORS['1'] as Validator[] + case '2': + return VALIDATORS['2'] as Validator[] + } +} + export const validate = (entities: Entities): Validation => { const { version } = findEntityVersion(entities) @@ -38,7 +46,6 @@ export const validate = (entities: Entities): Validation => { const result = schema.safeParse(entities) if (result.success) { - const validators = VALIDATORS[version] as Validator[] const issues = validators.flatMap((validation) => validation(result.data)) if (issues.length) { @@ -63,6 +70,7 @@ export const validate = (entities: Entities): Validation => { issues: schemaIssues || [] } } + export const emptyV2 = (): EntitiesV<'2'> => ({ version: '2', diff --git a/packages/policy-engine-shared/src/lib/util/validation.types.ts b/packages/policy-engine-shared/src/lib/util/validation.types.ts index 985fd6e03..27d52e93e 100644 --- a/packages/policy-engine-shared/src/lib/util/validation.types.ts +++ b/packages/policy-engine-shared/src/lib/util/validation.types.ts @@ -1,5 +1,5 @@ -import { EntityVersion, entitiesV1Schema } from '../schema/entity.schema' -import { Entities, EntitiesV } from '../type/entity.type' +import { EntityVersion, Entities, EntitiesV } from '../schema/entity.schema.shared' +import { entitiesV1Schema } from '../schema/entity.schema.v1' export type ValidationIssue = { code: string @@ -10,8 +10,12 @@ export type Validation = { success: true } | { success: false; issues: Validatio export type Validator = (entities: EntitiesV) => ValidationIssue[] +export type RequiredValidators = Required<{ + [V in EntityVersion]: Validator[] +}> + export type ValidationOption = { - version?: Validator[] + validators?: Validator[] } export const isV1 = (entities: Partial): entities is Partial> => diff --git a/packages/policy-engine-shared/src/lib/util/validators.v1.ts b/packages/policy-engine-shared/src/lib/util/validators.v1.ts index 60725ca73..b0b74b182 100644 --- a/packages/policy-engine-shared/src/lib/util/validators.v1.ts +++ b/packages/policy-engine-shared/src/lib/util/validators.v1.ts @@ -1,5 +1,5 @@ import { countBy, flatten, indexBy, keys, map, pickBy } from 'lodash/fp' -import { EntitiesV } from '../type/entity.type' +import { EntitiesV } from '../schema/entity.schema.shared' import { ValidationIssue, Validator } from './validation.types' const validateUserGroupMemberIntegrity: Validator<'1'> = (entities: EntitiesV<'1'>): ValidationIssue[] => { diff --git a/packages/policy-engine-shared/src/lib/util/validators.v2.ts b/packages/policy-engine-shared/src/lib/util/validators.v2.ts index a0ccfa1c2..8bb81a021 100644 --- a/packages/policy-engine-shared/src/lib/util/validators.v2.ts +++ b/packages/policy-engine-shared/src/lib/util/validators.v2.ts @@ -1,5 +1,5 @@ import { countBy, flatten, indexBy, keys, map, pickBy } from 'lodash/fp' -import { EntitiesV } from '../type/entity.type' +import { EntitiesV } from '../schema/entity.schema.shared' import { ValidationIssue, Validator } from './validation.types' const validateGroupMemberIntegrity: Validator<'2'> = (entities: EntitiesV<'2'>): ValidationIssue[] => {