Skip to content

Commit

Permalink
Engine provision (#166)
Browse files Browse the repository at this point in the history
  • Loading branch information
wcalderipe authored Mar 13, 2024
1 parent ede46d2 commit 9eeebf9
Show file tree
Hide file tree
Showing 58 changed files with 1,139 additions and 590 deletions.
26 changes: 10 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
<p align="center">
<a href="https://www.narval.xyz/" target="blank"><img src="./resource/narval_logo.png" width="150" alt="Narval logo" /></a>
</p>
<p align="center">Narval is the most advanced and secure authorization stack for web3.</p>
<p align="center">
<a href="https://github.com/narval-xyz/narval/actions/workflows/armory.yml" target="_blank"><img src="https://github.com/narval-xyz/narval/actions/workflows/armory.yml/badge.svg?branch=main" alt="@app/armory" /></a>
<a href="https://github.com/narval-xyz/narval/actions/workflows/policy-engine.yml" target="_blank"><img src="https://github.com/narval-xyz/narval/actions/workflows/policy-engine.yml/badge.svg?branch=main" alt="@app/policy-engine" /></a>
<a href="https://github.com/narval-xyz/narval/actions/workflows/transaction-request-intent.yml" target="_blank"><img src="https://github.com/narval-xyz/narval/actions/workflows/transaction-request-intent.yml/badge.svg?branch=main" alt="@narval/transaction-request-intent" /></a>
<a href="https://github.com/narval-xyz/narval/actions/workflows/policy-engine-shared.yml" target="_blank"><img src="https://github.com/narval-xyz/narval/actions/workflows/policy-engine-shared.yml/badge.svg?branch=main" alt="@narval/transaction-request-intent" /></a>
</p>
<p align="center">Armory is the most advanced and secure authorization stack for web3.</p>

## Description
## Project

TBD
| Project | Status |
|---------------------------------------------------------------------------------------|--------|
| [@app/armory](./apps/armory/README.md) | <a href="https://github.com/narval-xyz/narval/actions/workflows/armory.yml" target="_blank"><img src="https://github.com/narval-xyz/narval/actions/workflows/armory.yml/badge.svg?branch=main" alt="@app/armory CI status" /></a> |
| [@app/policy-engine](./apps/policy-engine/README.md) | <a href="https://github.com/narval-xyz/narval/actions/workflows/policy-engine.yml" target="_blank"><img src="https://github.com/narval-xyz/narval/actions/workflows/policy-engine.yml/badge.svg?branch=main" alt="@app/policy-engine CI status" /></a> |
| [@narval/encryption-module](./packages/encryption-module/README.md) | N/A |
| [@narval/policy-engine-shared](./packages/policy-engine-shared/README.md) | <a href="https://github.com/narval-xyz/narval/actions/workflows/policy-engine-shared.yml" target="_blank"><img src="https://github.com/narval-xyz/narval/actions/workflows/policy-engine-shared.yml/badge.svg?branch=main" alt="@narval/policy-engine-shared CI status" /></a> |
| [@narval/signature](./packages/signature/README.md) | N/A |
| [@narval/transaction-request-intent](./packages/transaction-request-intent/README.md) | <a href="https://github.com/narval-xyz/narval/actions/workflows/transaction-request-intent.yml" target="_blank"><img src="https://github.com/narval-xyz/narval/actions/workflows/transaction-request-intent.yml/badge.svg?branch=main" alt="@narval/transaction-request-intent CI status" /></a> |

## Getting started

Expand All @@ -25,13 +26,6 @@ make setup

At the end, you must have a working environment ready to run any application.

## Project

- [@app/armory](./apps/armory/README.md)
- [@app/policy-engine](./apps/policy-engine/README.md)
- [@narval/policy-engine-shared](./packages/policy-engine-shared/README.md)
- [@narval/transaction-request-intent](./packages/transaction-request-intent/README.md)

## Docker

We use Docker & `docker-compose` to run the application's dependencies.
Expand Down
9 changes: 9 additions & 0 deletions apps/policy-engine/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ policy-engine/setup:
make policy-engine/rego/build
make policy-engine/db/setup
make policy-engine/test/db/setup
make policy-engine/cli CMD=provision

policy-engine/copy-default-env:
cp ${POLICY_ENGINE_PROJECT_DIR}/.env.default ${POLICY_ENGINE_PROJECT_DIR}/.env
Expand Down Expand Up @@ -119,6 +120,14 @@ policy-engine/test:
make policy-engine/test/integration
make policy-engine/test/e2e

# === CLI ===

policy-engine/cli:
npx dotenv -e ${POLICY_ENGINE_PROJECT_DIR}/.env -- \
ts-node -r tsconfig-paths/register \
--project ${POLICY_ENGINE_PROJECT_DIR}/tsconfig.app.json \
${POLICY_ENGINE_PROJECT_DIR}/src/cli.ts ${CMD}

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

policy-engine/rego/build:
Expand Down
6 changes: 6 additions & 0 deletions apps/policy-engine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,9 @@ make policy-engine/lint
make policy-engine/format/check
make policy-engine/lint/check
```

## CLI

```bash
make policy-engine/cli CMD=help
```
12 changes: 7 additions & 5 deletions apps/policy-engine/src/app/__test__/e2e/tenant.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { EncryptionModuleOptionProvider } from '@narval/encryption-module'
import { HttpStatus, INestApplication } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { Test, TestingModule } from '@nestjs/testing'
import request from 'supertest'
import { v4 as uuid } from 'uuid'
import { AppModule } from '../../../app/app.module'
import { EncryptionService } from '../../../encryption/core/encryption.service'
import { Config, load } from '../../../policy-engine.config'
import { REQUEST_HEADER_API_KEY } from '../../../policy-engine.constant'
import { KeyValueRepository } from '../../../shared/module/key-value/core/repository/key-value.repository'
import { InMemoryKeyValueRepository } from '../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository'
import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service'
import { getTestRawAesKeyring } from '../../../shared/testing/encryption.testing'
import { EngineService } from '../../core/service/engine.service'
import { CreateTenantDto } from '../../http/rest/dto/create-tenant.dto'
import { TenantRepository } from '../../persistence/repository/tenant.repository'
Expand All @@ -19,7 +20,6 @@ describe('Tenant', () => {
let module: TestingModule
let testPrismaService: TestPrismaService
let tenantRepository: TenantRepository
let encryptionService: EncryptionService
let engineService: EngineService
let configService: ConfigService<Config, true>

Expand All @@ -37,20 +37,22 @@ describe('Tenant', () => {
})
.overrideProvider(KeyValueRepository)
.useValue(new InMemoryKeyValueRepository())
.overrideProvider(EncryptionModuleOptionProvider)
.useValue({
keyring: getTestRawAesKeyring()
})
.compile()

app = module.createNestApplication()

engineService = module.get<EngineService>(EngineService)
tenantRepository = module.get<TenantRepository>(TenantRepository)
testPrismaService = module.get<TestPrismaService>(TestPrismaService)
encryptionService = module.get<EncryptionService>(EncryptionService)
configService = module.get<ConfigService<Config, true>>(ConfigService)

await testPrismaService.truncateAll()
await encryptionService.setup()

await engineService.create({
await engineService.save({
id: configService.get('engine.id', { infer: true }),
masterKey: 'unsafe-test-master-key',
adminApiKey
Expand Down
22 changes: 16 additions & 6 deletions apps/policy-engine/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { EncryptionModule } from '@narval/encryption-module'
import { HttpModule } from '@nestjs/axios'
import { Module, OnApplicationBootstrap, ValidationPipe } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { APP_PIPE } from '@nestjs/core'
import { EncryptionModule } from '../encryption/encryption.module'
import { load } from '../policy-engine.config'
import { EncryptionModuleOptionFactory } from '../shared/factory/encryption-module-option.factory'
import { KeyValueModule } from '../shared/module/key-value/key-value.module'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { DataStoreRepositoryFactory } from './core/factory/data-store-repository.factory'
import { BootstrapService } from './core/service/bootstrap.service'
import { DataStoreService } from './core/service/data-store.service'
import { EngineService } from './core/service/engine.service'
import { ProvisionService } from './core/service/provision.service'
import { SigningService } from './core/service/signing.service'
import { TenantService } from './core/service/tenant.service'
import { TenantController } from './http/rest/controller/tenant.controller'
Expand All @@ -28,8 +30,12 @@ import { TenantRepository } from './persistence/repository/tenant.repository'
isGlobal: true
}),
HttpModule,
EncryptionModule,
KeyValueModule
KeyValueModule,
EncryptionModule.registerAsync({
imports: [AppModule],
inject: [ConfigService, EngineService],
useClass: EncryptionModuleOptionFactory
})
],
controllers: [AppController, TenantController],
providers: [
Expand All @@ -39,18 +45,22 @@ import { TenantRepository } from './persistence/repository/tenant.repository'
DataStoreService,
EngineRepository,
EngineService,
SigningService,
EntityRepository,
FileSystemDataStoreRepository,
HttpDataStoreRepository,
OpaService,
ProvisionService,
SigningService,
TenantRepository,
TenantService,
{
provide: APP_PIPE,
useClass: ValidationPipe
}
]
],
// - The EngineService is required by the EncryptionModule async registration.
// - The ProvisionService is required by the CliModule.
exports: [EngineService, ProvisionService]
})
export class AppModule implements OnApplicationBootstrap {
constructor(private bootstrapService: BootstrapService) {}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { ConfigModule } from '@nestjs/config'
import { Test } from '@nestjs/testing'
import { MockProxy, mock } from 'jest-mock-extended'
import { EngineRepository } from '../../../../../app/persistence/repository/engine.repository'
import { load } from '../../../../../policy-engine.config'
import { KeyValueRepository } from '../../../../../shared/module/key-value/core/repository/key-value.repository'
import { KeyValueService } from '../../../../../shared/module/key-value/core/service/key-value.service'
import { InMemoryKeyValueRepository } from '../../../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository'
import { BootstrapService } from '../../bootstrap.service'
import { EngineService } from '../../engine.service'
import { TenantService } from '../../tenant.service'

describe(BootstrapService.name, () => {
Expand Down Expand Up @@ -41,8 +48,21 @@ describe(BootstrapService.name, () => {
tenantServiceMock.findAll.mockResolvedValue([tenantOne, tenantTwo])

const module = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
load: [load],
isGlobal: true
})
],
providers: [
BootstrapService,
EngineService,
EngineRepository,
KeyValueService,
{
provide: KeyValueRepository,
useClass: InMemoryKeyValueRepository
},
{
provide: TenantService,
useValue: tenantServiceMock
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { EncryptionModule } from '@narval/encryption-module'
import { DataStoreConfiguration, FIXTURE } from '@narval/policy-engine-shared'
import { Test } from '@nestjs/testing'
import { MockProxy, mock } from 'jest-mock-extended'
import { KeyValueService } from '../../../../../shared/module/key-value/core/service/key-value.service'
import { KeyValueRepository } from '../../../../../shared/module/key-value/core/repository/key-value.repository'
import { EncryptKeyValueService } from '../../../../../shared/module/key-value/core/service/encrypt-key-value.service'
import { InMemoryKeyValueRepository } from '../../../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository'
import { getTestRawAesKeyring } from '../../../../../shared/testing/encryption.testing'
import { Tenant } from '../../../../../shared/type/domain.type'
import { TenantRepository } from '../../../../persistence/repository/tenant.repository'
import { DataStoreService } from '../../data-store.service'
Expand Down Expand Up @@ -48,15 +51,21 @@ describe(TenantService.name, () => {
dataStoreServiceMock.fetch.mockResolvedValue(stores)

const module = await Test.createTestingModule({
imports: [
EncryptionModule.register({
keyring: getTestRawAesKeyring()
})
],
providers: [
TenantService,
TenantRepository,
EncryptKeyValueService,
{
provide: DataStoreService,
useValue: dataStoreServiceMock
},
{
provide: KeyValueService,
provide: KeyValueRepository,
useClass: InMemoryKeyValueRepository
}
]
Expand Down
25 changes: 1 addition & 24 deletions apps/policy-engine/src/app/core/service/bootstrap.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,7 @@ export class BootstrapService {
constructor(private tenantService: TenantService) {}

async boot(): Promise<void> {
this.logger.log('Start application bootstrap procedure')

await this.tenantService.onboard(
{
clientId: '012553b0-34e9-4b48-b217-ced3c906cd39',
clientSecret: 'unsafe-dev-secret',
dataStore: {
entity: {
dataUrl: 'http://127.0.0.1:4200/api/data-store',
signatureUrl: 'http://127.0.0.1:4200/api/data-store',
keys: []
},
policy: {
dataUrl: 'http://127.0.0.1:4200/api/data-store',
signatureUrl: 'http://127.0.0.1:4200/api/data-store',
keys: []
}
},
createdAt: new Date(),
updatedAt: new Date()
},
// Disable sync after the onboard because we'll sync it as part of the boot.
{ syncAfter: false }
)
this.logger.log('Start engine bootstrap')

await this.syncTenants()
}
Expand Down
18 changes: 14 additions & 4 deletions apps/policy-engine/src/app/core/service/engine.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export class EngineService {
private engineRepository: EngineRepository
) {}

async getEngine(): Promise<Engine> {
const engine = await this.engineRepository.findById(this.getId())
async getEngineOrThrow(): Promise<Engine> {
const engine = await this.getEngine()

if (engine) {
return engine
Expand All @@ -22,8 +22,18 @@ export class EngineService {
throw new EngineNotProvisionedException()
}

async create(engine: Engine): Promise<Engine> {
return this.engineRepository.create(engine)
async getEngine(): Promise<Engine | null> {
const engine = await this.engineRepository.findById(this.getId())

if (engine) {
return engine
}

return null
}

async save(engine: Engine): Promise<Engine> {
return this.engineRepository.save(engine)
}

private getId(): string {
Expand Down
65 changes: 65 additions & 0 deletions apps/policy-engine/src/app/core/service/provision.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { generateKeyEncryptionKey, generateMasterKey } from '@narval/encryption-module'
import { Injectable, Logger } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { randomBytes } from 'crypto'
import { Config } from '../../../policy-engine.config'
import { EngineService } from './engine.service'

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

constructor(
private configService: ConfigService<Config, true>,
private engineService: EngineService
) {}

async provision(): Promise<void> {
this.logger.log('Start engine provision')

const engine = await this.engineService.getEngine()

const isFirstTime = engine === null

// IMPORTANT: The order of internal methods call matters.

if (isFirstTime) {
await this.createEngine()
await this.maybeSetupEncryption()
}
}

private async createEngine(): Promise<void> {
this.logger.log('Generate admin API key and save engine')

await this.engineService.save({
id: this.getEngineId(),
adminApiKey: randomBytes(20).toString('hex')
})
}

private async maybeSetupEncryption(): Promise<void> {
// Get the engine's latest state.
const engine = await this.engineService.getEngineOrThrow()

if (engine.masterKey) {
return this.logger.log('Skip master key set up because it already exists')
}

const keyring = this.configService.get('keyring', { infer: true })

if (keyring.type === 'raw') {
this.logger.log('Generate and save engine master key')

const { masterPassword } = keyring
const kek = generateKeyEncryptionKey(masterPassword, this.getEngineId())
const masterKey = await generateMasterKey(kek)

await this.engineService.save({ ...engine, masterKey })
}
}

private getEngineId(): string {
return this.configService.get('engine.id', { infer: true })
}
}
Loading

0 comments on commit 9eeebf9

Please sign in to comment.