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): tenant support for wallet address (#3114) #3152

Draft
wants to merge 33 commits into
base: 2893/multi-tenancy-v1
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2f74c4e
feat(backend): tenant service
njlie Nov 27, 2024
61d45f7
fix: integration tests
njlie Nov 27, 2024
d57bcc6
feat: use soft delete
njlie Nov 27, 2024
57a663d
refactor: compare whole object in test
njlie Nov 27, 2024
fb3d702
fix: better gql errors in tests
njlie Dec 2, 2024
e5cc2b5
feat: add idp columns to tenant model
njlie Dec 3, 2024
c824c56
feat: pagination tests, push deletedAt to auth api call
njlie Dec 3, 2024
9211ca3
feat: add cache
njlie Dec 3, 2024
e2bbc79
fix: update localenv environment variables
njlie Dec 3, 2024
fda0653
Merge branch 'refs/heads/nl/3123/backend-tenant-service' into 3114/te…
koekiebox Dec 4, 2024
151c3aa
Merge branch 'refs/heads/main' into 3114/tenanted-wallet-addresses
koekiebox Dec 4, 2024
d6257fe
feat(3114): add tenant to wallet address.
koekiebox Dec 5, 2024
86d9a39
feat(3114): test fixes.
koekiebox Dec 5, 2024
3625938
feat: make some tenants fields optional, small refactors
njlie Dec 6, 2024
6f16eaa
Merge branch 'nl/3123/backend-tenant-service' into 3114/tenanted-wall…
koekiebox Dec 9, 2024
490f748
feat(auth): tenants table v1
njlie Nov 25, 2024
10bc6ab
feat(backend): tenant service
njlie Nov 27, 2024
d5cc314
feat: use soft delete
njlie Nov 27, 2024
fc01ef7
feat: add idp columns to tenant model
njlie Dec 3, 2024
87213de
feat: pagination tests, push deletedAt to auth api call
njlie Dec 3, 2024
0a37b3c
feat: add cache
njlie Dec 3, 2024
3805b10
feat(backend): tenant signature validation for admin api
njlie Dec 6, 2024
92fc1ac
fix: rebase errors
njlie Dec 9, 2024
0ad7ccc
Merge branch '2893/multi-tenancy-v1' into 3114/tenanted-wallet-addresses
koekiebox Dec 10, 2024
645e181
feat(3114): update seed.ts
koekiebox Dec 10, 2024
cdc2bda
fix: remove admin api secret check from app
njlie Dec 10, 2024
c44cd03
fix: always expect tenant id in request
njlie Dec 10, 2024
4e17260
chore: remove some logs
njlie Dec 10, 2024
a9f0fff
Merge branch '2893/multi-tenancy-v1' into 3114/tenanted-wallet-addresses
koekiebox Dec 11, 2024
7d562b5
feat(3114): update for auth and resource server.
koekiebox Dec 11, 2024
b5a1667
feat(3114): fix asset service.test.ts
koekiebox Dec 11, 2024
f29862a
feat(3114): fix tests.
koekiebox Dec 11, 2024
868221c
Merge branch 'nl/2928/multi-tenancy-signatures' into 3114/tenanted-wa…
koekiebox Dec 12, 2024
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
Expand Up @@ -26,5 +26,7 @@ export const CONFIG: Config = {
testnetAutoPeerUrl: process.env.TESTNET_AUTOPEER_URL ?? '',
authServerDomain: process.env.AUTH_SERVER_DOMAIN || 'http://localhost:3006',
graphqlUrl: process.env.GRAPHQL_URL,
idpSecret: process.env.IDP_SECRET
idpSecret: process.env.IDP_SECRET,
operatorTenantId:
process.env.OPERATOR_TENANT_ID || '438fa74a-fa7d-4317-9ced-dde32ece1787'
}
2 changes: 2 additions & 0 deletions localenv/mock-account-servicing-entity/generated/graphql.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return Promise.all([
knex.schema.alterTable('walletAddresses', function (table) {
table.uuid('tenantId').index().notNullable()
//table.foreign(['tenantId']).references('tenants.id')
})
])
}

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return Promise.all([
knex.schema.alterTable('walletAddresses', function (table) {
table.dropIndex('tenantId')
table.dropColumn('tenantId')
})
])
}
26 changes: 20 additions & 6 deletions packages/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ import { IlpPaymentService } from './payment-method/ilp/service'
import { TelemetryService } from './telemetry/service'
import { ApolloArmor } from '@escape.tech/graphql-armor'
import { openPaymentsServerErrorMiddleware } from './open_payments/route-errors'
import { verifyApiSignature } from './shared/utils'
import { WalletAddress } from './open_payments/wallet_address/model'
import {
getWalletAddressUrlFromIncomingPayment,
Expand All @@ -101,6 +100,8 @@ import { LoggingPlugin } from './graphql/plugin'
import { LocalPaymentService } from './payment-method/local/service'
import { GrantService } from './open_payments/grant/service'
import { AuthServerService } from './open_payments/authServer/service'
import { Tenant } from './tenants/model'
import { verifyTenantOrOperatorApiSignature } from './shared/utils'
export interface AppContextData {
logger: Logger
container: AppContainer
Expand Down Expand Up @@ -144,6 +145,19 @@ export type HttpSigContext = AppContext & {
client: string
}

type TenantedHttpSigHeaders = HttpSigHeaders & Record<'tenantId', string>

type TenantedHttpSigRequest = Omit<HttpSigContext['request'], 'headers'> & {
headers: TenantedHttpSigHeaders
}

export type TenantedHttpSigContext = HttpSigContext & {
headers: TenantedHttpSigHeaders
request: TenantedHttpSigRequest
tenant?: Tenant
isOperator: boolean
}

export type HttpSigWithAuthenticatedStatusContext = HttpSigContext &
AuthenticatedStatusContext

Expand Down Expand Up @@ -383,14 +397,14 @@ export class App {
}
)

if (this.config.adminApiSecret) {
koa.use(async (ctx, next: Koa.Next): Promise<void> => {
if (!verifyApiSignature(ctx, this.config)) {
koa.use(
async (ctx: TenantedHttpSigContext, next: Koa.Next): Promise<void> => {
if (!verifyTenantOrOperatorApiSignature(ctx, this.config)) {
ctx.throw(401, 'Unauthorized')
}
return next()
})
}
}
)

koa.use(
koaMiddleware(this.apolloServer, {
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/asset/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ describe('Asset Service', (): void => {
// make sure there is at least 1 wallet address using asset
const walletAddress = walletAddressService.create({
url: 'https://alice.me/.well-known/pay',
tenantId: Config.operatorTenantId,
assetId: newAssetId
})
assert.ok(!isWalletAddressError(walletAddress))
Expand Down
16 changes: 16 additions & 0 deletions packages/backend/src/graphql/generated/graphql.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/backend/src/graphql/generated/graphql.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/backend/src/graphql/resolvers/wallet_address.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { createTestApp, TestContainer } from '../../tests/app'
import { IocContract } from '@adonisjs/fold'
import { AppServices } from '../../app'
import { Asset } from '../../asset/model'
import { Tenant } from '../../tenants/model'
import { initIocContainer } from '../..'
import { Config } from '../../config/app'
import { truncateTables } from '../../tests/tableManager'
Expand Down Expand Up @@ -35,6 +36,8 @@ import {
import { getPageTests } from './page.test'
import { WalletAddressAdditionalProperty } from '../../open_payments/wallet_address/additional_property/model'
import { GraphQLErrorCode } from '../errors'
//TODO import { TenantService } from '../../tenants/service'
import { createTenant } from '../../tests/tenant'

describe('Wallet Address Resolvers', (): void => {
let deps: IocContract<AppServices>
Expand Down Expand Up @@ -63,11 +66,14 @@ describe('Wallet Address Resolvers', (): void => {

describe('Create Wallet Address', (): void => {
let asset: Asset
let tenant: Tenant
let input: CreateWalletAddressInput

beforeEach(async (): Promise<void> => {
asset = await createAsset(deps)
tenant = await createTenant(deps)
input = {
tenantId: tenant.id,
assetId: asset.id,
url: 'https://alice.me/.well-known/pay'
}
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/graphql/resolvers/wallet_address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export const createWalletAddress: MutationResolvers<ApolloContext>['createWallet

const options: CreateOptions = {
assetId: args.input.assetId,
tenantId: args.input.tenantId,
additionalProperties: addProps,
publicName: args.input.publicName,
url: args.input.url
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,8 @@ type CreateReceiverResponse {
}

input CreateWalletAddressInput {
"Unique identifier of the tenant associated with the wallet address. This cannot be changed."
tenantId: String!
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This would need to be removed at a later stage (once we obtain the tenant from the signature):
#2928

"Unique identifier of the asset associated with the wallet address. This cannot be changed."
assetId: String!
"Wallet address URL. This cannot be changed."
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ export function initIocContainer(
logger: logger,
accountingService: await deps.use('accountingService'),
webhookService: await deps.use('webhookService'),
tenantService: await deps.use('tenantService'),
assetService: await deps.use('assetService'),
walletAddressCache: await deps.use('walletAddressCache')
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ describe('Incoming Payment Service', (): void => {

beforeEach(async (): Promise<void> => {
asset = await createAsset(deps)
const address = await createWalletAddress(deps, { assetId: asset.id })
const address = await createWalletAddress(deps, {
tenantId: config.operatorTenantId,
assetId: asset.id
})
walletAddressId = address.id
client = address.url
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,12 +272,14 @@ describe('OutgoingPaymentService', (): void => {
const { id: sendAssetId } = await createAsset(deps, asset)
assetId = sendAssetId
const walletAddress = await createWalletAddress(deps, {
tenantId: config.operatorTenantId,
assetId: sendAssetId
})
walletAddressId = walletAddress.id
client = walletAddress.url
const { id: destinationAssetId } = await createAsset(deps, destinationAsset)
receiverWalletAddress = await createWalletAddress(deps, {
tenantId: config.operatorTenantId,
assetId: destinationAssetId,
mockServerPort: appContainer.openPaymentsPort
})
Expand Down Expand Up @@ -408,8 +410,12 @@ describe('OutgoingPaymentService', (): void => {
let outgoingPayment: OutgoingPayment
let otherOutgoingPayment: OutgoingPayment
beforeEach(async (): Promise<void> => {
otherSenderWalletAddress = await createWalletAddress(deps, { assetId })
otherSenderWalletAddress = await createWalletAddress(deps, {
tenantId: config.operatorTenantId,
assetId
})
otherReceiverWalletAddress = await createWalletAddress(deps, {
tenantId: config.operatorTenantId,
assetId
})
const incomingPayment = await createIncomingPayment(deps, {
Expand Down Expand Up @@ -974,7 +980,9 @@ describe('OutgoingPaymentService', (): void => {
validDestination: false,
method: 'ilp'
})
const walletAddress = await createWalletAddress(deps)
const walletAddress = await createWalletAddress(deps, {
tenantId: config.operatorTenantId
})
const walletAddressUpdated = await WalletAddress.query(
knex
).patchAndFetchById(walletAddress.id, { deactivatedAt: new Date() })
Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/open_payments/quote/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,12 @@ describe('QuoteService', (): void => {
scale: debitAmount.assetScale
})
sendingWalletAddress = await createWalletAddress(deps, {
tenantId: config.operatorTenantId,
assetId: sendAssetId
})
const { id: destinationAssetId } = await createAsset(deps, destinationAsset)
receivingWalletAddress = await createWalletAddress(deps, {
tenantId: config.operatorTenantId,
assetId: destinationAssetId,
mockServerPort: appContainer.openPaymentsPort
})
Expand Down Expand Up @@ -525,9 +527,11 @@ describe('QuoteService', (): void => {
scale: 2
})
sendingWalletAddress = await createWalletAddress(deps, {
tenantId: config.operatorTenantId,
assetId: asset.id
})
receivingWalletAddress = await createWalletAddress(deps, {
tenantId: config.operatorTenantId,
assetId: asset.id
})
})
Expand Down Expand Up @@ -633,9 +637,11 @@ describe('QuoteService', (): void => {
scale: 2
})
sendingWalletAddress = await createWalletAddress(deps, {
tenantId: config.operatorTenantId,
assetId: sendAsset.id
})
receivingWalletAddress = await createWalletAddress(deps, {
tenantId: config.operatorTenantId,
assetId: receiveAsset.id
})
})
Expand Down
12 changes: 12 additions & 0 deletions packages/backend/src/open_payments/wallet_address/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { WebhookEvent } from '../../webhook/model'
import { WalletAddressKey } from '../../open_payments/wallet_address/key/model'
import { AmountJSON } from '../amount'
import { WalletAddressAdditionalProperty } from './additional_property/model'
import { Tenant } from '../../tenants/model'

export class WalletAddress
extends BaseModel
Expand All @@ -18,6 +19,14 @@ export class WalletAddress
}

static relationMappings = () => ({
tenant: {
relation: Model.HasOneRelation,
modelClass: Tenant,
join: {
from: 'walletAddresses.tenantId',
to: 'tenants.id'
}
},
asset: {
relation: Model.HasOneRelation,
modelClass: Asset,
Expand Down Expand Up @@ -53,6 +62,9 @@ export class WalletAddress
public readonly assetId!: string
public asset!: Asset

public readonly tenantId!: string
public tenant!: Tenant
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we actually need the tenant!: Tenant property on the model? I'm not sure we'd end up using it

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Originally I thought we would need it, but since we have authServer and resourceServer, we are good. Will update.


// The cumulative received amount tracked by
// `wallet_address.web_monetization` webhook events.
// The value should be equivalent to the following query:
Expand Down
12 changes: 8 additions & 4 deletions packages/backend/src/open_payments/wallet_address/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ describe('Wallet Address Routes', (): void => {
addPropNotVisibleInOpenPayments.fieldValue = 'it-is-not'
addPropNotVisibleInOpenPayments.visibleInOpenPayments = false
const walletAddress = await createWalletAddress(deps, {
tenantId: config.operatorTenantId,
publicName: faker.person.firstName(),
additionalProperties: [addProp, addPropNotVisibleInOpenPayments]
})
Expand All @@ -118,8 +119,9 @@ describe('Wallet Address Routes', (): void => {
publicName: walletAddress.publicName,
assetCode: walletAddress.asset.code,
assetScale: walletAddress.asset.scale,
authServer: config.authServerGrantUrl,
resourceServer: config.openPaymentsUrl,
// Ensure the tenant id is returned for auth and resource server:
authServer: `${config.authServerGrantUrl}/${config.operatorTenantId}`,
resourceServer: `${config.openPaymentsUrl}/${config.operatorTenantId}`,
additionalProperties: {
[addProp.fieldKey]: addProp.fieldValue
}
Expand All @@ -145,6 +147,7 @@ describe('Wallet Address Routes', (): void => {

test('returns wallet address', async (): Promise<void> => {
const walletAddress = await createWalletAddress(deps, {
tenantId: config.operatorTenantId,
publicName: faker.person.firstName()
})

Expand All @@ -160,8 +163,9 @@ describe('Wallet Address Routes', (): void => {
publicName: walletAddress.publicName,
assetCode: walletAddress.asset.code,
assetScale: walletAddress.asset.scale,
authServer: config.authServerGrantUrl,
resourceServer: config.openPaymentsUrl
// Ensure the tenant id is returned for auth and resource server:
authServer: `${config.authServerGrantUrl}/${config.operatorTenantId}`,
resourceServer: `${config.openPaymentsUrl}/${config.operatorTenantId}`
})
})
})
Expand Down
5 changes: 3 additions & 2 deletions packages/backend/src/open_payments/wallet_address/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '../../shared/pagination'
import { OpenPaymentsServerRouteError } from '../route-errors'
import { IAppConfig } from '../../config/app'
import { ensureTrailingSlash } from '../../shared/utils'

interface ServiceDependencies {
config: IAppConfig
Expand Down Expand Up @@ -60,8 +61,8 @@ export async function getWalletAddress(
)

ctx.body = walletAddress.toOpenPaymentsType({
authServer: deps.config.authServerGrantUrl,
resourceServer: deps.config.openPaymentsUrl
authServer: `${ensureTrailingSlash(deps.config.authServerGrantUrl)}${walletAddress.tenantId}`,
resourceServer: `${ensureTrailingSlash(deps.config.openPaymentsUrl)}${walletAddress.tenantId}`
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { CreateOptions, FORBIDDEN_PATHS, WalletAddressService } from './service'
import { AccountingService } from '../../accounting/service'
import { createTestApp, TestContainer } from '../../tests/app'
import { createAsset } from '../../tests/asset'
import { createTenant } from '../../tests/tenant'
import { createWalletAddress } from '../../tests/walletAddress'
import { truncateTables } from '../../tests/tableManager'
import { Config, IAppConfig } from '../../config/app'
Expand Down Expand Up @@ -61,9 +62,11 @@ describe('Open Payments Wallet Address Service', (): void => {

beforeEach(async (): Promise<void> => {
const { id: assetId } = await createAsset(deps)
const { id: tenantId } = await createTenant(deps)
options = {
url: 'https://alice.me/.well-known/pay',
assetId
assetId,
tenantId
}
})

Expand Down
Loading
Loading