Skip to content

Commit

Permalink
Add approval requirements to evaluations results (#502)
Browse files Browse the repository at this point in the history
* Add prisma schema

* Add migration file

* wip

* Fix

* fix test

* fix tests

* fixes after CR

* standardizing authRequest/evalLog/approvalReq decoding & enforce always fully populated object

---------

Co-authored-by: Matt Schoch <matt@narval.xyz>
  • Loading branch information
samteb and mattschoch authored Aug 16, 2024
1 parent af91f35 commit 86b1b2a
Show file tree
Hide file tree
Showing 9 changed files with 326 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ export class AuthorizationRequestService {
id: uuid(),
decision: evaluation.decision,
signature: evaluation.accessToken?.value || null,
approvalRequirements: evaluation.approvals,
transactionRequestIntent: evaluation.transactionRequestIntent,
createdAt: new Date()
}
]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,38 @@
import { Action, AuthorizationRequest, AuthorizationRequestError, Evaluation } from '@narval/policy-engine-shared'
import {
Action,
ApprovalRequirement,
Approvals,
AuthorizationRequest,
AuthorizationRequestError,
Evaluation
} from '@narval/policy-engine-shared'
import {
ApprovalRequirement as ApprovalRequirementModel,
AuthorizationRequestError as AuthorizationRequestErrorModel,
EvaluationLog as EvaluationLogModel,
Prisma
} from '@prisma/client/armory'
import { SetOptional } from 'type-fest'
import { ZodIssueCode, ZodSchema, z } from 'zod'
import { ACTION_REQUEST } from '../../orchestration.constant'
import { DecodeAuthorizationRequestException } from '../../persistence/exception/decode-authorization-request.exception'
import { AuthorizationRequestModel } from '../../persistence/type/model.type'

type Model = SetOptional<AuthorizationRequestModel, 'evaluationLog'>
import { AuthorizationRequestModel, EvaluationLogWithApprovalsModel } from '../../persistence/type/model.type'

const actionSchema = z.nativeEnum(Action)

const buildEvaluation = ({ id, decision, signature, createdAt }: EvaluationLogModel): Evaluation => ({
id,
decision,
signature,
createdAt
})

const buildError = ({ id, message, name }: AuthorizationRequestErrorModel): AuthorizationRequestError => ({
id,
message,
name
})

const buildSharedAttributes = (model: Model): Omit<AuthorizationRequest, 'action' | 'request'> => {
const buildSharedAttributes = (model: AuthorizationRequestModel): Omit<AuthorizationRequest, 'action' | 'request'> => {
return {
id: model.id,
clientId: model.clientId,
status: model.status,
idempotencyKey: model.idempotencyKey,
authentication: model.authnSig,
approvals: z.array(z.string()).parse(model.approvals.filter(({ error }) => !error).map((approval) => approval.sig)),
evaluations: (model.evaluationLog || []).map(buildEvaluation),
evaluations: (model.evaluationLog || []).map(decodeEvaluationLog),
metadata: model.metadata as Prisma.InputJsonObject,
errors: (model.errors || []).map(buildError),
createdAt: model.createdAt,
Expand All @@ -49,7 +46,7 @@ const buildSharedAttributes = (model: Model): Omit<AuthorizationRequest, 'action
* @throws {DecodeAuthorizationRequestException}
* @returns {AuthorizationRequest}
*/
const decode = ({ model, schema }: { model: Model; schema: ZodSchema }): AuthorizationRequest => {
const decode = ({ model, schema }: { model: AuthorizationRequestModel; schema: ZodSchema }): AuthorizationRequest => {
try {
const decode = schema.safeParse(model.request)

Expand Down Expand Up @@ -80,7 +77,7 @@ const decode = ({ model, schema }: { model: Model; schema: ZodSchema }): Authori
* @throws {DecodeAuthorizationRequestException}
* @returns {AuthorizationRequest}
*/
export const decodeAuthorizationRequest = (model: Model): AuthorizationRequest => {
export const decodeAuthorizationRequest = (model: AuthorizationRequestModel): AuthorizationRequest => {
const action = actionSchema.safeParse(model.action)

if (action.success) {
Expand All @@ -104,3 +101,43 @@ export const decodeAuthorizationRequest = (model: Model): AuthorizationRequest =
}
])
}

/**
* Decodes the list of approval requirements into the Approval object type, throws on errors.
*
* @throws {DecodeAuthorizationRequestException}
* @returns {Approvals}
*/
export const decodeApprovalRequirement = (requirements: ApprovalRequirementModel[]): Approvals => {
const result: Required<Approvals> = requirements.reduce(
(acc, r) => {
const parsed = ApprovalRequirement.safeParse(r)
if (!parsed.success) {
throw new DecodeAuthorizationRequestException(parsed.error.issues)
}
const requirement = parsed.data

if (r.isSatisfied) {
acc.satisfied.push(requirement)
} else {
acc.missing.push(requirement)
}

acc.required.push(requirement)

return acc
},
{ required: [], satisfied: [], missing: [] } as Required<Approvals>
)

return result
}

export const decodeEvaluationLog = (evaluation: EvaluationLogWithApprovalsModel): Evaluation => ({
id: evaluation.id,
decision: evaluation.decision,
signature: evaluation.signature,
transactionRequestIntent: evaluation.transactionRequestIntent,
createdAt: evaluation.createdAt,
approvalRequirements: decodeApprovalRequirement(evaluation.approvals)
})
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Action,
AuthorizationRequest,
AuthorizationRequestError,
EntityType,
Evaluation,
FIXTURE,
SignTransaction
Expand Down Expand Up @@ -116,27 +117,40 @@ describe(AuthorizationRequestRepository.name, () => {
id: '404853b2-1338-47f5-be17-a1aa78da8010',
decision: 'Permit',
signature: 'test-signature',
createdAt: new Date()
createdAt: new Date(),
transactionRequestIntent: {
action: Action.SIGN_MESSAGE,
nonce: '99',
resourceId: '239bb48b-f708-47ba-97fa-ef336be4dffe',
message: 'Test request'
},
approvalRequirements: {
required: [
{
approvalCount: 1,
approvalEntityType: EntityType.User,
entityIds: [client.id],
countPrincipal: true
}
],
missing: [
{
approvalCount: 1,
approvalEntityType: EntityType.User,
entityIds: [client.id],
countPrincipal: true
}
],
satisfied: []
}
}

await repository.create({
const { evaluations } = await repository.create({
...signMessageRequest,
evaluations: [permit]
})

const evaluations = await testPrismaService.getClient().evaluationLog.findMany({
where: {
requestId: signMessageRequest.id
}
})

expect(evaluations).toEqual([
{
...permit,
requestId: signMessageRequest.id,
clientId: signMessageRequest.clientId
}
])
expect(evaluations).toEqual([permit])
})

it('creates approvals', async () => {
Expand Down Expand Up @@ -237,8 +251,28 @@ describe(AuthorizationRequestRepository.name, () => {
evaluations: [
{
id: '404853b2-1338-47f5-be17-a1aa78da8010',
decision: 'Permit',
decision: 'Confirm',
signature: 'test-signature',
transactionRequestIntent: null,
approvalRequirements: {
required: [
{
approvalCount: 1,
approvalEntityType: EntityType.User,
entityIds: [client.id],
countPrincipal: true
}
],
missing: [
{
approvalCount: 1,
approvalEntityType: EntityType.User,
entityIds: [client.id],
countPrincipal: true
}
],
satisfied: []
},
createdAt: new Date()
}
]
Expand All @@ -251,6 +285,26 @@ describe(AuthorizationRequestRepository.name, () => {
id: 'cc329386-a2dd-4024-86fd-323a630ed703',
decision: 'Permit',
signature: 'test-signature',
approvalRequirements: {
required: [
{
approvalCount: 1,
approvalEntityType: EntityType.User,
entityIds: [client.id],
countPrincipal: true
}
],
missing: [],
satisfied: [
{
approvalCount: 1,
approvalEntityType: EntityType.User,
entityIds: [client.id],
countPrincipal: true
}
]
},
transactionRequestIntent: null,
createdAt: new Date()
}
]
Expand All @@ -261,6 +315,14 @@ describe(AuthorizationRequestRepository.name, () => {
expect(authzRequestOne.evaluations.length).toEqual(1)
expect(authzRequestTwo.evaluations.length).toEqual(2)
expect(actual?.evaluations.length).toEqual(2)

expect(actual?.evaluations[0].approvalRequirements?.required?.length).toEqual(1)
expect(actual?.evaluations[0].approvalRequirements?.missing?.length).toEqual(1)
expect(actual?.evaluations[0].approvalRequirements?.satisfied?.length).toEqual(0)

expect(actual?.evaluations[1].approvalRequirements?.required?.length).toEqual(1)
expect(actual?.evaluations[1].approvalRequirements?.missing?.length).toEqual(0)
expect(actual?.evaluations[1].approvalRequirements?.satisfied?.length).toEqual(1)
})

it('appends approvals', async () => {
Expand Down
Loading

0 comments on commit 86b1b2a

Please sign in to comment.