Skip to content

Commit

Permalink
schemas for serialized requests and usage in engine (#238)
Browse files Browse the repository at this point in the history
* schemas for serialized requests and usage in engine

* Zod interceptor to return correct DTO from controller

* added e2e test for serialization
  • Loading branch information
Ptroger authored May 2, 2024
1 parent d3a3011 commit b2cbd6f
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 22 deletions.
31 changes: 30 additions & 1 deletion apps/policy-engine/src/engine/__test__/e2e/evaluation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { ConfigModule, ConfigService } from '@narval/config-module'
import { EncryptionModuleOptionProvider } from '@narval/encryption-module'
import { Action, Criterion, Decision, EvaluationRequest, FIXTURE, Then } from '@narval/policy-engine-shared'
import {
Action,
Criterion,
Decision,
EvaluationRequest,
EvaluationResponse,
FIXTURE,
SerializedEvaluationRequest,
Then
} from '@narval/policy-engine-shared'
import { Alg, PrivateKey, privateKeyToJwk, secp256k1PrivateKeyToJwk } from '@narval/signature'
import { HttpStatus, INestApplication } from '@nestjs/common'
import { Test, TestingModule } from '@nestjs/testing'
Expand All @@ -20,6 +29,7 @@ import {
generateSignMessageRequest,
generateSignRawRequest,
generateSignTransactionRequest,
generateSignTransactionRequestWithGas,
generateSignTypedDataRequest
} from '../../../shared/testing/evaluation.testing'
import { Client } from '../../../shared/type/domain.type'
Expand Down Expand Up @@ -134,6 +144,25 @@ describe('Evaluation', () => {
expect(status).toEqual(HttpStatus.UNAUTHORIZED)
})

it('serializes and parses request at the edge', async () => {
const payload = await generateSignTransactionRequestWithGas()

const serializedPayload = SerializedEvaluationRequest.parse(payload)
const { body } = await request(app.getHttpServer())
.post('/evaluations')
.set(REQUEST_HEADER_CLIENT_ID, client.clientId)
.set(REQUEST_HEADER_CLIENT_SECRET, client.clientSecret)
.send(serializedPayload)

expect(body).toMatchObject({
decision: Decision.FORBID,
request: serializedPayload.request
})

const parsedRequest = EvaluationResponse.parse(body)
expect(parsedRequest.request).toEqual(payload.request)
})

const useCases = [
{
action: Action.SIGN_TRANSACTION,
Expand Down
8 changes: 6 additions & 2 deletions apps/policy-engine/src/engine/engine.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { ConfigModule, ConfigService } from '@narval/config-module'
import { EncryptionModule } from '@narval/encryption-module'
import { HttpModule } from '@nestjs/axios'
import { Module, ValidationPipe } from '@nestjs/common'
import { APP_PIPE } from '@nestjs/core'
import { ZodValidationPipe } from 'nestjs-zod'
import { APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'
import { ZodSerializerInterceptor, ZodValidationPipe } from 'nestjs-zod'
import { load } from '../policy-engine.config'
import { EncryptionModuleOptionFactory } from '../shared/factory/encryption-module-option.factory'
import { AdminApiKeyGuard } from '../shared/guard/admin-api-key.guard'
Expand Down Expand Up @@ -64,6 +64,10 @@ import { HttpDataStoreRepository } from './persistence/repository/http-data-stor
{
provide: APP_PIPE,
useClass: ZodValidationPipe
},
{
provide: APP_INTERCEPTOR,
useClass: ZodSerializerInterceptor
}
],
exports: [EngineService, ProvisionService, BootstrapService]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { Body, Controller, HttpCode, HttpStatus, Post, UseGuards } from '@nestjs/common'
import { ApiHeader, ApiOperation, ApiResponse } from '@nestjs/swagger'
import { ZodSerializerDto } from 'nestjs-zod'
import { REQUEST_HEADER_CLIENT_ID } from '../../../../policy-engine.constant'
import { ClientId } from '../../../../shared/decorator/client-id.decorator'
import { ClientSecretGuard } from '../../../../shared/guard/client-secret.guard'
import { EvaluationService } from '../../../core/service/evaluation.service'
import { EvaluationRequestDto } from '../dto/evaluation-request.dto'
import { SerializedEvaluationResponseDto } from '../dto/serialized-evaluation-response.dto'

@Controller('/evaluations')
@UseGuards(ClientSecretGuard)
Expand All @@ -11,6 +15,17 @@ export class EvaluationController {

@Post()
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Evaluates a request into a decision.'
})
@ApiHeader({
name: REQUEST_HEADER_CLIENT_ID
})
@ApiResponse({
status: HttpStatus.OK,
type: SerializedEvaluationResponseDto
})
@ZodSerializerDto(SerializedEvaluationResponseDto)
async evaluate(@ClientId() clientId: string, @Body() body: EvaluationRequestDto) {
return this.evaluationService.evaluate(clientId, body)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { SerializedEvaluationResponse } from '@narval/policy-engine-shared'
import { createZodDto } from 'nestjs-zod'

export class SerializedEvaluationResponseDto extends createZodDto(SerializedEvaluationResponse) {}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
Address,
CredentialEntity,
Feed,
TransactionRequest,
SerializedTransactionRequest,
UserRole
} from '@narval/policy-engine-shared'
import { Intent } from '@narval/transaction-request-intent'
Expand All @@ -20,7 +20,7 @@ export type OpenPolicyAgentInstance = PromiseType<ReturnType<typeof loadPolicy>>
export type Input = {
action: Action
intent?: Intent
transactionRequest?: TransactionRequest
transactionRequest?: SerializedTransactionRequest
principal: CredentialEntity
resource?: { uid: string }
approvals?: CredentialEntity[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
EvaluationRequest,
Feed,
Request,
SerializedTransactionRequest,
SignMessageAction,
SignRawAction,
SignTransactionAction,
Expand Down Expand Up @@ -37,7 +38,7 @@ const toSignTransaction: Mapping<SignTransactionAction> = (request, principal, a
principal,
approvals,
intent: result.intent,
transactionRequest: request.transactionRequest,
transactionRequest: SerializedTransactionRequest.parse(request.transactionRequest),
resource: { uid: request.resourceId },
feeds
}
Expand Down
28 changes: 28 additions & 0 deletions apps/policy-engine/src/shared/testing/evaluation.testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,34 @@ const sign = async (request: Request) => {
return { aliceSignature, bobSignature, carolSignature }
}

export const generateSignTransactionRequestWithGas = async (): Promise<EvaluationRequest> => {
const txRequest: TransactionRequest = {
from: FIXTURE.WALLET.Engineering.address,
to: FIXTURE.WALLET.Treasury.address,
chainId: 137,
value: toHex(ONE_ETH),
data: '0x00000000',
gas: BigInt(22000),
nonce: 192,
type: '2'
}

const request: Request = {
action: Action.SIGN_TRANSACTION,
nonce: uuid(),
transactionRequest: txRequest,
resourceId: FIXTURE.WALLET.Engineering.id
}

const { aliceSignature, bobSignature, carolSignature } = await sign(request)

return {
authentication: aliceSignature,
request,
approvals: [bobSignature, carolSignature]
}
}

export const generateSignTransactionRequest = async (): Promise<EvaluationRequest> => {
const txRequest: TransactionRequest = {
from: FIXTURE.WALLET.Engineering.address,
Expand Down
12 changes: 12 additions & 0 deletions packages/policy-engine-shared/src/lib/type/action.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ export const TransactionRequest = z.object({
})
export type TransactionRequest = z.infer<typeof TransactionRequest>

export const SerializedTransactionRequest = TransactionRequest.extend({
gas: z.coerce.string().optional(),
maxFeePerGas: z.coerce.string().optional(),
maxPriorityFeePerGas: z.coerce.string().optional()
})
export type SerializedTransactionRequest = z.infer<typeof SerializedTransactionRequest>

export const Eip712Domain = z.object({
name: z.string().optional(),
version: z.string().optional(),
Expand Down Expand Up @@ -76,6 +83,11 @@ export const SignTransactionAction = BaseAction.merge(
)
export type SignTransactionAction = z.infer<typeof SignTransactionAction>

export const SerializedTransactionAction = SignTransactionAction.extend({
transactionRequest: SerializedTransactionRequest
})
export type SerializedTransactionAction = z.infer<typeof SerializedTransactionAction>

// Matching viem's SignableMessage options
// See https://viem.sh/docs/actions/wallet/signMessage#message
export const SignableMessage = z.union([
Expand Down
59 changes: 43 additions & 16 deletions packages/policy-engine-shared/src/lib/type/domain.type.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { ZodTypeAny, z } from 'zod'
import { AccountId } from '../util/caip.util'
import { SignMessageAction, SignRawAction, SignTransactionAction, SignTypedDataAction } from './action.type'
import {
SerializedTransactionAction,
SignMessageAction,
SignRawAction,
SignTransactionAction,
SignTypedDataAction
} from './action.type'

export enum Decision {
PERMIT = 'Permit',
Expand Down Expand Up @@ -92,6 +98,14 @@ export const Request = z.discriminatedUnion('action', [
])
export type Request = z.infer<typeof Request>

export const SerializedRequest = z.discriminatedUnion('action', [
SerializedTransactionAction,
SignMessageAction,
SignTypedDataAction,
SignRawAction
])
export type SerializedRequest = z.infer<typeof SerializedRequest>

export const Feed = <Data extends ZodTypeAny>(dataSchema: Data) =>
z.object({
source: z.string(),
Expand Down Expand Up @@ -137,6 +151,11 @@ export const EvaluationRequest = z
.describe('The action being authorized')
export type EvaluationRequest = z.infer<typeof EvaluationRequest>

export const SerializedEvaluationRequest = EvaluationRequest.extend({
request: SerializedRequest
})
export type SerializedEvaluationRequest = z.infer<typeof SerializedEvaluationRequest>

export const ApprovalRequirement = z.object({
approvalCount: z.number().min(0),
approvalEntityType: z.nativeEnum(EntityType).describe('The number of requried approvals'),
Expand All @@ -145,22 +164,30 @@ export const ApprovalRequirement = z.object({
})
export type ApprovalRequirement = z.infer<typeof ApprovalRequirement>

export type AccessToken = {
value: string // JWT
// could include a key-proof
}
export const AccessToken = z.object({
value: JwtString
})
export type AccessToken = z.infer<typeof AccessToken>

export const EvaluationResponse = z.object({
decision: z.nativeEnum(Decision),
request: Request.optional(),
approvals: z
.object({
required: z.array(ApprovalRequirement).optional(),
missing: z.array(ApprovalRequirement).optional(),
satisfied: z.array(ApprovalRequirement).optional()
})
.optional(),
accessToken: AccessToken.optional(),
transactionRequestIntent: z.unknown().optional()
})
export type EvaluationResponse = z.infer<typeof EvaluationResponse>

export type EvaluationResponse = {
decision: Decision
request?: Request
approvals?: {
required: ApprovalRequirement[]
missing: ApprovalRequirement[]
satisfied: ApprovalRequirement[]
}
accessToken?: AccessToken
transactionRequestIntent?: unknown
}
export const SerializedEvaluationResponse = EvaluationResponse.extend({
request: SerializedRequest.optional()
})
export type SerializedEvaluationResponse = z.infer<typeof SerializedEvaluationResponse>

export type Hex = `0x${string}`

Expand Down

0 comments on commit b2cbd6f

Please sign in to comment.