Skip to content

Commit

Permalink
Sync tenant stores
Browse files Browse the repository at this point in the history
  • Loading branch information
wcalderipe committed Mar 7, 2024
1 parent e095487 commit 72c1421
Show file tree
Hide file tree
Showing 10 changed files with 380 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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 { InMemoryKeyValueRepository } from '../../../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository'
import { Tenant } from '../../../../../shared/type/domain.type'
import { TenantRepository } from '../../../../persistence/repository/tenant.repository'
import { DataStoreService } from '../../data-store.service'
import { TenantService } from '../../tenant.service'

describe(TenantService.name, () => {
let tenantService: TenantService
let tenantRepository: TenantRepository
let dataStoreServiceMock: MockProxy<DataStoreService>

const clientId = 'test-client-id'

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,
clientSecret: 'test-client-secret',
dataStore: {
entity: dataStoreConfiguration,
policy: dataStoreConfiguration
},
createdAt: new Date(),
updatedAt: new Date()
}

const stores = {
entity: {
data: FIXTURE.ENTITIES,
signature: 'test-signature'
},
policy: {
data: FIXTURE.POLICIES,
signature: 'test-signature'
}
}

beforeEach(async () => {
dataStoreServiceMock = mock<DataStoreService>()
dataStoreServiceMock.fetch.mockResolvedValue(stores)

const module = await Test.createTestingModule({
providers: [
TenantService,
TenantRepository,
{
provide: DataStoreService,
useValue: dataStoreServiceMock
},
{
provide: KeyValueService,
useClass: InMemoryKeyValueRepository
}
]
}).compile()

tenantService = module.get<TenantService>(TenantService)
tenantRepository = module.get<TenantRepository>(TenantRepository)
})

describe('syncDataStore', () => {
beforeEach(async () => {
await tenantRepository.save(tenant)
})

it('saves entity and policy stores', async () => {
expect(await tenantRepository.findEntityStore(clientId)).toEqual(null)
expect(await tenantRepository.findPolicyStore(clientId)).toEqual(null)

await tenantService.syncDataStore(clientId)

expect(await tenantRepository.findEntityStore(clientId)).toEqual(stores.entity)
expect(await tenantRepository.findPolicyStore(clientId)).toEqual(stores.policy)
})

it('fetches the data stores once', async () => {
await tenantService.syncDataStore(clientId)

expect(dataStoreServiceMock.fetch).toHaveBeenCalledTimes(1)
expect(dataStoreServiceMock.fetch).toHaveBeenCalledWith(tenant.dataStore)
})
})
})
14 changes: 4 additions & 10 deletions apps/policy-engine/src/app/core/service/data-store.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
DataStoreConfiguration,
Entities,
Policy,
EntityStore,
PolicyStore,
entityDataSchema,
entitySignatureSchema,
policyDataSchema,
Expand All @@ -17,14 +17,8 @@ export class DataStoreService {
constructor(private dataStoreRepositoryFactory: DataStoreRepositoryFactory) {}

async fetch(store: { entity: DataStoreConfiguration; policy: DataStoreConfiguration }): Promise<{
entity: {
data: Entities
signature: string
}
policy: {
data: Policy[]
signature: string
}
entity: EntityStore
policy: PolicyStore
}> {
const [entityData, entitySignature, policyData, policySignature] = await Promise.all([
this.fetchByUrl(store.entity.dataUrl, entityDataSchema),
Expand Down
27 changes: 24 additions & 3 deletions apps/policy-engine/src/app/core/service/tenant.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@ import { HttpStatus, Injectable } from '@nestjs/common'
import { ApplicationException } from '../../../shared/exception/application.exception'
import { Tenant } from '../../../shared/type/domain.type'
import { TenantRepository } from '../../persistence/repository/tenant.repository'
import { DataStoreService } from './data-store.service'

@Injectable()
export class TenantService {
constructor(private tenantRepository: TenantRepository) {}
constructor(
private tenantRepository: TenantRepository,
private dataStoreService: DataStoreService
) {}

async findByClientId(clientId: string): Promise<Tenant | null> {
return this.tenantRepository.findByClientId(clientId)
}

async create(tenant: Tenant): Promise<Tenant> {
async save(tenant: Tenant): Promise<Tenant> {
if (await this.tenantRepository.findByClientId(tenant.clientId)) {
throw new ApplicationException({
message: 'Tenant already exist',
Expand All @@ -20,6 +24,23 @@ export class TenantService {
})
}

return this.tenantRepository.create(tenant)
return this.tenantRepository.save(tenant)
}

async syncDataStore(clientId: string): Promise<boolean> {
const tenant = await this.findByClientId(clientId)

if (tenant) {
const store = await this.dataStoreService.fetch(tenant.dataStore)

await Promise.all([
this.tenantRepository.saveEntityStore(clientId, store.entity),
this.tenantRepository.savePolicyStore(clientId, store.policy)
])

return true
}

return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class TenantController {
async create(@Body() body: CreateTenantDto) {
const now = new Date()

const tenant = await this.tenantService.create({
const tenant = await this.tenantService.save({
clientId: body.clientId || uuid(),
clientSecret: randomBytes(42).toString('hex'),
dataStore: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { DataStoreConfiguration } from '@narval/policy-engine-shared'
import {
Action,
Criterion,
DataStoreConfiguration,
EntityStore,
FIXTURE,
PolicyStore,
Then
} from '@narval/policy-engine-shared'
import { Test } from '@nestjs/testing'
import { mock } from 'jest-mock-extended'
import { EncryptionService } from '../../../../../encryption/core/encryption.service'
Expand All @@ -14,6 +22,8 @@ describe(TenantRepository.name, () => {
let repository: TenantRepository
let inMemoryKeyValueRepository: InMemoryKeyValueRepository

const clientId = 'test-client-id'

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

Expand Down Expand Up @@ -50,7 +60,7 @@ describe(TenantRepository.name, () => {
repository = module.get<TenantRepository>(TenantRepository)
})

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

const dataStoreConfiguration: DataStoreConfiguration = {
Expand All @@ -60,7 +70,7 @@ describe(TenantRepository.name, () => {
}

const tenant: Tenant = {
clientId: 'test-client-id',
clientId,
clientSecret: 'test-client-secret',
dataStore: {
entity: dataStoreConfiguration,
Expand All @@ -70,8 +80,8 @@ describe(TenantRepository.name, () => {
updatedAt: now
}

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

const value = await inMemoryKeyValueRepository.get(repository.getKey(tenant.clientId))
const actualTenant = await repository.findByClientId(tenant.clientId)
Expand All @@ -81,9 +91,64 @@ describe(TenantRepository.name, () => {
})

it('indexes the new tenant', async () => {
await repository.create(tenant)
await repository.save(tenant)

expect(await repository.getTenantIndex()).toEqual([tenant.clientId])
})
})

describe('saveEntityStore', () => {
const store: EntityStore = {
data: FIXTURE.ENTITIES,
signature: 'test-fake-signature'
}

it('saves the entity store', async () => {
await repository.saveEntityStore(clientId, store)

expect(await repository.findEntityStore(clientId)).toEqual(store)
})

it('overwrites existing entity store', async () => {
const storeTwo = { ...store, signature: 'another-test-signature' }

await repository.saveEntityStore(clientId, store)
await repository.saveEntityStore(clientId, storeTwo)

expect(await repository.findEntityStore(clientId)).toEqual(storeTwo)
})
})

describe('savePolicyStore', () => {
const store: PolicyStore = {
data: [
{
then: Then.PERMIT,
name: 'test-policy',
when: [
{
criterion: Criterion.CHECK_ACTION,
args: [Action.SIGN_TRANSACTION]
}
]
}
],
signature: 'test-fake-signature'
}

it('saves the policy store', async () => {
await repository.savePolicyStore(clientId, store)

expect(await repository.findPolicyStore(clientId)).toEqual(store)
})

it('overwrites existing policy store', async () => {
const storeTwo = { ...store, signature: 'another-test-signature' }

await repository.savePolicyStore(clientId, store)
await repository.savePolicyStore(clientId, storeTwo)

expect(await repository.findPolicyStore(clientId)).toEqual(storeTwo)
})
})
})
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { EntityStore, PolicyStore, entityStoreSchema, policyStoreSchema } from '@narval/policy-engine-shared'
import { Injectable } from '@nestjs/common'
import { KeyValueService } from '../../../shared/module/key-value/core/service/key-value.service'
import { tenantIndexSchema, tenantSchema } from '../../../shared/schema/tenant.schema'
Expand All @@ -17,7 +18,7 @@ export class TenantRepository {
return null
}

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

Expand All @@ -42,12 +43,48 @@ export class TenantRepository {
return []
}

saveEntityStore(clientId: string, store: EntityStore): Promise<boolean> {
return this.keyValueService.set(this.getEntityStoreKey(clientId), this.encodeEntityStore(store))
}

async findEntityStore(clientId: string): Promise<EntityStore | null> {
const value = await this.keyValueService.get(this.getEntityStoreKey(clientId))

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

return null
}

savePolicyStore(clientId: string, store: PolicyStore): Promise<boolean> {
return this.keyValueService.set(this.getPolicyStoreKey(clientId), this.encodePolicyStore(store))
}

async findPolicyStore(clientId: string): Promise<PolicyStore | null> {
const value = await this.keyValueService.get(this.getPolicyStoreKey(clientId))

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

return null
}

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

getIndexKey(): string {
return `tenants`
return 'tenant:index'
}

getEntityStoreKey(clientId: string): string {
return `tenant:${clientId}:entity-store`
}

getPolicyStoreKey(clientId: string): string {
return `tenant:${clientId}:policy-store`
}

private encode(tenant: Tenant): string {
Expand All @@ -65,4 +102,20 @@ export class TenantRepository {
private decodeIndex(value: string): string[] {
return tenantIndexSchema.parse(JSON.parse(value))
}

private encodeEntityStore(value: EntityStore): string {
return KeyValueService.encode(entityStoreSchema.parse(value))
}

private decodeEntityStore(value: string): EntityStore {
return entityStoreSchema.parse(JSON.parse(value))
}

private encodePolicyStore(value: PolicyStore): string {
return KeyValueService.encode(policyStoreSchema.parse(value))
}

private decodePolicyStore(value: string): PolicyStore {
return policyStoreSchema.parse(JSON.parse(value))
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'
import { EncryptionService } from '../../../../../encryption/core/encryption.service'
import { KeyValueRepository } from '../repository/key-value.repository'

/**
Expand All @@ -13,10 +12,7 @@ import { KeyValueRepository } from '../repository/key-value.repository'
*/
@Injectable()
export class KeyValueService {
constructor(
@Inject(KeyValueRepository) private keyValueRepository: KeyValueRepository,
private encryptionService: EncryptionService
) {}
constructor(@Inject(KeyValueRepository) private keyValueRepository: KeyValueRepository) {}

async get(key: string): Promise<string | null> {
// const encryptedValue = await this.keyValueRepository.get(key)
Expand Down
Loading

0 comments on commit 72c1421

Please sign in to comment.