Skip to content

Commit

Permalink
Copying over opa-policy.ts script & mock data & types into authz app
Browse files Browse the repository at this point in the history
  • Loading branch information
mattschoch committed Jan 11, 2024
1 parent 202831e commit 3260aa1
Show file tree
Hide file tree
Showing 26 changed files with 3,573 additions and 12 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ node_modules
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace

# misc
/.sass-cache
Expand Down Expand Up @@ -52,3 +53,5 @@ Thumbs.db
.env.staging
.env.production
.env.test

/rego-build
5 changes: 5 additions & 0 deletions apps/authz/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@ authz/test/integration:

authz/test/e2e:
npx nx test:e2e ${AUTHZ_PROJECT_NAME}

authz/rego/compile:
mkdir -p ./rego-build
opa build -t wasm -e main/evaluate -o ./rego-build/policies.gz ${AUTHZ_PROJECT_DIR}/src/app/opa/rego/main.rego
tar -xzf ./rego-build/policies.gz -C ./rego-build/
18 changes: 17 additions & 1 deletion apps/authz/src/app/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { transactionRequestIntent } from '@narval/transaction-request-intent'
import { Controller, Get, Logger } from '@nestjs/common'
import { Controller, Get, Logger, Post } from '@nestjs/common'

import { generateInboundRequest } from '@app/authz/shared/module/persistence/mock_data'
import { AppService } from './app.service'

@Controller()
Expand All @@ -23,4 +24,19 @@ export class AppController {

return 'pong'
}

@Post('/evaluation')
async evaluate() {
this.logger.log({
message: 'Received evaluation',
})
const fakeRequest = await generateInboundRequest()
const result = await this.appService.runEvaluation(fakeRequest)
this.logger.log({
message: 'Evaluation Result',
result
})

return result
}
}
21 changes: 17 additions & 4 deletions apps/authz/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
import { Module } from '@nestjs/common'
import { Logger, Module, OnApplicationBootstrap } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { PersistenceModule } from '@app/authz/shared/module/persistence/persistence.module'
import { load } from './app.config'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { OpaService } from './opa/opa.service'

@Module({
imports: [
ConfigModule.forRoot({
load: [load]
})
}),
PersistenceModule
],
controllers: [AppController],
providers: [AppService]
providers: [AppService, OpaService]
})
export class AppModule {}
export class AppModule implements OnApplicationBootstrap {
private logger = new Logger(AppModule.name)

constructor(private opaService: OpaService) {}

async onApplicationBootstrap() {
this.logger.log('Armory Engine app module boot')

await this.opaService.onApplicationBootstrap()
}
}
138 changes: 137 additions & 1 deletion apps/authz/src/app/app.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,144 @@
import {
ApprovalSignature,
AuthZRequest,
AuthZRequestPayload,
AuthZResponse,
NarvalDecision
} from '@app/authz/shared/types/http'
import { Injectable } from '@nestjs/common'
import { recoverMessageAddress } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { BlockchainActions } from '@app/authz/shared/types/enums'
import { OpaResult, RegoInput } from '@app/authz/shared/types/rego'
import { PersistenceRepository } from '@app/authz/shared/module/persistence/persistence.repository'
import { OpaService } from './opa/opa.service'

const ENGINE_PRIVATE_KEY = '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5'

@Injectable()
export class AppService {
constructor(private persistenceRepository: PersistenceRepository, private opaService: OpaService) {}
getData(): { message: string } {
return { message: 'Hello API' }
return { message: 'Hello AuthZ API' }
}

async #populateApprovals(
approvalSignatures: `0x${string}`[] | undefined,
verificationMessage: string
): Promise<ApprovalSignature[] | null> {
if (!approvalSignatures) return null
const approvalSigs = await Promise.all(
approvalSignatures.map(async (sig) => {
const address = await recoverMessageAddress({
message: verificationMessage,
signature: sig
})
const userId = await this.persistenceRepository.getUserForAddress(address);

return {
signature: sig,
address,
userId
}
})
)
return approvalSigs
}

#getRegoInputFromRequest(
principalId: string,
request: AuthZRequest,
approvals: ApprovalSignature[] | null
): RegoInput {
const intent = request.activityType === BlockchainActions.SIGN_TRANSACTION ? request.intent : undefined
// intent only exists in SignTransaction actions
return {
activityType: request.activityType,
intent,
request: request.transactionRequest,
principal: {
uid: principalId
},
resource: {
uid: request.resourceId
},
signatures: approvals?.map((a) => ({ signer: a.userId })) || []
}
}

#finalizeDecision(response: OpaResult[]) {
const firstResponse = response[0]
if (firstResponse.result.permit === false && !firstResponse.result.confirms?.length) {
return {
originalResponse: firstResponse,
decision: NarvalDecision.Forbid
}
}
// TODO: also verify errors

if (firstResponse.result.confirms?.length) {
// 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: []
}
}

async runEvaluation({ request, authn, approvalSignatures }: AuthZRequestPayload) {
/**
* Actual Eval Flow
*/

// Pre-Process - verify the signature/recover the address
const verificationMessage = JSON.stringify(request.transactionRequest)
const recoveredAddress = await recoverMessageAddress({
message: verificationMessage,
signature: authn.signature
})
console.log('Recovered Principal address', recoveredAddress)
const principalUserId = await this.persistenceRepository.getUserForAddress(recoveredAddress);

if (!principalUserId) throw new Error(`Could not find user for address ${recoveredAddress}`)
// Populate any approval signatures with recovered address/user info
const populatedApprovals = await this.#populateApprovals(approvalSignatures, verificationMessage)

const input = this.#getRegoInputFromRequest(principalUserId, request, populatedApprovals)

// Actual Rego Evaluation
const resultSet: OpaResult[] = await this.opaService.evaluate(input)

console.log('OPA Result Set', JSON.stringify(resultSet, null, 2))

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

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

// If we are allowing, then the ENGINE signs the verification too
if (finalDecision.decision === NarvalDecision.Permit) {
const permitSignature = await privateKeyToAccount(ENGINE_PRIVATE_KEY).signMessage({
message: verificationMessage
})
authzResponse.permitSignature = permitSignature
}

console.log('End')

return authzResponse;
}
}
35 changes: 35 additions & 0 deletions apps/authz/src/app/opa/opa.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { PersistenceRepository } from '@app/authz/shared/module/persistence/persistence.repository'
import { OpaResult, RegoInput } from '@app/authz/shared/types/rego'
import { Injectable, Logger } from '@nestjs/common'
import { loadPolicy } from '@open-policy-agent/opa-wasm'
import { readFileSync } from 'fs'
import path from 'path'

type PromiseType<T extends Promise<unknown>> = T extends Promise<infer U> ? U : never
type OpaEngine = PromiseType<ReturnType<typeof loadPolicy>>

const OPA_WASM_PATH = path.join(process.cwd(), './rego-build/policy.wasm')

@Injectable()
export class OpaService {
private logger = new Logger(OpaService.name)
private opaEngine: OpaEngine | undefined

constructor(private persistenceRepository: PersistenceRepository) {}

async onApplicationBootstrap() {
this.logger.log('OPA Service boot')
const policyWasmPath = OPA_WASM_PATH
const policyWasm = readFileSync(policyWasmPath)
const opaEngine = await loadPolicy(policyWasm)
const data = await this.persistenceRepository.getEntityData()
opaEngine.setData(data)
this.opaEngine = opaEngine
}

async evaluate(input: RegoInput): Promise<OpaResult[]> {
if (!this.opaEngine) throw new Error('OPA Engine not initialized')
const result = await this.opaEngine.evaluate(input)
return result
}
}
Loading

0 comments on commit 3260aa1

Please sign in to comment.