Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(backend): tenanted assets #3206

Merged
merged 23 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3827964
feat(backend): migration to backfill tenantId on assets
mkurapov Jan 8, 2025
0ad9b69
feat(backend): add tenantId to asset, use it in service
mkurapov Jan 8, 2025
f47c6a0
feat(backend): use tenantId in asset resolvers
mkurapov Jan 8, 2025
2edea03
test(backend): update tests to use asset tenantId where necessary
mkurapov Jan 8, 2025
54434cb
test(backend): truncate tenant table manually in tenant tests
mkurapov Jan 8, 2025
54ef8b7
test(backend): update failing accounting tests
mkurapov Jan 8, 2025
4255595
test(backend): update tenant service test
mkurapov Jan 8, 2025
5dce25b
test: fix accounting tests linting
mkurapov Jan 9, 2025
5a31294
test(backend): update accounting tests
mkurapov Jan 9, 2025
8151328
feat(backend): use tenantId when fetching asset
mkurapov Jan 9, 2025
5c507fd
test(backend): make tests work with separate middleware
mkurapov Jan 9, 2025
557e78a
test(backend): keep operator tenant when truncating tables
mkurapov Jan 9, 2025
62be42b
test(backend): skip tenant pagination tests for now
mkurapov Jan 10, 2025
2ae9bd0
test(backend): seed operator tenant in truncateTable
mkurapov Jan 10, 2025
1b57e15
test(backend): seed operator tenant after tenants service is done
mkurapov Jan 10, 2025
c89d84e
test(backend): use separate schema for tenant tests
mkurapov Jan 10, 2025
5c38bf0
test(backend): pass operator tenant id in pagination tests
mkurapov Jan 10, 2025
79422ed
feat(backend): make tenantId required in asset pagination
mkurapov Jan 10, 2025
f3eadec
test(backend): update tenant service tests
mkurapov Jan 10, 2025
34a05c2
chore(backend): update config file
mkurapov Jan 10, 2025
d3fa2b2
test: update truncateTables to take in dbSchema
mkurapov Jan 10, 2025
bf85eb6
feat(backend): make tenantId optional in asset pagination
mkurapov Jan 14, 2025
279a3d2
Merge branch '2893/multi-tenancy-v1' into 3033/mk/tenanted-assets
mkurapov Jan 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
53 changes: 43 additions & 10 deletions packages/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ import {
getTenantFromApiSignature,
TenantApiSignatureResult
} from './shared/utils'
import { TenantService } from './tenants/service'
export interface AppContextData {
logger: Logger
container: AppContainer
Expand Down Expand Up @@ -265,6 +266,7 @@ export interface AppServices {
paymentMethodHandlerService: Promise<PaymentMethodHandlerService>
ilpPaymentService: Promise<IlpPaymentService>
localPaymentService: Promise<LocalPaymentService>
tenantService: Promise<TenantService>
}

export type AppContainer = IocContract<AppServices>
Expand Down Expand Up @@ -393,21 +395,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
)
Comment on lines +439 to +445
Copy link
Contributor Author

@mkurapov mkurapov Jan 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically, had an issue where the replay attack prevention via redis (ie canApiSignature function) was resulting a 401 Unauthorized error for many resolver tests (for cases where we call the same resolver back to back)


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
Loading