Skip to content

Commit

Permalink
Tenant persistence with in-memory key-value storage (#144)
Browse files Browse the repository at this point in the history
  • Loading branch information
wcalderipe authored Mar 4, 2024
1 parent f581e6b commit 3ae44da
Show file tree
Hide file tree
Showing 16 changed files with 378 additions and 8 deletions.
2 changes: 1 addition & 1 deletion apps/policy-engine/src/app/__test__/e2e/admin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { readFileSync, unlinkSync } from 'fs'
import { mock } from 'jest-mock-extended'
import request from 'supertest'
import { AppModule } from '../../../app/app.module'
import { load } from '../../../policy-engine.config'
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'
Expand Down
2 changes: 1 addition & 1 deletion apps/policy-engine/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { HttpModule } from '@nestjs/axios'
import { Module, ValidationPipe } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { APP_PIPE } from '@nestjs/core'
import { load } from './app.config'
import { load } from '../policy-engine.config'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { AdminService } from './core/admin.service'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { DataStoreConfiguration } from '@narval/policy-engine-shared'
import { Test } from '@nestjs/testing'
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 { Tenant } from '../../../../../shared/types/domain.type'
import { TenantRepository } from '../../../repository/tenant.repository'

describe(TenantRepository.name, () => {
let repository: TenantRepository
let inMemoryKeyValueRepository: InMemoryKeyValueRepository

beforeEach(async () => {
inMemoryKeyValueRepository = new InMemoryKeyValueRepository()

const module = await Test.createTestingModule({
providers: [
KeyValueService,
TenantRepository,
{
provide: KeyValueRepository,
useValue: inMemoryKeyValueRepository
}
]
}).compile()

repository = module.get<TenantRepository>(TenantRepository)
})

describe('create', () => {
const now = new Date()

const dataStoreConfiguration: DataStoreConfiguration = {
dataUrl: 'a-url-that-doesnt-need-to-exist-for-the-purpose-of-this-test',
signatureUrl: 'a-url-that-doesnt-need-to-exist-for-the-purpose-of-this-test',
keys: []
}

const tenant: Tenant = {
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
dataStore: {
entity: dataStoreConfiguration,
policy: dataStoreConfiguration
},
createdAt: now,
updatedAt: now
}

it('creates a new tenant', async () => {
await repository.create(tenant)

const value = await inMemoryKeyValueRepository.get(repository.getKey(tenant.clientId))
const actualTenant = await repository.findByClientId(tenant.clientId)

expect(value).not.toEqual(null)
expect(tenant).toEqual(actualTenant)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common'
import { KeyValueService } from '../../../shared/module/key-value/core/service/key-value.service'
import { tenantSchema } from '../../../shared/schema/tenant.schema'
import { Tenant } from '../../../shared/types/domain.type'

@Injectable()
export class TenantRepository {
constructor(private keyValueService: KeyValueService) {}

async findByClientId(clientId: string): Promise<Tenant | null> {
const value = await this.keyValueService.get(this.getKey(clientId))

if (value) {
return this.decode(value)
}

return null
}

async create(tenant: Tenant): Promise<Tenant> {
await this.keyValueService.set(this.getKey(tenant.clientId), this.encode(tenant))

return tenant
}

getKey(clientId: string): string {
return `${clientId}:tenant`
}

private encode(tenant: Tenant): string {
return JSON.stringify(tenant)
}

private decode(value: string): Tenant {
return tenantSchema.parse(JSON.parse(value))
}
}
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const KeyValueRepository = Symbol('KeyValueRepository')

export interface KeyValueRepository {
get(key: string): Promise<string | null>
set(key: string, value: string): Promise<boolean>
delete(key: string): Promise<boolean>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ConfigModule } from '@nestjs/config'
import { Test } from '@nestjs/testing'
import { load } from '../../../../../../../policy-engine.config'
import { InMemoryKeyValueRepository } from '../../../../persistence/repository/in-memory-key-value.repository'
import { KeyValueRepository } from '../../../repository/key-value.repository'
import { KeyValueService } from '../../key-value.service'

describe('foo', () => {
let service: KeyValueService
let inMemoryKeyValueRepository: InMemoryKeyValueRepository

beforeEach(async () => {
inMemoryKeyValueRepository = new InMemoryKeyValueRepository()

const module = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
load: [load],
isGlobal: true
})
],
providers: [
KeyValueService,
{
provide: KeyValueRepository,
useValue: inMemoryKeyValueRepository
}
]
}).compile()

service = module.get<KeyValueService>(KeyValueService)
})

describe('set', () => {
it('sets encrypted value in the key-value storage', async () => {
const key = 'test-key'
const value = 'not encrypted value'

await service.set(key, value)

expect(await service.get(key)).toEqual(value)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Inject, Injectable } from '@nestjs/common'
import { KeyValueRepository } from '../repository/key-value.repository'

/**
* The key-value service is the main interface to interact with any storage
* back-end. Since the storage backend lives outside the engine, it's considered
* untrusted so the engine will encrypt the data before it sends them to the
* storage.
*
* It's because of that the key-value service assumes data is always encrypted.
* If you need non-encrypted data, you can use the key-value repository.
*/
@Injectable()
export class KeyValueService {
constructor(@Inject(KeyValueRepository) private keyValueRepository: KeyValueRepository) {}

async get(key: string): Promise<string | null> {
// TODO (@wcalderipe, 01/03/2024): Add decryption step.
return this.keyValueRepository.get(key)
}

async set(key: string, value: string): Promise<boolean> {
// TODO (@wcalderipe, 01/03/2024): Add encryption step.
return this.keyValueRepository.set(key, value)
}

async delete(key: string): Promise<boolean> {
return this.keyValueRepository.delete(key)
}
}
16 changes: 16 additions & 0 deletions apps/policy-engine/src/shared/module/key-value/key-value.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Module } from '@nestjs/common'
import { KeyValueRepository } from './core/repository/key-value.repository'
import { KeyValueService } from './core/service/key-value.service'
import { InMemoryKeyValueRepository } from './persistence/repository/in-memory-key-value.repository'

@Module({
providers: [
KeyValueService,
{
provide: KeyValueRepository,
useClass: InMemoryKeyValueRepository
}
],
exports: [KeyValueService, KeyValueRepository]
})
export class KeyValueModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Injectable } from '@nestjs/common'
import { KeyValueRepository } from '../../core/repository/key-value.repository'

@Injectable()
export class InMemoryKeyValueRepository implements KeyValueRepository {
private store = new Map<string, string>()

async get(key: string): Promise<string | null> {
return this.store.get(key) || null
}

async set(key: string, value: string): Promise<boolean> {
this.store.set(key, value)

return true
}

async delete(key: string): Promise<boolean> {
this.store.delete(key)

return true
}
}
13 changes: 13 additions & 0 deletions apps/policy-engine/src/shared/schema/tenant.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { dataStoreConfigurationSchema } from 'packages/policy-engine-shared/src/lib/schema/data-store.schema'
import { z } from 'zod'

export const tenantSchema = z.object({
clientId: z.string(),
clientSecret: z.string(),
dataStore: z.object({
entity: dataStoreConfigurationSchema,
policy: dataStoreConfigurationSchema
}),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date()
})
9 changes: 3 additions & 6 deletions apps/policy-engine/src/shared/types/domain.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@ import {
TransactionRequest
} from '@narval/policy-engine-shared'
import { Intent } from '@narval/transaction-request-intent'
import { z } from 'zod'
import { tenantSchema } from '../schema/tenant.schema'

export enum UserRoles {
ROOT = 'root',
ADMIN = 'admin',
MEMBER = 'member',
MANAGER = 'manager'
}
export type Tenant = z.infer<typeof tenantSchema>

export type RegoInput = {
action: Action
Expand Down
1 change: 1 addition & 0 deletions packages/policy-engine-shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from './lib/dto'
export * from './lib/schema/address.schema'
export * from './lib/schema/hex.schema'
export * from './lib/type/action.type'
export * from './lib/type/data-store.type'
export * from './lib/type/domain.type'
export * from './lib/type/entity.type'
export * from './lib/util/caip.util'
Expand Down
37 changes: 37 additions & 0 deletions packages/policy-engine-shared/src/lib/schema/data-store.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { z } from 'zod'
import { entitiesSchema } from './entity.schema'

export const jsonWebKeySetSchema = z.object({
kty: z.string().describe('Key Type (e.g. RSA or EC'),
use: z.string(),
kid: z.string().describe('Arbitrary key ID'),
alg: z.string().describe('Algorithm'),
n: z.string().describe('Key modulus'),
e: z.string().describe('Key exponent')
})

export const dataStoreProtocolSchema = z.enum(['file'])

export const dataStoreConfigurationSchema = z.object({
dataUrl: z.string(),
signatureUrl: z.string(),
keys: z.array(jsonWebKeySetSchema)
})

export const entityDataSchema = z.object({
entity: z.object({
data: entitiesSchema
})
})

export const entitySignatureSchema = z.object({
entity: z.object({
signature: z.string()
})
})

export const entityJsonWebKeySetSchema = z.object({
entity: z.object({
keys: z.array(jsonWebKeySetSchema)
})
})
Loading

0 comments on commit 3ae44da

Please sign in to comment.