Skip to content

Commit

Permalink
Accurate finalizeDecision logic + test
Browse files Browse the repository at this point in the history
  • Loading branch information
mattschoch committed Jan 18, 2024
1 parent f1ad88a commit ef6d89b
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 36 deletions.
149 changes: 149 additions & 0 deletions apps/authz/src/app/__test__/unit/app.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { finalizeDecision } from '@app/authz/app/app.service'
import { NarvalDecision, NarvalEntities } from '@app/authz/shared/types/domain.type'
import { OpaResult } from '@app/authz/shared/types/rego'

describe('finalizeDecision', () => {
it('should return Forbid if any of the reasons is Forbid', () => {
const response: OpaResult[] = [
{
decision: 'forbid',
reasons: [
{
policyId: 'forbid-rule-id',
type: 'forbid',
decision: 'forbid',
approvalsMissing: [],
approvalsSatisfied: []
},
{
policyId: 'permit-rule-id',
type: 'permit',
decision: 'permit',
approvalsMissing: [],
approvalsSatisfied: []
}
]
}
]
const result = finalizeDecision(response)
expect(result.decision).toEqual(NarvalDecision.Forbid)
})

it('should return Permit if all of the reasons are Permit', () => {
const response: OpaResult[] = [
{
decision: 'permit',
reasons: [
{
policyId: 'permit-rule-id',
type: 'permit',
decision: 'permit',
approvalsMissing: [],
approvalsSatisfied: []
},
{
policyId: 'permit-rule-id',
type: 'permit',
decision: 'permit',
approvalsMissing: [],
approvalsSatisfied: []
}
]
}
]
const result = finalizeDecision(response)
expect(result.decision).toEqual(NarvalDecision.Permit)
})

it('should return Confirm if any of the reasons are Forbid for a Permit type rule where approvals are missing', () => {
const response: OpaResult[] = [
{
decision: 'forbid',
reasons: [
{
policyId: 'permit-rule-id',
type: 'permit',
decision: 'forbid',
approvalsMissing: [
{
policyId: 'permit-rule-id',
approvalCount: 1,
approvalEntityType: NarvalEntities.User,
entityIds: ['user-id'],
countPrincipal: true
}
],
approvalsSatisfied: []
}
]
}
]
const result = finalizeDecision(response)
expect(result.decision).toEqual(NarvalDecision.Confirm)
})

it('should return all missing, satisfied, and total approvals', () => {
const missingApproval = {
policyId: 'permit-rule-id',
approvalCount: 1,
approvalEntityType: NarvalEntities.User,
entityIds: ['user-id'],
countPrincipal: true
}
const missingApproval2 = {
policyId: 'permit-rule-id-4',
approvalCount: 1,
approvalEntityType: NarvalEntities.User,
entityIds: ['user-id'],
countPrincipal: true
}
const satisfiedApproval = {
policyId: 'permit-rule-id-2',
approvalCount: 1,
approvalEntityType: NarvalEntities.User,
entityIds: ['user-id'],
countPrincipal: true
}
const satisfiedApproval2 = {
policyId: 'permit-rule-id-3',
approvalCount: 1,
approvalEntityType: NarvalEntities.User,
entityIds: ['user-id'],
countPrincipal: true
}
const response: OpaResult[] = [
{
decision: 'forbid',
reasons: [
{
policyId: 'permit-rule-id',
type: 'permit',
decision: 'forbid',
approvalsMissing: [missingApproval],
approvalsSatisfied: [satisfiedApproval]
}
]
},
{
decision: 'forbid',
reasons: [
{
policyId: 'permit-rule-id',
type: 'permit',
decision: 'forbid',
approvalsMissing: [missingApproval2],
approvalsSatisfied: [satisfiedApproval2]
}
]
}
]
const result = finalizeDecision(response)
expect(result).toEqual({
originalResponse: response,
decision: NarvalDecision.Confirm,
totalApprovalsRequired: [missingApproval, missingApproval2, satisfiedApproval, satisfiedApproval2],
approvalsMissing: [missingApproval, missingApproval2],
approvalsSatisfied: [satisfiedApproval, satisfiedApproval2]
})
})
})
5 changes: 0 additions & 5 deletions apps/authz/src/app/__test__/unit/example.spec.ts

This file was deleted.

72 changes: 42 additions & 30 deletions apps/authz/src/app/app.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,45 @@ import { OpaService } from './opa/opa.service'

const ENGINE_PRIVATE_KEY = '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5'

export const finalizeDecision = (response: OpaResult[]) => {
// Explicit Forbid - a Forbid rule type that matches & decides Forbid
const anyExplicitForbid = response.some(
(r) => r.decision === 'forbid' && r.reasons?.some((rr) => rr.decision === 'forbid' && rr.type === 'forbid')
)

const allPermit = response.every(
(r) => r.decision === 'permit' && r.reasons?.every((rr) => rr.decision === 'permit' && rr.type === 'permit')
)

const anyPermitWithMissingApprovals = response.some((r) =>
r.reasons?.some((rr) => rr.decision === 'forbid' && rr.type === 'permit' && rr.approvalsMissing.length > 0)
)

if (anyExplicitForbid) {
return {
originalResponse: response,
decision: NarvalDecision.Forbid,
approvalsMissing: [],
approvalsSatisfied: []
}
}
// Collect all the approvalsMissing & approvalsSatisfied using functional map/flat operators
const approvalsSatisfied = response
.flatMap((r) => r.reasons?.flatMap((rr) => rr.approvalsSatisfied))
.filter((v) => !!v)
const approvalsMissing = response.flatMap((r) => r.reasons?.flatMap((rr) => rr.approvalsMissing)).filter((v) => !!v)
const totalApprovalsRequired = approvalsMissing.concat(approvalsSatisfied)

const decision = allPermit && !anyPermitWithMissingApprovals ? NarvalDecision.Permit : NarvalDecision.Confirm
return {
originalResponse: response,
decision,
totalApprovalsRequired,
approvalsMissing,
approvalsSatisfied
}
}

@Injectable()
export class AppService {
constructor(private persistenceRepository: PersistenceRepository, private opaService: OpaService) {}
Expand Down Expand Up @@ -83,33 +122,6 @@ export class AppService {
}
}

#finalizeDecision(response: OpaResult[]) {
const firstResponse = response[0]
if (firstResponse.decision === 'forbid' && firstResponse.reasons?.every((r) => r.decision === 'forbid')) {
return {
originalResponse: firstResponse,
decision: NarvalDecision.Forbid
}
}
// TODO: also verify errors

if (firstResponse.reasons.some((r) => r.approvalsMissing.length > 0)) {
// TODO: find the approvalsSatisfied and approvalsMissing data & format/return here
return {
originalResponse: firstResponse,
decision: NarvalDecision.Confirm
}
}

return {
originalResponse: firstResponse,
decision: NarvalDecision.Permit,
totalApprovalsRequired: [],
approvalsSatisfied: [],
approvalsMissing: []
}
}

/**
* Actual Eval Flow
*/
Expand Down Expand Up @@ -144,14 +156,14 @@ export class AppService {
console.log('OPA Result Set', JSON.stringify(resultSet, null, 2))

// Post-processing to evaluate multisigs
const finalDecision = this.#finalizeDecision(resultSet)
const finalDecision = finalizeDecision(resultSet)

const authzResponse: AuthZResponse = {
decision: finalDecision.decision,
request,
totalApprovalsRequired: finalDecision.totalApprovalsRequired,
approvalsSatisfied: finalDecision.approvalsSatisfied,
approvalsMissing: finalDecision.approvalsMissing
approvalsMissing: finalDecision.approvalsMissing,
approvalsSatisfied: finalDecision.approvalsSatisfied
}

// If we are allowing, then the ENGINE signs the verification too
Expand Down
2 changes: 1 addition & 1 deletion apps/authz/src/shared/types/domain.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ export type AuthZResponse = {
permitSignature?: RequestSignature // The ENGINE's approval signature
request?: AuthZRequest // The actual authorized request
totalApprovalsRequired?: ApprovalRequirement[]
approvalsSatisfied?: ApprovalRequirement[]
approvalsMissing?: ApprovalRequirement[]
approvalsSatisfied?: ApprovalRequirement[]
}

export type VerifiedApproval = {
Expand Down

0 comments on commit ef6d89b

Please sign in to comment.