From eb8a9fa1c1edcae550de4b1f73cb573d4989f51e Mon Sep 17 00:00:00 2001 From: Samuel Date: Tue, 13 Feb 2024 10:50:05 +0100 Subject: [PATCH] Add unit tests for policy builder (#107) --- apps/authz/src/app/opa/opa.service.ts | 51 +--------- .../shared/__test__/unit/opa.utils.spec.ts | 93 +++++++++++++++++++ apps/authz/src/shared/types/policy.type.ts | 86 ++++++++--------- apps/authz/src/shared/utils/opa.utils.ts | 45 +++++++++ 4 files changed, 185 insertions(+), 90 deletions(-) create mode 100644 apps/authz/src/shared/__test__/unit/opa.utils.spec.ts create mode 100644 apps/authz/src/shared/utils/opa.utils.ts diff --git a/apps/authz/src/app/opa/opa.service.ts b/apps/authz/src/app/opa/opa.service.ts index 8f1285e86..1f987cea2 100644 --- a/apps/authz/src/app/opa/opa.service.ts +++ b/apps/authz/src/app/opa/opa.service.ts @@ -2,13 +2,13 @@ import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common' import { loadPolicy } from '@open-policy-agent/opa-wasm' import { readFileSync, writeFileSync } from 'fs' import Handlebars from 'handlebars' -import { isEmpty } from 'lodash' import path from 'path' import * as R from 'remeda' import { v4 as uuidv4 } from 'uuid' import { RegoData, User, UserGroup, WalletGroup } from '../../shared/types/entities.types' -import { Criterion, Policy, Then } from '../../shared/types/policy.type' +import { Policy } from '../../shared/types/policy.type' import { OpaResult, RegoInput } from '../../shared/types/rego' +import { criterionToString, reasonToString } from '../../shared/utils/opa.utils' import { AdminRepository } from '../persistence/repository/admin.repository' type PromiseType> = T extends Promise ? U : never @@ -41,52 +41,9 @@ export class OpaService implements OnApplicationBootstrap { } generateRegoFile(policies: Policy[]): string { - Handlebars.registerHelper('criterion', function (item) { - const criterion: Criterion = item.criterion - const args = item.args + Handlebars.registerHelper('criterion', criterionToString) - if (args === null) { - return `${criterion}` - } - - if (!isEmpty(args)) { - if (Array.isArray(args)) { - if (typeof args[0] === 'string') { - return `${criterion}({${args.map((el) => `"${el}"`).join(', ')}})` - } - - if (criterion === Criterion.CHECK_APPROVALS) { - return `approvals = ${criterion}([${args.map((el) => JSON.stringify(el)).join(', ')}])` - } - - return `${criterion}([${args.map((el) => JSON.stringify(el)).join(', ')}])` - } - - return `${criterion}(${JSON.stringify(args)})` - } - }) - - Handlebars.registerHelper('reason', function (item) { - if (item.then === Then.PERMIT) { - const reason = [ - `"type": "${item.then}"`, - `"policyId": "${item.name}"`, - '"approvalsSatisfied": approvals.approvalsSatisfied', - '"approvalsMissing": approvals.approvalsMissing' - ] - return `reason = {${reason.join(', ')}}` - } - - if (item.then === Then.FORBID) { - const reason = { - type: item.then, - policyId: item.name, - approvalsSatisfied: [], - approvalsMissing: [] - } - return `reason = ${JSON.stringify(reason)}` - } - }) + Handlebars.registerHelper('reason', reasonToString) const templateSource = readFileSync('./apps/authz/src/opa/template/template.hbs', 'utf-8') diff --git a/apps/authz/src/shared/__test__/unit/opa.utils.spec.ts b/apps/authz/src/shared/__test__/unit/opa.utils.spec.ts new file mode 100644 index 000000000..f4c929210 --- /dev/null +++ b/apps/authz/src/shared/__test__/unit/opa.utils.spec.ts @@ -0,0 +1,93 @@ +import { EntityType, ValueOperators } from '@narval/authz-shared' +import { + ApprovalsCriterion, + Criterion, + ERC1155TransfersCriterion, + IntentAmountCriterion, + NonceRequiredCriterion, + Policy, + Then, + WalletAddressCriterion +} from '../../types/policy.type' +import { criterionToString, reasonToString } from '../../utils/opa.utils' + +describe('criterionToString', () => { + it('returns criterion if args are null', () => { + const item: NonceRequiredCriterion = { + criterion: Criterion.CHECK_NONCE_EXISTS, + args: null + } + expect(criterionToString(item)).toEqual(Criterion.CHECK_NONCE_EXISTS) + }) + + it('returns criterion if args is an array of strings', () => { + const item: WalletAddressCriterion = { + criterion: Criterion.CHECK_WALLET_ADDRESS, + args: ['0x123', '0x456'] + } + expect(criterionToString(item)).toEqual(`${Criterion.CHECK_WALLET_ADDRESS}({"0x123", "0x456"})`) + }) + + it('returns criterion if args is an array of objects', () => { + const item: ERC1155TransfersCriterion = { + criterion: Criterion.CHECK_ERC1155_TRANSFERS, + args: [{ tokenId: 'eip155:137/erc1155:0x12345/123', operator: ValueOperators.LESS_THAN_OR_EQUAL, value: '5' }] + } + expect(criterionToString(item)).toEqual( + `${Criterion.CHECK_ERC1155_TRANSFERS}([${item.args.map((el) => JSON.stringify(el)).join(', ')}])` + ) + }) + + it('returns criterion if args is an object', () => { + const item: IntentAmountCriterion = { + criterion: Criterion.CHECK_INTENT_AMOUNT, + args: { + currency: '*', + operator: ValueOperators.LESS_THAN_OR_EQUAL, + value: '1000000000000000000' + } + } + expect(criterionToString(item)).toEqual(`${Criterion.CHECK_INTENT_AMOUNT}(${JSON.stringify(item.args)})`) + }) + + it('returns approvals criterion', () => { + const item: ApprovalsCriterion = { + criterion: Criterion.CHECK_APPROVALS, + args: [ + { + approvalCount: 2, + countPrincipal: false, + approvalEntityType: EntityType.User, + entityIds: ['aa@narval.xyz'] + } + ] + } + expect(criterionToString(item)).toEqual( + `approvals = ${Criterion.CHECK_APPROVALS}([${item.args.map((el) => JSON.stringify(el)).join(', ')}])` + ) + }) +}) + +describe('reasonToString', () => { + it('returns reason for PERMIT rules', () => { + const item: Policy = { + then: Then.PERMIT, + name: 'policyId', + when: [] + } + expect(reasonToString(item)).toBe( + 'reason = {"type":"permit","policyId":"policyId","approvalsSatisfied":approvals.approvalsSatisfied,"approvalsMissing":approvals.approvalsMissing}' + ) + }) + + it('returns reason for FORBID rules', () => { + const item: Policy = { + then: Then.FORBID, + name: 'policyId', + when: [] + } + expect(reasonToString(item)).toBe( + 'reason = {"type":"forbid","policyId":"policyId","approvalsSatisfied":[],"approvalsMissing":[]}' + ) + }) +}) diff --git a/apps/authz/src/shared/types/policy.type.ts b/apps/authz/src/shared/types/policy.type.ts index cc54c56c6..6dda5f06a 100644 --- a/apps/authz/src/shared/types/policy.type.ts +++ b/apps/authz/src/shared/types/policy.type.ts @@ -249,7 +249,7 @@ class BaseCriterion { criterion: Criterion } -class ActionCriterion extends BaseCriterion { +export class ActionCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_ACTION) criterion: typeof Criterion.CHECK_ACTION @@ -257,14 +257,14 @@ class ActionCriterion extends BaseCriterion { args: Action[] } -class ResourceIntegrityCriterion extends BaseCriterion { +export class ResourceIntegrityCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_RESOURCE_INTEGRITY) criterion: typeof Criterion.CHECK_RESOURCE_INTEGRITY args: null } -class PrincipalIdCriterion extends BaseCriterion { +export class PrincipalIdCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_PRINCIPAL_ID) criterion: typeof Criterion.CHECK_PRINCIPAL_ID @@ -272,7 +272,7 @@ class PrincipalIdCriterion extends BaseCriterion { args: string[] } -class PrincipalRoleCriterion extends BaseCriterion { +export class PrincipalRoleCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_PRINCIPAL_ROLE) criterion: typeof Criterion.CHECK_PRINCIPAL_ROLE @@ -280,7 +280,7 @@ class PrincipalRoleCriterion extends BaseCriterion { args: UserRole[] } -class PrincipalGroupCriterion extends BaseCriterion { +export class PrincipalGroupCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_PRINCIPAL_GROUP) criterion: typeof Criterion.CHECK_PRINCIPAL_GROUP @@ -288,7 +288,7 @@ class PrincipalGroupCriterion extends BaseCriterion { args: string[] } -class WalletIdCriterion extends BaseCriterion { +export class WalletIdCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_WALLET_ID) criterion: typeof Criterion.CHECK_WALLET_ID @@ -296,7 +296,7 @@ class WalletIdCriterion extends BaseCriterion { args: string[] } -class WalletAddressCriterion extends BaseCriterion { +export class WalletAddressCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_WALLET_ADDRESS) criterion: typeof Criterion.CHECK_WALLET_ADDRESS @@ -304,7 +304,7 @@ class WalletAddressCriterion extends BaseCriterion { args: string[] } -class WalletAccountTypeCriterion extends BaseCriterion { +export class WalletAccountTypeCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_WALLET_ACCOUNT_TYPE) criterion: typeof Criterion.CHECK_WALLET_ACCOUNT_TYPE @@ -312,7 +312,7 @@ class WalletAccountTypeCriterion extends BaseCriterion { args: AccountType[] } -class WalletChainIdCriterion extends BaseCriterion { +export class WalletChainIdCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_WALLET_CHAIN_ID) criterion: typeof Criterion.CHECK_WALLET_CHAIN_ID @@ -320,7 +320,7 @@ class WalletChainIdCriterion extends BaseCriterion { args: string[] } -class WalletGroupCriterion extends BaseCriterion { +export class WalletGroupCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_WALLET_GROUP) criterion: typeof Criterion.CHECK_WALLET_GROUP @@ -328,7 +328,7 @@ class WalletGroupCriterion extends BaseCriterion { args: string[] } -class IntentTypeCriterion extends BaseCriterion { +export class IntentTypeCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_INTENT_TYPE) criterion: typeof Criterion.CHECK_INTENT_TYPE @@ -336,7 +336,7 @@ class IntentTypeCriterion extends BaseCriterion { args: Intents[] } -class DestinationIdCriterion extends BaseCriterion { +export class DestinationIdCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_DESTINATION_ID) criterion: typeof Criterion.CHECK_DESTINATION_ID @@ -345,7 +345,7 @@ class DestinationIdCriterion extends BaseCriterion { args: AccountId[] } -class DestinationAddressCriterion extends BaseCriterion { +export class DestinationAddressCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_DESTINATION_ADDRESS) criterion: typeof Criterion.CHECK_DESTINATION_ADDRESS @@ -353,7 +353,7 @@ class DestinationAddressCriterion extends BaseCriterion { args: string[] } -class DestinationAccountTypeCriterion extends BaseCriterion { +export class DestinationAccountTypeCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_DESTINATION_ACCOUNT_TYPE) criterion: typeof Criterion.CHECK_DESTINATION_ACCOUNT_TYPE @@ -361,7 +361,7 @@ class DestinationAccountTypeCriterion extends BaseCriterion { args: AccountType[] } -class DestinationClassificationCriterion extends BaseCriterion { +export class DestinationClassificationCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_DESTINATION_CLASSIFICATION) criterion: typeof Criterion.CHECK_DESTINATION_CLASSIFICATION @@ -369,7 +369,7 @@ class DestinationClassificationCriterion extends BaseCriterion { args: string[] } -class IntentContractCriterion extends BaseCriterion { +export class IntentContractCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_INTENT_CONTRACT) criterion: typeof Criterion.CHECK_INTENT_CONTRACT @@ -378,7 +378,7 @@ class IntentContractCriterion extends BaseCriterion { args: AccountId[] } -class IntentTokenCriterion extends BaseCriterion { +export class IntentTokenCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_INTENT_TOKEN) criterion: typeof Criterion.CHECK_INTENT_TOKEN @@ -387,7 +387,7 @@ class IntentTokenCriterion extends BaseCriterion { args: AssetId[] } -class IntentSpenderCriterion extends BaseCriterion { +export class IntentSpenderCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_INTENT_SPENDER) criterion: typeof Criterion.CHECK_INTENT_SPENDER @@ -396,7 +396,7 @@ class IntentSpenderCriterion extends BaseCriterion { args: AccountId[] } -class IntentChainIdCriterion extends BaseCriterion { +export class IntentChainIdCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_INTENT_CHAIN_ID) criterion: typeof Criterion.CHECK_INTENT_CHAIN_ID @@ -404,7 +404,7 @@ class IntentChainIdCriterion extends BaseCriterion { args: string[] } -class IntentHexSignatureCriterion extends BaseCriterion { +export class IntentHexSignatureCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_INTENT_HEX_SIGNATURE) criterion: typeof Criterion.CHECK_INTENT_HEX_SIGNATURE @@ -413,7 +413,7 @@ class IntentHexSignatureCriterion extends BaseCriterion { args: Hex[] } -class IntentAmountCriterion extends BaseCriterion { +export class IntentAmountCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_INTENT_AMOUNT) criterion: typeof Criterion.CHECK_INTENT_AMOUNT @@ -422,7 +422,7 @@ class IntentAmountCriterion extends BaseCriterion { args: AmountCondition } -class ERC721TokenIdCriterion extends BaseCriterion { +export class ERC721TokenIdCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_ERC721_TOKEN_ID) criterion: typeof Criterion.CHECK_ERC721_TOKEN_ID @@ -431,7 +431,7 @@ class ERC721TokenIdCriterion extends BaseCriterion { args: AssetId[] } -class ERC1155TokenIdCriterion extends BaseCriterion { +export class ERC1155TokenIdCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_ERC1155_TOKEN_ID) criterion: typeof Criterion.CHECK_ERC1155_TOKEN_ID @@ -440,7 +440,7 @@ class ERC1155TokenIdCriterion extends BaseCriterion { args: AssetId[] } -class ERC1155TransfersCriterion extends BaseCriterion { +export class ERC1155TransfersCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_ERC1155_TRANSFERS) criterion: typeof Criterion.CHECK_ERC1155_TRANSFERS @@ -449,7 +449,7 @@ class ERC1155TransfersCriterion extends BaseCriterion { args: ERC1155AmountCondition[] } -class IntentMessageCriterion extends BaseCriterion { +export class IntentMessageCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_INTENT_MESSAGE) criterion: typeof Criterion.CHECK_INTENT_MESSAGE @@ -458,7 +458,7 @@ class IntentMessageCriterion extends BaseCriterion { args: SignMessageCondition } -class IntentPayloadCriterion extends BaseCriterion { +export class IntentPayloadCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_INTENT_PAYLOAD) criterion: typeof Criterion.CHECK_INTENT_PAYLOAD @@ -466,7 +466,7 @@ class IntentPayloadCriterion extends BaseCriterion { args: string[] } -class IntentAlgorithmCriterion extends BaseCriterion { +export class IntentAlgorithmCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_INTENT_ALGORITHM) criterion: typeof Criterion.CHECK_INTENT_ALGORITHM @@ -474,7 +474,7 @@ class IntentAlgorithmCriterion extends BaseCriterion { args: Alg[] } -class IntentDomainCriterion extends BaseCriterion { +export class IntentDomainCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_INTENT_DOMAIN) criterion: typeof Criterion.CHECK_INTENT_DOMAIN @@ -483,7 +483,7 @@ class IntentDomainCriterion extends BaseCriterion { args: SignTypedDataDomainCondition } -class PermitDeadlineCriterion extends BaseCriterion { +export class PermitDeadlineCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_PERMIT_DEADLINE) criterion: typeof Criterion.CHECK_PERMIT_DEADLINE @@ -492,7 +492,7 @@ class PermitDeadlineCriterion extends BaseCriterion { args: PermitDeadlineCondition } -class GasFeeAmountCriterion extends BaseCriterion { +export class GasFeeAmountCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_GAS_FEE_AMOUNT) criterion: typeof Criterion.CHECK_GAS_FEE_AMOUNT @@ -501,21 +501,21 @@ class GasFeeAmountCriterion extends BaseCriterion { args: AmountCondition } -class NonceRequiredCriterion extends BaseCriterion { +export class NonceRequiredCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_NONCE_EXISTS) criterion: typeof Criterion.CHECK_NONCE_EXISTS args: null } -class NonceNotRequiredCriterion extends BaseCriterion { +export class NonceNotRequiredCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_NONCE_NOT_EXISTS) criterion: typeof Criterion.CHECK_NONCE_NOT_EXISTS args: null } -class ApprovalsCriterion extends BaseCriterion { +export class ApprovalsCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_APPROVALS) criterion: typeof Criterion.CHECK_APPROVALS @@ -524,7 +524,7 @@ class ApprovalsCriterion extends BaseCriterion { args: ApprovalCondition[] } -class SpendingLimitCriterion extends BaseCriterion { +export class SpendingLimitCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_SPENDING_LIMIT) criterion: typeof Criterion.CHECK_SPENDING_LIMIT @@ -533,6 +533,15 @@ class SpendingLimitCriterion extends BaseCriterion { args: SpendingLimitCondition } +export type SetPolicyRulesAction = BaseAction & { + action: typeof Action.SET_POLICY_RULES + data: Policy[] +} + +export type SetPolicyRulesRequest = BaseAdminRequest & { + request: SetPolicyRulesAction +} + const SUPPORTED_CRITERION = [ ActionCriterion, ResourceIntegrityCriterion, @@ -707,12 +716,3 @@ const instantiateCriterion = (criterion: PolicyCriterion) => { throw new Error('Unknown criterion: ' + criterion) } } - -export type SetPolicyRulesAction = BaseAction & { - action: typeof Action.SET_POLICY_RULES - data: Policy[] -} - -export type SetPolicyRulesRequest = BaseAdminRequest & { - request: SetPolicyRulesAction -} diff --git a/apps/authz/src/shared/utils/opa.utils.ts b/apps/authz/src/shared/utils/opa.utils.ts new file mode 100644 index 000000000..9de10d320 --- /dev/null +++ b/apps/authz/src/shared/utils/opa.utils.ts @@ -0,0 +1,45 @@ +import { isEmpty } from 'lodash' +import { Criterion, Policy, PolicyCriterion, Then } from '../types/policy.type' + +export const criterionToString = (item: PolicyCriterion) => { + const criterion: Criterion = item.criterion + const args = item.args + + if (!isEmpty(args)) { + if (Array.isArray(args)) { + if (typeof args[0] === 'string') { + return `${criterion}({${args.map((el) => `"${el}"`).join(', ')}})` + } + + if (criterion === Criterion.CHECK_APPROVALS) { + return `approvals = ${criterion}([${args.map((el) => JSON.stringify(el)).join(', ')}])` + } + + return `${criterion}([${args.map((el) => JSON.stringify(el)).join(', ')}])` + } + + return `${criterion}(${JSON.stringify(args)})` + } + + return `${criterion}` +} + +export const reasonToString = (item: Policy) => { + if (item.then === Then.PERMIT) { + const reason = [ + `"type":"${item.then}"`, + `"policyId":"${item.name}"`, + '"approvalsSatisfied":approvals.approvalsSatisfied', + '"approvalsMissing":approvals.approvalsMissing' + ] + return `reason = {${reason.join(',')}}` + } + + const reason = { + type: item.then, + policyId: item.name, + approvalsSatisfied: [], + approvalsMissing: [] + } + return `reason = ${JSON.stringify(reason)}` +}