Skip to content

Commit

Permalink
feat(backend): tenanted assets (#3206)
Browse files Browse the repository at this point in the history
* feat(backend): migration to backfill tenantId on assets

* feat(backend): add tenantId to asset, use it in service

* feat(backend): use tenantId in asset resolvers

* test(backend): update tests to use asset tenantId where necessary

* test(backend): truncate tenant table manually in tenant tests

* test(backend): update failing accounting tests

* test(backend): update tenant service test

* test: fix accounting tests linting

* test(backend): update accounting tests

* feat(backend): use tenantId when fetching asset

* test(backend): make tests work with separate middleware

* test(backend): keep operator tenant when truncating tables

* test(backend): skip tenant pagination tests for now

* test(backend): seed operator tenant in truncateTable

* test(backend): seed operator tenant after tenants service is done

* test(backend): use separate schema for tenant tests

* test(backend): pass operator tenant id in pagination tests

* feat(backend): make tenantId required in asset pagination

* test(backend): update tenant service tests

* chore(backend): update config file

* test: update truncateTables to take in dbSchema

* feat(backend): make tenantId optional in asset pagination
  • Loading branch information
mkurapov authored Jan 15, 2025
1 parent fd8283b commit 0b6fb1a
Show file tree
Hide file tree
Showing 21 changed files with 474 additions and 185 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema
.alterTable('assets', (table) => {
table.uuid('tenantId').references('tenants.id').index()
})
.then(() => {
return knex.raw(
`UPDATE "assets" SET "tenantId" = (SELECT id from "tenants" LIMIT 1)`
)
})
.then(() => {
return knex.schema.alterTable('assets', (table) => {
table.uuid('tenantId').notNullable().alter()
})
})
}

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.alterTable('assets', (table) => {
table.dropColumn('tenantId')
})
}
11 changes: 7 additions & 4 deletions packages/backend/src/accounting/psql/balance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,26 @@ import { createTestApp, TestContainer } from '../../tests/app'
import { Config } from '../../config/app'
import { initIocContainer } from '../../'
import { Asset } from '../../asset/model'
import { randomAsset } from '../../tests/asset'
import { createAsset } from '../../tests/asset'
import { truncateTables } from '../../tests/tableManager'
import { LedgerAccount } from './ledger-account/model'
import { createLedgerAccount } from '../../tests/ledgerAccount'
import { getAccountBalances } from './balance'
import { ServiceDependencies } from './service'
import { LedgerTransferState } from '../service'
import { createLedgerTransfer } from '../../tests/ledgerTransfer'
import { IocContract } from '@adonisjs/fold'
import { AppServices } from '../../app'

describe('Balances', (): void => {
let deps: IocContract<AppServices>
let serviceDeps: ServiceDependencies
let appContainer: TestContainer
let knex: Knex
let asset: Asset

beforeAll(async (): Promise<void> => {
const deps = initIocContainer({ ...Config, useTigerBeetle: false })
deps = initIocContainer({ ...Config, useTigerBeetle: false })
appContainer = await createTestApp(deps)
serviceDeps = {
logger: await deps.use('logger'),
Expand All @@ -31,7 +34,7 @@ describe('Balances', (): void => {
})

beforeEach(async (): Promise<void> => {
asset = await Asset.query().insertAndFetch(randomAsset())
asset = await createAsset(deps)
})

afterEach(async (): Promise<void> => {
Expand All @@ -48,7 +51,7 @@ describe('Balances', (): void => {
let peerAccount: LedgerAccount

beforeEach(async (): Promise<void> => {
asset = await Asset.query(knex).insertAndFetch(randomAsset())
asset = await createAsset(deps)
;[account, peerAccount] = await Promise.all([
createLedgerAccount({ ledger: asset.ledger }, knex),
createLedgerAccount({ ledger: asset.ledger }, knex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ describe('Ledger Account', (): void => {
})

beforeEach(async (): Promise<void> => {
asset = await Asset.query().insertAndFetch(randomAsset())
asset = await Asset.query().insertAndFetch({
...randomAsset(),
tenantId: Config.operatorTenantId
})
})

afterEach(async (): Promise<void> => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ describe('Ledger Transfer', (): void => {
let settlementAccount: LedgerAccount

beforeEach(async (): Promise<void> => {
asset = await Asset.query(knex).insertAndFetch(randomAsset())
asset = await Asset.query(knex).insertAndFetch({
...randomAsset(),
tenantId: Config.operatorTenantId
})
;[account, peerAccount, settlementAccount] = await Promise.all([
createLedgerAccount({ ledger: asset.ledger }, knex),
createLedgerAccount({ ledger: asset.ledger }, knex),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ import { LedgerAccount, LedgerAccountType } from '../ledger-account/model'
import { createLedgerAccount } from '../../../tests/ledgerAccount'
import { LedgerTransferState } from '../../service'
import { createLedgerTransfer } from '../../../tests/ledgerTransfer'
import { IocContract } from '@adonisjs/fold'
import { AppServices } from '../../../app'

describe('Ledger Transfer Model', (): void => {
let deps: IocContract<AppServices>
let appContainer: TestContainer
let knex: Knex
let asset: Asset

beforeAll(async (): Promise<void> => {
const deps = initIocContainer({ ...Config, useTigerBeetle: false })
deps = initIocContainer({ ...Config, useTigerBeetle: false })
appContainer = await createTestApp(deps)
knex = appContainer.knex
})
Expand All @@ -25,7 +28,10 @@ describe('Ledger Transfer Model', (): void => {
let debitAccount: LedgerAccount

beforeEach(async (): Promise<void> => {
asset = await Asset.query(knex).insertAndFetch(randomAsset())
asset = await Asset.query(knex).insertAndFetch({
...randomAsset(),
tenantId: Config.operatorTenantId
})
;[creditAccount, debitAccount] = await Promise.all([
createLedgerAccount({ ledger: asset.ledger }, knex),
createLedgerAccount(
Expand Down
15 changes: 12 additions & 3 deletions packages/backend/src/accounting/psql/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ describe('Psql Accounting Service', (): void => {
})

beforeEach(async (): Promise<void> => {
asset = await Asset.query().insertAndFetch(randomAsset())
asset = await Asset.query().insertAndFetch({
...randomAsset(),
tenantId: Config.operatorTenantId
})
})

afterEach(async (): Promise<void> => {
Expand Down Expand Up @@ -892,7 +895,10 @@ describe('Psql Accounting Service', (): void => {
const timeout = 10 // 10 seconds

beforeEach(async (): Promise<void> => {
const sourceAsset = await assetService.create(randomAsset())
const sourceAsset = await assetService.create({
...randomAsset(),
tenantId: Config.operatorTenantId
})
assert.ok(!isAssetError(sourceAsset))

sourceAccount = await accountFactory.build({
Expand All @@ -902,7 +908,10 @@ describe('Psql Accounting Service', (): void => {

const destinationAsset = sameAsset
? sourceAsset
: await assetService.create(randomAsset())
: await assetService.create({
...randomAsset(),
tenantId: Config.operatorTenantId
})

assert.ok(!isAssetError(destinationAsset))

Expand Down
54 changes: 44 additions & 10 deletions packages/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ import {
getTenantFromApiSignature,
TenantApiSignatureResult
} from './shared/utils'
import { TenantService } from './tenants/service'
import { AuthServiceClient } from './auth-service-client/client'

export interface AppContextData {
logger: Logger
container: AppContainer
Expand Down Expand Up @@ -266,6 +268,7 @@ export interface AppServices {
paymentMethodHandlerService: Promise<PaymentMethodHandlerService>
ilpPaymentService: Promise<IlpPaymentService>
localPaymentService: Promise<LocalPaymentService>
tenantService: Promise<TenantService>
authServiceClient: AuthServiceClient
}

Expand Down Expand Up @@ -395,21 +398,52 @@ export class App {
)

let tenantApiSignatureResult: TenantApiSignatureResult
if (this.config.env !== 'test') {
koa.use(async (ctx, next: Koa.Next): Promise<void> => {
const result = await getTenantFromApiSignature(ctx, this.config)
if (!result) {
ctx.throw(401, 'Unauthorized')
} else {
const tenantSignatureMiddleware = async (
ctx: AppContext,
next: Koa.Next
): Promise<void> => {
const result = await getTenantFromApiSignature(ctx, this.config)
if (!result) {
ctx.throw(401, 'Unauthorized')
} else {
tenantApiSignatureResult = {
tenant: result.tenant,
isOperator: result.isOperator ? true : false
}
}
return next()
}

const testTenantSignatureMiddleware = async (
ctx: AppContext,
next: Koa.Next
): Promise<void> => {
if (ctx.headers['tenant-id']) {
const tenantService = await ctx.container.use('tenantService')
const tenant = await tenantService.get(
ctx.headers['tenant-id'] as string
)

if (tenant) {
tenantApiSignatureResult = {
tenant: result.tenant,
isOperator: result.isOperator ? true : false
tenant,
isOperator: tenant.apiSecret === this.config.adminApiSecret
}
} else {
ctx.throw(401, 'Unauthorized')
}
return next()
})
}
return next()
}

// For tests, we still need to get the tenant in the middleware, but
// we don't need to verify the signature nor prevent replay attacks
koa.use(
this.config.env !== 'test'
? tenantSignatureMiddleware
: testTenantSignatureMiddleware
)

koa.use(
koaMiddleware(this.apolloServer, {
context: async (): Promise<TenantedApolloContext> => {
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/asset/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ describe('Models', (): void => {
beforeEach(async (): Promise<void> => {
const options = {
...randomAsset(),
tenantId: Config.operatorTenantId,
liquidityThreshold: BigInt(100)
}
const assetOrError = await assetService.create(options)
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/asset/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class Asset extends BaseModel implements LiquidityAccount {

// TigerBeetle account 2 byte ledger field representing account's asset
public readonly ledger!: number
public readonly tenantId!: string

public readonly withdrawalThreshold!: bigint | null

Expand Down
Loading

0 comments on commit 0b6fb1a

Please sign in to comment.