-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Copying over opa-policy.ts script & mock data & types into authz app
- Loading branch information
1 parent
202831e
commit 3260aa1
Showing
26 changed files
with
3,573 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
Oops, something went wrong.