Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into feature/forward-creat…
Browse files Browse the repository at this point in the history
…e-user-request
  • Loading branch information
samuel committed Feb 14, 2024
2 parents a2c2cd7 + df31c46 commit e2ee392
Show file tree
Hide file tree
Showing 17 changed files with 413 additions and 274 deletions.
13 changes: 3 additions & 10 deletions apps/authz/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -113,35 +113,28 @@ authz/test:

# === Open Policy Agent & Rego ===

authz/rego/compile:
authz/rego/build:
rm -rf ./rego-build
mkdir -p ./rego-build
opa build \
--target wasm \
--entrypoint main/evaluate \
--bundle ${AUTHZ_PROJECT_DIR}/src/opa/rego \
--ignore "__test__" \
--ignore "policies" \
--output ./rego-build/policies.gz
tar -xzf ./rego-build/policies.gz -C ./rego-build/

authz/rego/eval:
make authz/rego/compile

npx ts-node \
--compiler-options "{\"module\":\"CommonJS\"}" \
${AUTHZ_PROJECT_DIR}/src/opa/rego/script.ts

authz/rego/template:
npx dotenv -e ${AUTHZ_PROJECT_DIR}/.env -- \
ts-node -r tsconfig-paths/register \
--project ${AUTHZ_PROJECT_DIR}/tsconfig.app.json ${AUTHZ_PROJECT_DIR}/src/opa/template/script.ts

make authz/rego/eval

authz/rego/test:
opa test \
--format="pretty" \
${AUTHZ_PROJECT_DIR}/src/opa/rego \
--ignore "generated" \
--verbose \
${ARGS}

Expand Down
9 changes: 4 additions & 5 deletions apps/authz/src/app/core/admin.service.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { Injectable } from '@nestjs/common'
import { Policy, SetPolicyRulesRequest } from '../../shared/types/policy.type'
import { OpaService } from '../opa/opa.service'
import { AdminRepository } from '../persistence/repository/admin.repository'

@Injectable()
export class AdminService {
constructor(private adminRepository: AdminRepository, private opaService: OpaService) {}
constructor(private opaService: OpaService) {}

async setPolicyRules(payload: SetPolicyRulesRequest): Promise<{ policies: Policy[]; fileId: string }> {
const fileId = this.opaService.generateRegoFile(payload.request.data)
async setPolicyRules(payload: SetPolicyRulesRequest): Promise<{ fileId: string; policies: Policy[] }> {
const { fileId, policies } = this.opaService.buildPoliciesWasm(payload.request.data)

return { policies: payload.request.data, fileId }
return { fileId, policies }
}
}
82 changes: 27 additions & 55 deletions apps/authz/src/app/opa/opa.service.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'
import { loadPolicy } from '@open-policy-agent/opa-wasm'
import { execSync } from 'child_process'
import { readFileSync, writeFileSync } from 'fs'
import Handlebars from 'handlebars'
import { isEmpty } from 'lodash'
import path from 'path'
import * as R from 'remeda'
import { v4 as uuidv4 } from 'uuid'
import { RegoData, User, UserGroup, WalletGroup } from '../../shared/types/entities.types'
import { Criterion, Policy, Then } from '../../shared/types/policy.type'
import { Policy } from '../../shared/types/policy.type'
import { OpaResult, RegoInput } from '../../shared/types/rego'
import { criterionToString, reasonToString } from '../../shared/utils/opa.utils'
import { AdminRepository } from '../persistence/repository/admin.repository'

type PromiseType<T extends Promise<unknown>> = T extends Promise<infer U> ? U : never
Expand All @@ -26,12 +27,20 @@ export class OpaService implements OnApplicationBootstrap {
async onApplicationBootstrap(): Promise<void> {
this.logger.log('OPA Service boot')
const policyWasmPath = OPA_WASM_PATH
const policyWasm = readFileSync(policyWasmPath)
const opaEngine = await loadPolicy(policyWasm, undefined, {
'time.now_ns': () => new Date().getTime() * 1000000 // TODO: @sam this happens on app bootstrap one time; if you need a timestamp per-request then this needs to be passed in w/ Entity data not into the Policy.
})
this.opaEngine = opaEngine
await this.reloadEntityData()
try {
const policyWasm = readFileSync(policyWasmPath)
const opaEngine = await loadPolicy(policyWasm, undefined, {
'time.now_ns': () => new Date().getTime() * 1000000 // TODO: @sam this happens on app bootstrap one time; if you need a timestamp per-request then this needs to be passed in w/ Entity data not into the Policy.
})
this.opaEngine = opaEngine
await this.reloadEntityData()
} catch (err) {
if (err.code === 'ENOENT') {
this.logger.error(`Policy wasm not found at ${policyWasmPath}`)
} else {
throw err
}
}
}

async evaluate(input: RegoInput): Promise<OpaResult[]> {
Expand All @@ -40,58 +49,17 @@ export class OpaService implements OnApplicationBootstrap {
return evalResult.map(({ result }) => result)
}

generateRegoFile(policies: Policy[]): string {
Handlebars.registerHelper('criterion', function (item) {
const criterion: Criterion = item.criterion
const args = item.args

if (args === null) {
return `${criterion}`
}

if (!isEmpty(args)) {
if (Array.isArray(args)) {
if (typeof args[0] === 'string') {
return `${criterion}({${args.map((el) => `"${el}"`).join(', ')}})`
}

if (criterion === Criterion.CHECK_APPROVALS) {
return `approvals = ${criterion}([${args.map((el) => JSON.stringify(el)).join(', ')}])`
}

return `${criterion}([${args.map((el) => JSON.stringify(el)).join(', ')}])`
}

return `${criterion}(${JSON.stringify(args)})`
}
})
buildPoliciesWasm(payload: Policy[]): { fileId: string; policies: Policy[] } {
Handlebars.registerHelper('criterion', criterionToString)

Handlebars.registerHelper('reason', function (item) {
if (item.then === Then.PERMIT) {
const reason = [
`"type": "${item.then}"`,
`"policyId": "${item.name}"`,
'"approvalsSatisfied": approvals.approvalsSatisfied',
'"approvalsMissing": approvals.approvalsMissing'
]
return `reason = {${reason.join(', ')}}`
}

if (item.then === Then.FORBID) {
const reason = {
type: item.then,
policyId: item.name,
approvalsSatisfied: [],
approvalsMissing: []
}
return `reason = ${JSON.stringify(reason)}`
}
})
Handlebars.registerHelper('reason', reasonToString)

const templateSource = readFileSync('./apps/authz/src/opa/template/template.hbs', 'utf-8')

const template = Handlebars.compile(templateSource)

const policies = payload.map((p) => ({ ...p, id: uuidv4() }))

const regoContent = template({ policies })

const fileId = uuidv4()
Expand All @@ -100,7 +68,11 @@ export class OpaService implements OnApplicationBootstrap {

this.logger.log('Policy .rego file generated successfully.')

return fileId
execSync('make authz/rego/build')

this.logger.log('Policies .wasm file build successfully.')

return { fileId, policies }
}

private async fetchEntityData(): Promise<RegoData> {
Expand Down
6 changes: 4 additions & 2 deletions apps/authz/src/opa/rego/lib/main.rego
Original file line number Diff line number Diff line change
Expand Up @@ -54,23 +54,25 @@ default evaluate = {
"default": true,
}

permit[{"policyId": "allow-root-user"}] = reason {
permit[{"policyId": "allow-root-user", "policyName": "Allow root user"}] = reason {
isPrincipalRootUser

reason = {
"type": "permit",
"policyId": "allow-root-user",
"policyName": "Allow root user",
"approvalsSatisfied": [],
"approvalsMissing": [],
}
}

forbid[{"policyId": "default-forbid-policy"}] = reason {
forbid[{"policyId": "default-forbid-policy", "policyName": "Default Forbid Policy"}] = reason {
false

reason = {
"type": "forbid",
"policyId": "default-forbid-policy",
"policyName": "Default Forbid Policy",
"approvalsSatisfied": [],
"approvalsMissing": [],
}
Expand Down
51 changes: 51 additions & 0 deletions apps/authz/src/opa/rego/lib/policies/meta-permission.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package main

metaPermissions = {
"CREATE_ORGANIZATION",
"CREATE_USER",
"UPDATE_USER",
"CREATE_CREDENTIAL",
"ASSIGN_USER_GROUP",
"ASSIGN_WALLET_GROUP",
"ASSIGN_USER_WALLET",
"DELETE_USER",
"REGISTER_WALLET",
"CREATE_ADDRESS_BOOK_ACCOUNT",
"EDIT_WALLET",
"UNASSIGN_WALLET",
"REGISTER_TOKENS",
"EDIT_USER_GROUP",
"DELETE_USER_GROUP",
"CREATE_WALLET_GROUP",
"DELETE_WALLET_GROUP",
}

permit[{"policyId": "permit-meta-permissions", "policyName": "Permit admin user role for meta permissions"}] = reason {
checkAction(metaPermissions)
checkPrincipalRole({"admin"})
approvals = checkApprovals([{
"approvalCount": 2,
"countPrincipal": false,
"approvalEntityType": "Narval::UserRole",
"entityIds": ["root", "admin"],
}])
reason = {
"type": "permit",
"policyId": "permit-meta-permissions",
"policyName": "Permit admin user role for meta permissions",
"approvalsSatisfied": approvals.approvalsSatisfied,
"approvalsMissing": approvals.approvalsMissing,
}
}

forbid[{"policyId": "forbid-meta-permissions", "policyName": "Forbid member user role for meta permissions"}] = reason {
checkAction(metaPermissions)
checkPrincipalRole({"member"})
reason = {
"type": "forbid",
"policyId": "forbid-meta-permissions",
"policyName": "Forbid member user role for meta permissions",
"approvalsSatisfied": [],
"approvalsMissing": [],
}
}
47 changes: 23 additions & 24 deletions apps/authz/src/opa/rego/policies/e2e.rego
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
package main

permit[{"policyId": "examplePermitPolicy" }] = reason {
checkResourceIntegrity
checkNonceExists
checkAction({"signTransaction"})
checkPrincipalId({"matt@narval.xyz"})
checkWalletId({"eip155:eoa:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b"})
checkIntentType({"transferNative"})
checkIntentToken({"eip155:137/slip44:966"})
checkIntentAmount({"currency":"*","operator":"lte","value":"1000000000000000000"})
approvals = checkApprovals([{"approvalCount":2,"countPrincipal":false,"approvalEntityType":"Narval::User","entityIds":["aa@narval.xyz","bb@narval.xyz"]}, {"approvalCount":1,"countPrincipal":false,"approvalEntityType":"Narval::UserRole","entityIds":["admin"]}])
reason = {"type": "permit", "policyId": "examplePermitPolicy", "approvalsSatisfied": approvals.approvalsSatisfied, "approvalsMissing": approvals.approvalsMissing}
}

forbid[{"policyId": "exampleForbidPolicy" }] = reason {
checkResourceIntegrity
checkNonceExists
checkAction({"signTransaction"})
checkPrincipalId({"matt@narval.xyz"})
checkWalletId({"eip155:eoa:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b"})
checkIntentType({"transferNative"})
checkIntentToken({"eip155:137/slip44:966"})
checkSpendingLimit({"limit":"1000000000000000000","timeWindow":{"type":"rolling","value":43200},"filters":{"tokens":["eip155:137/slip44:966"],"users":["matt@narval.xyz"]}})
reason = {"type":"forbid","policyId":"exampleForbidPolicy","approvalsSatisfied":[],"approvalsMissing":[]}
}
permit[{"policyId": "examplePermitPolicy"}] = reason {
checkResourceIntegrity
checkNonceExists
checkAction({"signTransaction"})
checkPrincipalId({"matt@narval.xyz"})
checkWalletId({"eip155:eoa:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b"})
checkIntentType({"transferNative"})
checkIntentToken({"eip155:137/slip44:966"})
checkIntentAmount({"currency": "*", "operator": "lte", "value": "1000000000000000000"})
approvals = checkApprovals([{"approvalCount": 2, "countPrincipal": false, "approvalEntityType": "Narval::User", "entityIds": ["aa@narval.xyz", "bb@narval.xyz"]}, {"approvalCount": 1, "countPrincipal": false, "approvalEntityType": "Narval::UserRole", "entityIds": ["admin"]}])
reason = {"type": "permit", "policyId": "examplePermitPolicy", "approvalsSatisfied": approvals.approvalsSatisfied, "approvalsMissing": approvals.approvalsMissing}
}

forbid[{"policyId": "exampleForbidPolicy"}] = reason {
checkResourceIntegrity
checkNonceExists
checkAction({"signTransaction"})
checkPrincipalId({"matt@narval.xyz"})
checkWalletId({"eip155:eoa:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b"})
checkIntentType({"transferNative"})
checkIntentToken({"eip155:137/slip44:966"})
checkSpendingLimit({"limit": "1000000000000000000", "timeWindow": {"type": "rolling", "value": 43200}, "filters": {"tokens": ["eip155:137/slip44:966"], "users": ["matt@narval.xyz"]}})
reason = {"type": "forbid", "policyId": "exampleForbidPolicy", "approvalsSatisfied": [], "approvalsMissing": []}
}
62 changes: 61 additions & 1 deletion apps/authz/src/opa/template/mockData.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Action, EntityType, ValueOperators } from '@narval/authz-shared'
import { Intents } from '@narval/transaction-request-intent'

import { Criterion, Policy, Then } from '../../shared/types/policy.type'

export const examplePermitPolicy: Policy = {
Expand Down Expand Up @@ -111,3 +110,64 @@ export const exampleForbidPolicy: Policy = {
export const policies = {
policies: [examplePermitPolicy, exampleForbidPolicy]
}

const metaPermissions = [
Action.CREATE_ORGANIZATION,
Action.CREATE_USER,
Action.UPDATE_USER,
Action.CREATE_CREDENTIAL,
Action.ASSIGN_USER_GROUP,
Action.ASSIGN_WALLET_GROUP,
Action.ASSIGN_USER_WALLET,
Action.DELETE_USER,
Action.REGISTER_WALLET,
Action.CREATE_ADDRESS_BOOK_ACCOUNT,
Action.EDIT_WALLET,
Action.UNASSIGN_WALLET,
Action.REGISTER_TOKENS,
Action.EDIT_USER_GROUP,
Action.DELETE_USER_GROUP,
Action.CREATE_WALLET_GROUP,
Action.DELETE_WALLET_GROUP
]

export const permitMetaPermission: Policy = {
name: 'permitMetaPermission',
when: [
{
criterion: Criterion.CHECK_ACTION,
args: metaPermissions
},
{
criterion: Criterion.CHECK_PRINCIPAL_ROLE,
args: ['admin']
},
{
criterion: Criterion.CHECK_APPROVALS,
args: [
{
approvalCount: 2,
countPrincipal: false,
approvalEntityType: EntityType.UserRole,
entityIds: ['admin', 'root']
}
]
}
],
then: Then.PERMIT
}

export const forbidMetaPermission: Policy = {
name: 'forbidMetaPermission',
when: [
{
criterion: Criterion.CHECK_ACTION,
args: metaPermissions
},
{
criterion: Criterion.CHECK_PRINCIPAL_ROLE,
args: ['admin']
}
],
then: Then.FORBID
}
Loading

0 comments on commit e2ee392

Please sign in to comment.