Skip to content

Commit

Permalink
Support sign message in the Policy Engine (#219)
Browse files Browse the repository at this point in the history
* Support sign message

* Fix engine unit tests
  • Loading branch information
wcalderipe authored Apr 4, 2024
1 parent 80cf3ef commit 071f140
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 97 deletions.
164 changes: 114 additions & 50 deletions apps/policy-engine/src/engine/__test__/e2e/evaluation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { InMemoryKeyValueRepository } from '../../../shared/module/key-value/per
import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service'
import { getEntityStore, getPolicyStore } from '../../../shared/testing/data-store.testing'
import { getTestRawAesKeyring } from '../../../shared/testing/encryption.testing'
import { generateInboundEvaluationRequest } from '../../../shared/testing/evaluation.testing'
import { generateSignMessageRequest, generateSignTransactionRequest } from '../../../shared/testing/evaluation.testing'
import { Client } from '../../../shared/type/domain.type'
import { ClientService } from '../../core/service/client.service'
import { EngineSignerConfigService } from '../../core/service/engine-signer-config.service'
Expand Down Expand Up @@ -114,68 +114,132 @@ describe('Evaluation', () => {
})

describe('POST /evaluations', () => {
it('evaluates a forbid', async () => {
const payload = await generateInboundEvaluationRequest()
describe('when sign transaction', () => {
it('evaluates a forbid', async () => {
const payload = await generateSignTransactionRequest()

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

expect(body).toEqual({
decision: Decision.FORBID,
request: payload.request
expect(body).toEqual({
decision: Decision.FORBID,
request: payload.request
})
expect(status).toEqual(HttpStatus.OK)
})

it('evaluates a permit', async () => {
await clientService.savePolicyStore(
client.clientId,
await getPolicyStore(
[
{
id: 'test-permit-policy',
then: Then.PERMIT,
description: 'test permit policy',
when: [
{
criterion: Criterion.CHECK_ACTION,
args: [Action.SIGN_TRANSACTION]
}
]
}
],
privateKey
)
)

const payload = await generateSignTransactionRequest()

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

expect(body).toMatchObject({
decision: Decision.PERMIT,
request: payload.request,
accessToken: {
value: expect.any(String)
},
approvals: {
missing: [],
required: [],
satisfied: []
}
})
expect(status).toEqual(HttpStatus.OK)
})
expect(status).toEqual(HttpStatus.OK)
})

it('evaluates a permit', async () => {
await clientService.savePolicyStore(
client.clientId,
await getPolicyStore(
[
{
id: 'test-permit-policy',
then: Then.PERMIT,
description: 'test permit policy',
when: [
{
criterion: Criterion.CHECK_ACTION,
args: [Action.SIGN_TRANSACTION]
}
]
}
],
privateKey
describe('when sign message ', () => {
it('evaluates a forbid', async () => {
const payload = await generateSignMessageRequest()

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

expect(body).toEqual({
decision: Decision.FORBID,
request: payload.request
})
expect(status).toEqual(HttpStatus.OK)
})

it('evaluates a permit', async () => {
await clientService.savePolicyStore(
client.clientId,
await getPolicyStore(
[
{
id: 'test-permit-policy',
then: Then.PERMIT,
description: 'test permit policy',
when: [
{
criterion: Criterion.CHECK_ACTION,
args: [Action.SIGN_MESSAGE]
}
]
}
],
privateKey
)
)
)

const payload = await generateInboundEvaluationRequest()
const payload = await generateSignMessageRequest()

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

expect(body).toMatchObject({
decision: Decision.PERMIT,
request: payload.request,
accessToken: {
value: expect.any(String)
},
approvals: {
missing: [],
required: [],
satisfied: []
}
expect(body).toMatchObject({
decision: Decision.PERMIT,
request: payload.request,
accessToken: {
value: expect.any(String)
},
approvals: {
missing: [],
required: [],
satisfied: []
}
})
expect(status).toEqual(HttpStatus.OK)
})
expect(status).toEqual(HttpStatus.OK)
})

it('responds with forbid when client secret is missing', async () => {
const payload = await generateInboundEvaluationRequest()
const payload = await generateSignTransactionRequest()

const { status, body } = await request(app.getHttpServer())
.post('/evaluations')
Expand Down
6 changes: 3 additions & 3 deletions apps/policy-engine/src/engine/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FIXTURE } from '@narval/policy-engine-shared'
import { Controller, Get, Logger, Post } from '@nestjs/common'
import { generateInboundEvaluationRequest } from '../shared/testing/evaluation.testing'
import { generateSignTransactionRequest } from '../shared/testing/evaluation.testing'
import { EvaluationService } from './core/service/evaluation.service'

@Controller()
Expand All @@ -24,7 +24,7 @@ export class AppController {

@Post('/evaluation-demo')
async evaluateDemo() {
const evaluation = await generateInboundEvaluationRequest()
const evaluation = await generateSignTransactionRequest()
this.logger.log('Received evaluation', {
evaluation
})
Expand All @@ -43,6 +43,6 @@ export class AppController {

@Get('/generate-inbound-request')
generateInboundRequest() {
return generateInboundEvaluationRequest()
return generateSignTransactionRequest()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
JwtString,
Policy,
Request,
SignMessageAction,
Then,
toHex
} from '@narval/policy-engine-shared'
Expand Down Expand Up @@ -107,16 +108,22 @@ describe('OpenPolicyAgentEngine', () => {

describe('evaluate', () => {
it('throws OpenPolicyAgentException when action is unsupported', async () => {
const request: Partial<EvaluationRequest> = {
request: {
action: Action.SIGN_MESSAGE,
nonce: 'test-nonce',
resourceId: 'test-resource-id',
message: 'test-message'
}
const request = {
action: 'UNSUPPORTED ACTION',
nonce: 'test-nonce',
resourceId: 'test-resource-id',
message: 'test-message'
}
const evaluation = {
request,
authentication: await getJwt({
privateKey: FIXTURE.UNSAFE_PRIVATE_KEY.Alice,
sub: FIXTURE.USER.Alice.id,
request: request as SignMessageAction
})
}

await expect(() => engine.evaluate(request as EvaluationRequest)).rejects.toThrow(OpenPolicyAgentException)
await expect(() => engine.evaluate(evaluation as EvaluationRequest)).rejects.toThrow(OpenPolicyAgentException)
})

it('evaluates a forbid rule', async () => {
Expand Down Expand Up @@ -154,12 +161,12 @@ describe('OpenPolicyAgentEngine', () => {
}

const evaluation: EvaluationRequest = {
request,
authentication: await getJwt({
privateKey: FIXTURE.UNSAFE_PRIVATE_KEY.Alice,
sub: FIXTURE.USER.Alice.id,
request
}),
request
})
}

const response = await e.evaluate(evaluation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ 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 { Input, OpenPolicyAgentInstance, Result } from './type/open-policy-agent.type'
import { OpenPolicyAgentInstance, Result } from './type/open-policy-agent.type'
import { toData, toInput } from './util/evaluation.util'
import { getRegoRuleTemplatePath } from './util/rego-transpiler.util'
import { build, getRegoCorePath } from './util/wasm-build.util'

const SUPPORTED_ACTIONS: Action[] = [Action.SIGN_MESSAGE, Action.SIGN_TRANSACTION]

export class OpenPolicyAgentEngine implements Engine<OpenPolicyAgentEngine> {
private policies: Policy[]

Expand Down Expand Up @@ -108,7 +110,7 @@ export class OpenPolicyAgentEngine implements Engine<OpenPolicyAgentEngine> {
async evaluate(evaluation: EvaluationRequest): Promise<EvaluationResponse> {
const { action } = evaluation.request

if (action !== Action.SIGN_TRANSACTION) {
if (!SUPPORTED_ACTIONS.includes(action)) {
throw new OpenPolicyAgentException({
message: 'Open Policy Agent engine unsupported action',
suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
Expand Down Expand Up @@ -191,13 +193,11 @@ export class OpenPolicyAgentEngine implements Engine<OpenPolicyAgentEngine> {
})
}

const input: Input = {
...toInput(evaluation),
const input = toInput({
evaluation,
principal: credentials.principal,
// TODO: Why the EvaluationRequest specifies approvals as optional but
// the OPA input doesn't?
approvals: credentials.approvals || []
}
approvals: credentials.approvals
})

// NOTE: When we evaluate an input against the Rego policy core, it returns
// an array of results with an inner result. We perform a typecast here to
Expand Down
Loading

0 comments on commit 071f140

Please sign in to comment.