Skip to content

Commit

Permalink
Add entity repository in the policy engine
Browse files Browse the repository at this point in the history
  • Loading branch information
wcalderipe committed Feb 15, 2024
1 parent ac7dd38 commit e57bd48
Show file tree
Hide file tree
Showing 20 changed files with 166 additions and 952 deletions.
16 changes: 0 additions & 16 deletions apps/authz/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ authz/db/setup:
prisma migrate reset \
--schema ${AUTHZ_DATABASE_SCHEMA} \
--force
make authz/db/seed

@echo ""
@echo "${TERM_GREEN}🛠️ Setting up Authz test database${TERM_NO_COLOR}"
Expand All @@ -65,23 +64,8 @@ authz/db/create-migration:
--schema ${AUTHZ_DATABASE_SCHEMA} \
--name ${NAME}

# To maintain seed data within their respective modules and then import them
# into the main seed.ts file for execution, it's necessary to compile the
# project and resolve its path aliases before running the vanilla JavaScript
# seed entry point.
authz/db/seed:
npx dotenv -e ${AUTHZ_PROJECT_DIR}/.env -- \
ts-node -r tsconfig-paths/register --project ${AUTHZ_PROJECT_DIR}/tsconfig.app.json ${AUTHZ_PROJECT_DIR}/src/shared/module/persistence/seed.ts

# === Testing ===

authz/test/db/setup:
npx dotenv -e ${AUTHZ_PROJECT_DIR}/.env.test --override -- \
prisma migrate reset \
--schema ${AUTHZ_DATABASE_SCHEMA} \
--skip-seed \
--force

authz/test/type:
make authz/db/generate-types
npx tsc \
Expand Down
13 changes: 11 additions & 2 deletions apps/authz/src/app/__test__/e2e/admin.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { Action, Alg, EntityType, Signature, UserRole, ValueOperators } from '@narval/authz-shared'
import { Action, Alg, EntityType, FIXTURE, Signature, UserRole, ValueOperators } from '@narval/authz-shared'
import { Intents } from '@narval/transaction-request-intent'
import { HttpStatus, INestApplication } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { Test, TestingModule } from '@nestjs/testing'
import { readFileSync, unlinkSync } from 'fs'
import { mock } from 'jest-mock-extended'
import request from 'supertest'
import { AppModule } from '../../../app/app.module'
import { PersistenceModule } from '../../../shared/module/persistence/persistence.module'
import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service'
import { Organization } from '../../../shared/types/entities.types'
import { Criterion, Then, TimeWindow } from '../../../shared/types/policy.type'
import { load } from '../../app.config'
import { EntityRepository } from '../../persistence/repository/entity.repository'

const REQUEST_HEADER_ORG_ID = 'x-org-id'
describe('Admin Endpoints', () => {
Expand Down Expand Up @@ -43,6 +45,9 @@ describe('Admin Endpoints', () => {
}

beforeAll(async () => {
const entityRepositoryMock = mock<EntityRepository>()
entityRepositoryMock.fetch.mockResolvedValue(FIXTURE.ENTITIES)

module = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
Expand All @@ -52,9 +57,13 @@ describe('Admin Endpoints', () => {
PersistenceModule,
AppModule
]
}).compile()
})
.overrideProvider(EntityRepository)
.useValue(entityRepositoryMock)
.compile()

testPrismaService = module.get<TestPrismaService>(TestPrismaService)

app = module.createNestApplication()

await app.init()
Expand Down
4 changes: 2 additions & 2 deletions apps/authz/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { AppService } from './app.service'
import { AdminService } from './core/admin.service'
import { AdminController } from './http/rest/controller/admin.controller'
import { OpaService } from './opa/opa.service'
import { AdminRepository } from './persistence/repository/admin.repository'
import { EntityRepository } from './persistence/repository/entity.repository'

@Module({
imports: [
Expand All @@ -24,8 +24,8 @@ import { AdminRepository } from './persistence/repository/admin.repository'
providers: [
AppService,
AdminService,
AdminRepository,
OpaService,
EntityRepository,
{
provide: APP_PIPE,
useClass: ValidationPipe
Expand Down
26 changes: 18 additions & 8 deletions apps/authz/src/app/app.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,20 @@ import {
hashRequest
} from '@narval/authz-shared'
import { safeDecode } from '@narval/transaction-request-intent'
import { Injectable } from '@nestjs/common'
import {
BadRequestException,
Injectable,
InternalServerErrorException,
NotFoundException,
UnprocessableEntityException
} from '@nestjs/common'
import { InputType } from 'packages/transaction-request-intent/src/lib/domain'
import { Intent } from 'packages/transaction-request-intent/src/lib/intent.types'
import { Hex, verifyMessage } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { AdminRepository } from '../app/persistence/repository/admin.repository'
import { OpaResult, RegoInput } from '../shared/types/domain.type'
import { OpaService } from './opa/opa.service'
import { EntityRepository } from './persistence/repository/entity.repository'

const ENGINE_PRIVATE_KEY = '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5'

Expand Down Expand Up @@ -62,11 +68,16 @@ export const finalizeDecision = (response: OpaResult[]) => {

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

async #verifySignature(requestSignature: Signature, verificationMessage: string): Promise<AuthCredential> {
const { pubKey, alg, sig } = requestSignature
const credential = await this.adminRepository.getCredentialForPubKey(pubKey)
const credential = this.entityRepository.getCredentialForPubKey(pubKey)

if (!credential) {
throw new NotFoundException('Credential not found')
}

if (alg === Alg.ES256K) {
// TODO: ensure sig & pubkey begins with 0x
const signature = sig.startsWith('0x') ? sig : `0x${sig}`
Expand All @@ -82,10 +93,9 @@ export class AppService {
sig
})

throw new Error('Invalid Signature')
throw new BadRequestException('Invalid signature')
}
}
// TODO: verify other alg types

return credential
}
Expand Down Expand Up @@ -133,7 +143,7 @@ export class AppService {
}
}

throw new Error(`Unsupported action ${request.action}`)
throw new InternalServerErrorException(`Unsupported action ${request.action}`)
}

/**
Expand Down Expand Up @@ -165,7 +175,7 @@ export class AppService {
: undefined

if (intentResult?.success === false) {
throw new Error(`Could not decode intent: ${intentResult.error.message}`)
throw new UnprocessableEntityException(`Could not decode intent: ${intentResult.error.message}`)
}

const intent = intentResult?.intent
Expand Down
4 changes: 1 addition & 3 deletions apps/authz/src/app/core/admin.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { Injectable, Logger } from '@nestjs/common'
import { Injectable } from '@nestjs/common'
import { Policy, SetPolicyRulesRequest } from '../../shared/types/policy.type'
import { OpaService } from '../opa/opa.service'

@Injectable()
export class AdminService {
private logger = new Logger(AdminService.name)

constructor(private opaService: OpaService) {}

async setPolicyRules(payload: SetPolicyRulesRequest): Promise<{ fileId: string; policies: Policy[] }> {
Expand Down
107 changes: 19 additions & 88 deletions apps/authz/src/app/opa/opa.service.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { Entities } from '@narval/authz-shared'
import { HttpService } from '@nestjs/axios'
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 { indexBy } from 'lodash/fp'
import { ORGANIZATION } from 'packages/authz-shared/src/lib/dev.fixture'
import path from 'path'
import * as R from 'remeda'
import { lastValueFrom, map, tap } from 'rxjs'
import { v4 as uuidv4 } from 'uuid'
import { RegoData, User, UserGroup, WalletGroup } from '../../shared/types/entities.types'
import { v4 as uuid } from 'uuid'
import { RegoData } from '../../shared/types/entities.types'
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'
import { EntityRepository } from '../persistence/repository/entity.repository'

type PromiseType<T extends Promise<unknown>> = T extends Promise<infer U> ? U : never
type OpaEngine = PromiseType<ReturnType<typeof loadPolicy>>
Expand All @@ -25,7 +23,7 @@ export class OpaService implements OnApplicationBootstrap {
private logger = new Logger(OpaService.name)
private opaEngine: OpaEngine | undefined

constructor(private adminRepository: AdminRepository, private httpService: HttpService) {}
constructor(private entityRepository: EntityRepository) {}

async onApplicationBootstrap(): Promise<void> {
this.logger.log('OPA Service boot')
Expand Down Expand Up @@ -61,11 +59,11 @@ export class OpaService implements OnApplicationBootstrap {

const template = Handlebars.compile(templateSource)

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

const regoContent = template({ policies })

const fileId = uuidv4()
const fileId = uuid()

writeFileSync(`./apps/authz/src/opa/rego/generated/${fileId}.rego`, regoContent, 'utf-8')

Expand All @@ -78,90 +76,23 @@ export class OpaService implements OnApplicationBootstrap {
return { fileId, policies }
}

private getEntities(): Promise<Entities> {
return lastValueFrom(
this.httpService
.get<Entities>('http://localhost:3005/store/entities', {
headers: {
'x-org-id': '7d704a62-d15e-4382-a826-1eb41563043b'
}
})
.pipe(
map((response) => response.data),
tap((entities) => {
this.logger.log('Received entities snapshot', entities)
})
)
)
}

private async fetchEntityData(): Promise<RegoData> {
const users = await this.adminRepository.getAllUsers()
const wallets = await this.adminRepository.getAllWallets()
const walletGroups = await this.adminRepository.getAllWalletGroups()
const userWallets = await this.adminRepository.getAllUserWallets()
const userGroups = await this.adminRepository.getAllUserGroups()
const addressBook = await this.adminRepository.getAllAddressBook()
const tokens = await this.adminRepository.getAllTokens()

const entities = await this.getEntities()

console.log('####', entities)

const regoUsers: Record<string, User> = R.indexBy(users, (u) => u.uid)
const regoWallets = R.indexBy(wallets, (w) => w.uid)
const regoAddressBook = R.indexBy(addressBook, (a) => a.uid)
const regoTokens = R.indexBy(tokens, (t) => t.uid)

// Add the assignees into the regoWallets
userWallets.forEach((uw) => {
if (regoWallets[uw.walletId]) {
if (!regoWallets[uw.walletId].assignees) regoWallets[uw.walletId].assignees = []
regoWallets[uw.walletId].assignees?.push(uw.userId)
}
})

const regoUserGroups = userGroups.reduce((acc, ug) => {
if (!acc[ug.userGroupId]) {
acc[ug.userGroupId] = {
uid: ug.userGroupId,
users: []
}
}

acc[ug.userGroupId].users.push(ug.userId)

return acc
}, {} as Record<string, UserGroup>)
const entities = await this.entityRepository.fetch(ORGANIZATION.uid)

const regoWalletGroups = walletGroups.reduce((acc, ug) => {
if (!acc[ug.walletGroupId]) {
acc[ug.walletGroupId] = {
uid: ug.walletGroupId,
wallets: []
}
}

acc[ug.walletGroupId].wallets.push(ug.walletId)

return acc
}, {} as Record<string, WalletGroup>)

const mockData = await this.adminRepository.getEntityData()

const regoData: RegoData = {
const data: RegoData = {
entities: {
users: regoUsers,
wallets: regoWallets,
userGroups: regoUserGroups,
walletGroups: regoWalletGroups,
addressBook: regoAddressBook,
tokens: regoTokens
addressBook: indexBy('uid', entities.addressBook),
users: indexBy('uid', entities.users),
userGroups: indexBy('uid', entities.userGroups),
wallets: indexBy('uid', entities.wallets),
walletGroups: indexBy('uid', entities.walletGroups),
tokens: indexBy('uid', entities.tokens)
}
}
this.logger.log('Fetched OPA Engine data', regoData)

return mockData
this.logger.log('Fetched OPA Engine data', data)

return data
}

async reloadEntityData() {
Expand Down
Loading

0 comments on commit e57bd48

Please sign in to comment.