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 dce4400e3..b4ded1901 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 @@ -187,8 +187,6 @@ export class OpenPolicyAgentEngine implements Engine { // an array of results with an inner result. We perform a typecast here to // satisfy TypeScript compiler. Later, we parse the schema a few lines // below to ensure type-safety for data coming from external sources. - - console.log('###input ', JSON.stringify(input, null, 2)) const results = (await this.opa.evaluate(input, POLICY_ENTRYPOINT)) as { result: unknown }[] const parse = z.array(resultSchema).safeParse(results.map(({ result }) => result)) diff --git a/examples/approvals-by-spending-limit/README.md b/examples/approvals-by-spending-limit/README.md index ccb581d85..7b177e594 100644 --- a/examples/approvals-by-spending-limit/README.md +++ b/examples/approvals-by-spending-limit/README.md @@ -1,4 +1,5 @@ # Collect approval before authorizing a request + Setup `npm i` (run from this directory) @@ -7,23 +8,22 @@ Run each command in order 1. Set up Policies & base Entity data (see data.ts) - as the system manager - `tsx 1-setup.ts` + `tsx 1-setup.ts` 2. Add a new Destination to the address book - as the system manager - `tsx 2-add-destination.ts` + `tsx 2-add-destination.ts` 3. Add a second Account to be managed - as the system manager - `tsx 3-add-account.ts` + `tsx 3-add-account.ts` 4. Transfer from Account A to Account B - as the Member User - Run the transfer 4 times in a row; the 4th requires an Approval + Run the transfer 4 times in a row; the 4th requires an Approval - `tsx 4-transfer-a-to-b.ts` + `tsx 4-transfer-a-to-b.ts` 5. Use the ID from the pending request to approve - as the Admin User - `tsx 5-approve-transfer.ts $REQUEST_ID` - + `tsx 5-approve-transfer.ts $REQUEST_ID` diff --git a/examples/approvals-by-spending-limit/package.json b/examples/approvals-by-spending-limit/package.json index f73c96167..b4d6661c6 100644 --- a/examples/approvals-by-spending-limit/package.json +++ b/examples/approvals-by-spending-limit/package.json @@ -22,4 +22,4 @@ "devDependencies": { "@types/minimist": "^1.2.5" } -} \ No newline at end of file +} diff --git a/packages/armory-sdk/src/lib/__test__/e2e/scenarii/tiered-eth-transfer-policy.spec.ts b/packages/armory-sdk/src/lib/__test__/e2e/scenarii/tiered-eth-transfer-policy.spec.ts index 5df0b14ee..8811a8883 100644 --- a/packages/armory-sdk/src/lib/__test__/e2e/scenarii/tiered-eth-transfer-policy.spec.ts +++ b/packages/armory-sdk/src/lib/__test__/e2e/scenarii/tiered-eth-transfer-policy.spec.ts @@ -1,5 +1,5 @@ /* eslint-disable jest/consistent-test-it */ -import { Action, Decision, entitiesSchema, FIXTURE, policySchema, Request, ValueOperators } from '@narval/policy-engine-shared' +import { Action, Decision, entitiesSchema, FIXTURE, policySchema, Request, toHex } from '@narval/policy-engine-shared' import { v4 } from 'uuid' import defaultEntities from '../../../../resource/entity/default.json' import tieredEthTransfer from '../../../../resource/policy/set/tiered-eth-transfer.json' @@ -54,87 +54,62 @@ describe('tiered approvals and spending limits', () => { clientId, host: getAuthHost(), entities, - policies: [ { - "id": "tier1-low-value-transfers", - "description": "Permit members to transfer up to 0.00000000001 ETH per day without approval", - "when": [ - { - "criterion": "checkPrincipalRole", - "args": [ - "member" - ] - }, - { - "criterion": "checkIntentToken", - "args": [ - "eip155:1/slip44:60" - ] - }, - { - "criterion": "checkIntentAmount", - "args": { - "value": "1", - "operator": "lte" as ValueOperators - } - } - ], - "then": "permit" - }] + policies }) }) - // it('permits member to transfer less than or equal to 1 ETH', async () => { - // const { authClient } = await buildAuthClient(antoinePrivateKey, { - // host: getAuthHost(), - // clientId - // }) - - // const lowValueRequest = genNonce({ - // ...request, - // transactionRequest: { - // ...request.transactionRequest, - // value: '0xDE0B6B3A7640000' // 0.3 ETH - // } - // }) - - // const response = await authClient.requestAccessToken(lowValueRequest) - // expect(response).toMatchObject({ value: expect.any(String) }) - // }) - - // it('requires manager approval for transfers between 1 and 10 ETH', async () => { - // expect.assertions(2) - - // const { authClient: managerClient } = await buildAuthClient(carolPrivateKey, { - // host: getAuthHost(), - // clientId - // }) - - // const { authClient } = await buildAuthClient(antoinePrivateKey, { - // host: getAuthHost(), - // clientId - // }) - - // const mediumValueRequest = genNonce({ - // ...request, - // transactionRequest: { - // ...request.transactionRequest, - // value: '0x4563918244F40000' // 5 ETH - // } - // }) - - // const res = await authClient.authorize(mediumValueRequest) - // expect(res.decision).toEqual(Decision.CONFIRM) - - // if (res.decision === Decision.CONFIRM) { - // await managerClient.approve(res.authId) - - // const accessToken = await authClient.getAccessToken(res.authId) - // expect(accessToken).toMatchObject({ value: expect.any(String) }) - // } - // }) + it('permits member to transfer less than or equal to 1 ETH', async () => { + const { authClient } = await buildAuthClient(antoinePrivateKey, { + host: getAuthHost(), + clientId + }) + + const lowValueRequest = genNonce({ + ...request, + transactionRequest: { + ...request.transactionRequest, + value: toHex(1000000000000000000n) // 1 ETH + } + }) + + const response = await authClient.requestAccessToken(lowValueRequest) + expect(response).toMatchObject({ value: expect.any(String) }) + }) + + it('requires manager approval for transfers between 1 and 10 ETH', async () => { + expect.assertions(2) + + const { authClient: managerClient } = await buildAuthClient(carolPrivateKey, { + host: getAuthHost(), + clientId + }) + + const { authClient } = await buildAuthClient(antoinePrivateKey, { + host: getAuthHost(), + clientId + }) + + const mediumValueRequest = genNonce({ + ...request, + transactionRequest: { + ...request.transactionRequest, + value: '0x4563918244F40000' // 5 ETH + } + }) + + const res = await authClient.authorize(mediumValueRequest) + expect(res.decision).toEqual(Decision.CONFIRM) + + if (res.decision === Decision.CONFIRM) { + await managerClient.approve(res.authId) + + const accessToken = await authClient.getAccessToken(res.authId) + expect(accessToken).toMatchObject({ value: expect.any(String) }) + } + }) it('requires admin approval for transfers between 10 and 100 ETH', async () => { - expect.assertions(1) + expect.assertions(2) const { authClient: adminClient } = await buildAuthClient(alicePrivateKey, { host: getAuthHost(), @@ -147,19 +122,19 @@ describe('tiered approvals and spending limits', () => { }) const highValueRequest = genNonce({ - action: Action.SIGN_TRANSACTION, - nonce: 'test-nonce-4', - transactionRequest: { - from: '0x0301e2724a40E934Cce3345928b88956901aA127', - to: '0x76d1b7f9b3F69C435eeF76a98A415332084A856F', - value: '0x7FFFFFFFFFFFFDFF', // 10 ETH - chainId: 1 - }, - resourceId: 'eip155:eoa:0x0301e2724a40e934cce3345928b88956901aa127' - }) + action: Action.SIGN_TRANSACTION, + nonce: 'test-nonce-4', + transactionRequest: { + from: '0x0301e2724a40E934Cce3345928b88956901aA127', + to: '0x76d1b7f9b3F69C435eeF76a98A415332084A856F', + value: '0x8000000000000000', // 10 ETH + chainId: 1 + }, + resourceId: 'eip155:eoa:0x0301e2724a40e934cce3345928b88956901aa127' + }) const res = await authClient.authorize(highValueRequest) - expect(res.decision).toEqual(Decision.FORBID) + expect(res.decision).toEqual(Decision.CONFIRM) if (res.decision === Decision.CONFIRM) { await adminClient.approve(res.authId) @@ -169,42 +144,41 @@ describe('tiered approvals and spending limits', () => { } }) + it('requires two admin approvals for transfers above 100 ETH', async () => { + expect.assertions(2) - // it('requires two admin approvals for transfers above 100 ETH', async () => { - // expect.assertions(3) - - // const { authClient: adminClient1 } = await buildAuthClient(alicePrivateKey, { - // host: getAuthHost(), - // clientId - // }) + const { authClient: adminClient1 } = await buildAuthClient(alicePrivateKey, { + host: getAuthHost(), + clientId + }) - // const { authClient: adminClient2 } = await buildAuthClient(bobPrivateKey, { - // host: getAuthHost(), - // clientId - // }) + const { authClient: adminClient2 } = await buildAuthClient(bobPrivateKey, { + host: getAuthHost(), + clientId + }) - // const { authClient } = await buildAuthClient(antoinePrivateKey, { - // host: getAuthHost(), - // clientId - // }) + const { authClient } = await buildAuthClient(antoinePrivateKey, { + host: getAuthHost(), + clientId + }) - // const veryHighValueRequest = genNonce({ - // ...request, - // transactionRequest: { - // ...request.transactionRequest, - // value: '0x56BC75E2D63100000' // 150 ETH - // } - // }) + const veryHighValueRequest = genNonce({ + ...request, + transactionRequest: { + ...request.transactionRequest, + value: '0x56BC75E2D63100000' // 150 ETH + } + }) - // const res = await authClient.authorize(veryHighValueRequest) - // expect(res.decision).toEqual(Decision.CONFIRM) + const res = await authClient.authorize(veryHighValueRequest) + expect(res.decision).toEqual(Decision.CONFIRM) - // if (res.decision === Decision.CONFIRM) { - // await adminClient1.approve(res.authId) - // await adminClient2.approve(res.authId) + if (res.decision === Decision.CONFIRM) { + await adminClient1.approve(res.authId) + await adminClient2.approve(res.authId) - // const accessToken = await authClient.getAccessToken(res.authId) - // expect(accessToken).toMatchObject({ value: expect.any(String) }) - // } - // }) + const accessToken = await authClient.getAccessToken(res.authId) + expect(accessToken).toMatchObject({ value: expect.any(String) }) + } + }) }) diff --git a/packages/armory-sdk/src/lib/__test__/util/setup.ts b/packages/armory-sdk/src/lib/__test__/util/setup.ts index ceb54c229..6ad031b06 100644 --- a/packages/armory-sdk/src/lib/__test__/util/setup.ts +++ b/packages/armory-sdk/src/lib/__test__/util/setup.ts @@ -6,7 +6,6 @@ import { AuthAdminClient, AuthClient, AuthConfig } from '../../auth' import { createHttpDataStore, DataStoreConfig, EntityStoreClient, PolicyStoreClient } from '../../data-store' import { VaultAdminClient, VaultConfig } from '../../vault' - export const getAuthHost = () => 'http://localhost:3005' export const getAuthAdminApiKey = () => 'armory-admin-api-key' diff --git a/packages/armory-sdk/src/resource/policy/set/tiered-eth-transfer.json b/packages/armory-sdk/src/resource/policy/set/tiered-eth-transfer.json index 6c9e384de..f41eb2e55 100644 --- a/packages/armory-sdk/src/resource/policy/set/tiered-eth-transfer.json +++ b/packages/armory-sdk/src/resource/policy/set/tiered-eth-transfer.json @@ -5,15 +5,11 @@ "when": [ { "criterion": "checkPrincipalRole", - "args": [ - "member" - ] + "args": ["member"] }, { "criterion": "checkIntentToken", - "args": [ - "eip155:1/slip44:60" - ] + "args": ["eip155:1/slip44:60"] }, { "criterion": "checkSpendingLimit", @@ -26,9 +22,7 @@ }, "filters": { "perPrincipal": true, - "tokens": [ - "eip155:1/slip44:60" - ] + "tokens": ["eip155:1/slip44:60"] } } } @@ -39,6 +33,14 @@ "id": "tier2-medium-value-transfers", "description": "Require manager approval for transfers between 1-10 ETH per day", "when": [ + { + "criterion": "checkPrincipalRole", + "args": ["member"] + }, + { + "criterion": "checkIntentToken", + "args": ["eip155:1/slip44:60"] + }, { "criterion": "checkSpendingLimit", "args": { @@ -50,9 +52,7 @@ }, "filters": { "perPrincipal": true, - "tokens": [ - "eip155:1/slip44:60" - ] + "tokens": ["eip155:1/slip44:60"] } } }, @@ -67,9 +67,7 @@ }, "filters": { "perPrincipal": true, - "tokens": [ - "eip155:1/slip44:60" - ] + "tokens": ["eip155:1/slip44:60"] } } }, @@ -80,13 +78,108 @@ "approvalCount": 1, "countPrincipal": false, "approvalEntityType": "Narval::UserRole", - "entityIds": [ - "manager" - ] + "entityIds": ["manager"] + } + ] + } + ], + "then": "permit" + }, + { + "id": "tier3-high-value-transfers", + "description": "Require one admin approval for transfers between 10-100 ETH per day", + "when": [ + { + "criterion": "checkPrincipalRole", + "args": ["member"] + }, + { + "criterion": "checkIntentToken", + "args": ["eip155:1/slip44:60"] + }, + { + "criterion": "checkSpendingLimit", + "args": { + "limit": "10000000000000000000", + "operator": "gt", + "timeWindow": { + "type": "rolling", + "value": 86400 + }, + "filters": { + "perPrincipal": true, + "tokens": ["eip155:1/slip44:60"] + } + } + }, + { + "criterion": "checkSpendingLimit", + "args": { + "limit": "100000000000000000000", + "operator": "lte", + "timeWindow": { + "type": "rolling", + "value": 86400 + }, + "filters": { + "perPrincipal": true, + "tokens": ["eip155:1/slip44:60"] + } + } + }, + { + "criterion": "checkApprovals", + "args": [ + { + "approvalCount": 1, + "countPrincipal": false, + "approvalEntityType": "Narval::UserRole", + "entityIds": ["admin"] + } + ] + } + ], + "then": "permit" + }, + { + "id": "tier4-very-high-value-transfers", + "description": "Require two admin approvals for transfers above 100 ETH", + "when": [ + { + "criterion": "checkPrincipalRole", + "args": ["member"] + }, + { + "criterion": "checkIntentToken", + "args": ["eip155:1/slip44:60"] + }, + { + "criterion": "checkSpendingLimit", + "args": { + "limit": "100000000000000000000", + "operator": "gt", + "timeWindow": { + "type": "rolling", + "value": 86400 + }, + "filters": { + "perPrincipal": true, + "tokens": ["eip155:1/slip44:60"] + } + } + }, + { + "criterion": "checkApprovals", + "args": [ + { + "approvalCount": 2, + "countPrincipal": false, + "approvalEntityType": "Narval::UserRole", + "entityIds": ["admin"] } ] } ], "then": "permit" } -] \ No newline at end of file +] diff --git a/packages/policy-engine-shared/src/lib/schema/policy.schema.ts b/packages/policy-engine-shared/src/lib/schema/policy.schema.ts index 504edfde4..ffa77d8a4 100644 --- a/packages/policy-engine-shared/src/lib/schema/policy.schema.ts +++ b/packages/policy-engine-shared/src/lib/schema/policy.schema.ts @@ -168,7 +168,6 @@ const rollingTimeWindowSchema = z.object({ endDate: z.number().int().optional() }) - export const timeWindowSchema = z.discriminatedUnion('type', [rollingTimeWindowSchema, fixedTimeWindowSchema]) export const transferFiltersSchema = z.object({