From 2f74c4ee152bde06ef41e54de23ef9f6248a45cf Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Wed, 27 Nov 2024 10:20:34 -0800 Subject: [PATCH 01/66] feat(backend): tenant service --- packages/backend/jest.config.js | 2 + .../20241125224212_create_tenants_table.js | 3 + packages/backend/src/config/app.ts | 3 + packages/backend/src/index.ts | 93 +++++ packages/backend/src/tenants/model.ts | 8 + packages/backend/src/tenants/service.test.ts | 366 ++++++++++++++++++ packages/backend/src/tenants/service.ts | 202 ++++++++++ 7 files changed, 677 insertions(+) create mode 100644 packages/backend/src/tenants/service.test.ts create mode 100644 packages/backend/src/tenants/service.ts diff --git a/packages/backend/jest.config.js b/packages/backend/jest.config.js index 492a6e5e30..c0245a4d3b 100644 --- a/packages/backend/jest.config.js +++ b/packages/backend/jest.config.js @@ -12,6 +12,8 @@ process.env.ILP_CONNECTOR_URL = 'http://127.0.0.1:3002' process.env.ILP_ADDRESS = 'test.rafiki' process.env.AUTH_SERVER_GRANT_URL = 'http://127.0.0.1:3006' process.env.AUTH_SERVER_INTROSPECTION_URL = 'http://127.0.0.1:3007/' +process.env.AUTH_ADMIN_API_URL = 'http://127.0.0.1:3003/graphql' +process.env.AUTH_ADMIN_API_SECRET = 'test-secret' process.env.WEBHOOK_URL = 'http://127.0.0.1:4001/webhook' process.env.STREAM_SECRET = '2/PxuRFV9PAp0yJlnAifJ+1OxujjjI16lN+DBnLNRLA=' process.env.USE_TIGERBEETLE = false diff --git a/packages/backend/migrations/20241125224212_create_tenants_table.js b/packages/backend/migrations/20241125224212_create_tenants_table.js index 2b00eb981d..df567d29af 100644 --- a/packages/backend/migrations/20241125224212_create_tenants_table.js +++ b/packages/backend/migrations/20241125224212_create_tenants_table.js @@ -8,6 +8,9 @@ exports.up = function (knex) { table.string('email').notNullable() table.string('publicName') table.string('apiSecret') + + table.timestamp('createdAt').defaultTo(knex.fn.now()) + table.timestamp('updatedAt').defaultTo(knex.fn.now()) }) } diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index b266c062ac..2f70ab2164 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -126,6 +126,9 @@ export const Config = { authServerGrantUrl: envString('AUTH_SERVER_GRANT_URL'), authServerIntrospectionUrl: envString('AUTH_SERVER_INTROSPECTION_URL'), + authAdminApiUrl: envString('AUTH_ADMIN_API_URL'), + authAdminApiSecret: envString('AUTH_ADMIN_API_SECRET'), + authAdminApiSignatureVersion: envInt('AUTH_ADMIN_API_SIGNATURE_VERSION', 1), outgoingPaymentWorkers: envInt('OUTGOING_PAYMENT_WORKERS', 1), outgoingPaymentWorkerIdle: envInt('OUTGOING_PAYMENT_WORKER_IDLE', 10), // milliseconds diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 91db346566..4734ace48e 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -7,6 +7,7 @@ import { createClient } from 'tigerbeetle-node' import { createClient as createIntrospectionClient } from 'token-introspection' import net from 'net' import dns from 'dns' +import { createHmac } from 'crypto' import { createAuthenticatedClient as createOpenPaymentsClient, @@ -15,6 +16,17 @@ import { } from '@interledger/open-payments' import { StreamServer } from '@interledger/stream-receiver' import axios from 'axios' +import { + ApolloClient, + ApolloLink, + createHttpLink, + InMemoryCache +} from '@apollo/client' +import { onError } from '@apollo/client/link/error' +import { setContext } from '@apollo/client/link/context' +import { canonicalize } from 'json-canonicalize' +import { print } from 'graphql/language/printer' + import { createAccountingService as createPsqlAccountingService } from './accounting/psql/service' import { createAccountingService as createTigerbeetleAccountingService } from './accounting/tigerbeetle/service' import { App, AppServices } from './app' @@ -61,6 +73,7 @@ import { } from './telemetry/service' import { createWebhookService } from './webhook/service' import { createInMemoryDataStore } from './middleware/cache/data-stores/in-memory' +import { createTenantService } from './tenants/service' BigInt.prototype.toJSON = function () { return this.toString() @@ -131,6 +144,86 @@ export function initIocContainer( }) }) + container.singleton('apolloClient', async (deps) => { + const [logger, config] = await Promise.all([ + deps.use('logger'), + deps.use('config') + ]) + + const httpLink = createHttpLink({ + uri: config.authAdminApiUrl + }) + + const errorLink = onError(({ graphQLErrors }) => { + if (graphQLErrors) { + logger.error(graphQLErrors) + graphQLErrors.map(({ extensions }) => { + if (extensions && extensions.code === 'UNAUTHENTICATED') { + logger.error('UNAUTHENTICATED') + } + + if (extensions && extensions.code === 'FORBIDDEN') { + logger.error('FORBIDDEN') + } + }) + } + }) + + const authLink = setContext((request, { headers }) => { + if (!config.authAdminApiSecret || !config.authAdminApiSignatureVersion) + return { headers } + const timestamp = Math.round(new Date().getTime() / 1000) + const version = config.authAdminApiSignatureVersion + + const { query, variables, operationName } = request + const formattedRequest = { + variables, + operationName, + query: print(query) + } + + const payload = `${timestamp}.${canonicalize(formattedRequest)}` + const hmac = createHmac('sha256', config.authAdminApiSecret) + hmac.update(payload) + const digest = hmac.digest('hex') + + return { + headers: { + ...headers, + signature: `t=${timestamp}, v${version}=${digest}` + } + } + }) + + const link = ApolloLink.from([errorLink, authLink, httpLink]) + + const client = new ApolloClient({ + cache: new InMemoryCache({}), + link: link, + defaultOptions: { + query: { + fetchPolicy: 'no-cache' + }, + mutate: { + fetchPolicy: 'no-cache' + }, + watchQuery: { + fetchPolicy: 'no-cache' + } + } + }) + + return client + }) + + container.singleton('tenantService', async (deps) => { + return createTenantService({ + logger: await deps.use('logger'), + knex: await deps.use('knex'), + apolloClient: await deps.use('apolloClient') + }) + }) + container.singleton('ratesService', async (deps) => { const config = await deps.use('config') return createRatesService({ diff --git a/packages/backend/src/tenants/model.ts b/packages/backend/src/tenants/model.ts index e1347fff7f..961de413f9 100644 --- a/packages/backend/src/tenants/model.ts +++ b/packages/backend/src/tenants/model.ts @@ -9,3 +9,11 @@ export class Tenant extends BaseModel { public apiSecret!: string public publicName?: string } + +export type TenantWithIdpConfig = Pick< + Tenant, + 'id' | 'email' | 'apiSecret' | 'publicName' | 'createdAt' | 'updatedAt' +> & { + idpConsentUrl: string + idpSecret: string +} diff --git a/packages/backend/src/tenants/service.test.ts b/packages/backend/src/tenants/service.test.ts new file mode 100644 index 0000000000..f2159b40bf --- /dev/null +++ b/packages/backend/src/tenants/service.test.ts @@ -0,0 +1,366 @@ +import assert from 'assert' +import { faker } from '@faker-js/faker' +import { IocContract } from '@adonisjs/fold' +import nock from 'nock' +import { AppServices } from '../app' +import { initIocContainer } from '..' +import { createTestApp, TestContainer } from '../tests/app' +import { TenantService } from './service' +import { Config, IAppConfig } from '../config/app' +import { truncateTables } from '../tests/tableManager' +import { ApolloClient, NormalizedCacheObject } from '@apollo/client' +import { Tenant } from './model' + +describe('Tenant Service', (): void => { + let deps: IocContract + let appContainer: TestContainer + let tenantService: TenantService + let config: IAppConfig + let apolloClient: ApolloClient + + beforeAll(async (): Promise => { + deps = initIocContainer(Config) + appContainer = await createTestApp(deps) + tenantService = await deps.use('tenantService') + config = await deps.use('config') + apolloClient = await deps.use('apolloClient') + }) + + afterEach(async (): Promise => { + await truncateTables(appContainer.knex) + }) + + afterAll(async (): Promise => { + nock.cleanAll() + await appContainer.shutdown() + }) + + describe('get', (): void => { + test('can get a tenant', async (): Promise => { + const createOptions = { + apiSecret: 'test-api-secret', + publicName: 'test tenant', + email: faker.internet.email(), + idpConsentUrl: faker.internet.url(), + idpSecret: 'test-idp-secret' + } + + const createScope = nock(config.authAdminApiUrl) + .post('') + .reply(200, { data: { createTenant: { id: 1234 } } }) + + const createdTenant = await tenantService.create(createOptions) + createScope.done() + + const getScope = nock(config.authAdminApiUrl) + .post('') + .reply(200, { + data: { + getTenant: { + tenant: { + idpConsentUrl: createOptions.idpConsentUrl, + idpSecret: createOptions.idpSecret + } + } + } + }) + const apolloSpy = jest.spyOn(apolloClient, 'query') + const tenant = await tenantService.get(createdTenant.id) + assert.ok(tenant) + expect(tenant.id).toEqual(createdTenant.id) + expect(tenant.email).toEqual(createOptions.email) + expect(tenant.publicName).toEqual(createOptions.publicName) + expect(tenant.apiSecret).toEqual(createOptions.apiSecret) + expect(tenant.idpConsentUrl).toEqual(createOptions.idpConsentUrl) + expect(tenant.idpSecret).toEqual(createOptions.idpSecret) + expect(apolloSpy).toHaveBeenCalledWith( + expect.objectContaining({ + variables: { + input: { + id: tenant.id + } + } + }) + ) + getScope.done() + }) + + test("returns undefined if auth tenant doesn't exist", async (): Promise => { + const createOptions = { + apiSecret: 'test-api-secret', + publicName: 'test tenant', + email: faker.internet.email(), + idpConsentUrl: faker.internet.url(), + idpSecret: 'test-idp-secret' + } + + const createScope = nock(config.authAdminApiUrl) + .post('') + .reply(200, { data: { createTenant: { id: 1234 } } }) + + const createdTenant = await tenantService.create(createOptions) + createScope.done() + + const getScope = nock(config.authAdminApiUrl).post('').reply(400) + const apolloSpy = jest.spyOn(apolloClient, 'query') + let tenant + try { + tenant = await tenantService.get(createdTenant.id) + } catch (err) { + expect(tenant).toBeUndefined() + expect(apolloSpy).toHaveBeenCalledWith( + expect.objectContaining({ + variables: { + input: { + id: createdTenant.id + } + } + }) + ) + } + getScope.done() + }) + }) + + describe('create', (): void => { + test('can create a tenant', async (): Promise => { + const createOptions = { + apiSecret: 'test-api-secret', + publicName: 'test tenant', + email: faker.internet.email(), + idpConsentUrl: faker.internet.url(), + idpSecret: 'test-idp-secret' + } + + const scope = nock(config.authAdminApiUrl) + .post('') + .reply(200, { data: { createTenant: { id: 1234 } } }) + + const apolloSpy = jest.spyOn(apolloClient, 'mutate') + const tenant = await tenantService.create(createOptions) + + expect(tenant.email).toEqual(createOptions.email) + expect(tenant.publicName).toEqual(createOptions.publicName) + expect(tenant.apiSecret).toEqual(createOptions.apiSecret) + + expect(apolloSpy).toHaveBeenCalledWith( + expect.objectContaining({ + variables: { + input: { + id: tenant.id, + idpSecret: createOptions.idpSecret, + idpConsentUrl: createOptions.idpConsentUrl + } + } + }) + ) + + scope.done() + }) + + test('tenant creation rolls back if auth tenant create fails', async (): Promise => { + const createOptions = { + apiSecret: 'test-api-secret', + publicName: 'test tenant', + email: faker.internet.email(), + idpConsentUrl: faker.internet.url(), + idpSecret: 'test-idp-secret' + } + + const scope = nock(config.authAdminApiUrl).post('').reply(400) + + const apolloSpy = jest.spyOn(apolloClient, 'mutate') + let tenant + try { + tenant = await tenantService.create(createOptions) + } catch (err) { + expect(tenant).toBeUndefined() + + const tenants = await Tenant.query() + expect(tenants.length).toEqual(0) + + expect(apolloSpy).toHaveBeenCalledWith( + expect.objectContaining({ + variables: { + input: { + id: expect.any(String), + idpConsentUrl: createOptions.idpConsentUrl, + idpSecret: createOptions.idpSecret + } + } + }) + ) + } + scope.done() + }) + }) + + describe('update', (): void => { + test('can update a tenant', async (): Promise => { + const originalTenantInfo = { + apiSecret: 'test-api-secret', + email: faker.internet.url(), + publicName: 'test name', + idpConsentUrl: faker.internet.url(), + idpSecret: 'test-idp-secret' + } + + const scope = nock(config.authAdminApiUrl) + .post('') + .reply(200, { data: { createTenant: { id: 1234 } } }) + .persist() + const tenant = await tenantService.create(originalTenantInfo) + + const updatedTenantInfo = { + id: tenant.id, + apiSecret: 'test-api-secret-two', + email: faker.internet.url(), + publicName: 'second test name', + idpConsentUrl: faker.internet.url(), + idpSecret: 'test-idp-secret-two' + } + + const apolloSpy = jest.spyOn(apolloClient, 'mutate') + const updatedTenant = await tenantService.update(updatedTenantInfo) + + expect(updatedTenant.apiSecret).toEqual(updatedTenantInfo.apiSecret) + expect(updatedTenant.email).toEqual(updatedTenantInfo.email) + expect(updatedTenant.publicName).toEqual(updatedTenantInfo.publicName) + expect(apolloSpy).toHaveBeenCalledWith( + expect.objectContaining({ + variables: { + input: { + id: tenant.id, + idpConsentUrl: updatedTenantInfo.idpConsentUrl, + idpSecret: updatedTenantInfo.idpSecret + } + } + }) + ) + scope.done() + }) + + test('rolls back tenant if auth tenant update fails', async (): Promise => { + const originalTenantInfo = { + apiSecret: 'test-api-secret', + email: faker.internet.url(), + publicName: 'test name', + idpConsentUrl: faker.internet.url(), + idpSecret: 'test-idp-secret' + } + + nock(config.authAdminApiUrl) + .post('') + .reply(200, { data: { createTenant: { id: 1234 } } }) + const tenant = await tenantService.create(originalTenantInfo) + const updatedTenantInfo = { + id: tenant.id, + apiSecret: 'test-api-secret-two', + email: faker.internet.url(), + publicName: 'second test name', + idpConsentUrl: faker.internet.url(), + idpSecret: 'test-idp-secret-two' + } + + nock.cleanAll() + + nock(config.authAdminApiUrl).post('').reply(400) + const apolloSpy = jest.spyOn(apolloClient, 'mutate') + let updatedTenant + try { + updatedTenant = await tenantService.update(updatedTenantInfo) + } catch (err) { + expect(updatedTenant).toBeUndefined() + const dbTenant = await Tenant.query().findById(tenant.id) + assert.ok(dbTenant) + expect(dbTenant.apiSecret).toEqual(originalTenantInfo.apiSecret) + expect(dbTenant.email).toEqual(originalTenantInfo.email) + expect(dbTenant.publicName).toEqual(originalTenantInfo.publicName) + expect(apolloSpy).toHaveBeenCalledWith( + expect.objectContaining({ + variables: { + input: { + id: tenant.id, + idpConsentUrl: updatedTenantInfo.idpConsentUrl, + idpSecret: updatedTenantInfo.idpSecret + } + } + }) + ) + } + + nock.cleanAll() + }) + }) + + describe('Delete Tenant', (): void => { + test('Can delete tenant', async (): Promise => { + const createOptions = { + apiSecret: 'test-api-secret', + email: faker.internet.url(), + publicName: 'test name', + idpConsentUrl: faker.internet.url(), + idpSecret: 'test-idp-secret' + } + + const scope = nock(config.authAdminApiUrl) + .post('') + .reply(200, { data: { createTenant: { id: 1234 } } }) + .persist() + const tenant = await tenantService.create(createOptions) + + const apolloSpy = jest.spyOn(apolloClient, 'mutate') + await tenantService.delete(tenant.id) + + const dbTenant = await Tenant.query().findById(tenant.id) + expect(dbTenant).toBeUndefined() + expect(apolloSpy).toHaveBeenCalledWith( + expect.objectContaining({ + variables: { + input: { id: tenant.id } + } + }) + ) + + scope.done() + }) + + test('Reverts deletion if auth tenant delete fails', async (): Promise => { + const createOptions = { + apiSecret: 'test-api-secret', + email: faker.internet.url(), + publicName: 'test name', + idpConsentUrl: faker.internet.url(), + idpSecret: 'test-idp-secret' + } + + nock(config.authAdminApiUrl) + .post('') + .reply(200, { data: { createTenant: { id: 1234 } } }) + const tenant = await tenantService.create(createOptions) + + nock.cleanAll() + + const apolloSpy = jest.spyOn(apolloClient, 'mutate') + const deleteScope = nock(config.authAdminApiUrl).post('').reply(400) + try { + await tenantService.delete(tenant.id) + } catch (err) { + const dbTenant = await Tenant.query().findById(tenant.id) + assert.ok(dbTenant) + expect(dbTenant.id).toEqual(tenant.id) + expect(apolloSpy).toHaveBeenCalledWith( + expect.objectContaining({ + variables: { + input: { + id: tenant.id + } + } + }) + ) + } + + deleteScope.done() + }) + }) +}) diff --git a/packages/backend/src/tenants/service.ts b/packages/backend/src/tenants/service.ts new file mode 100644 index 0000000000..be4a6b67f8 --- /dev/null +++ b/packages/backend/src/tenants/service.ts @@ -0,0 +1,202 @@ +import { Tenant, TenantWithIdpConfig } from './model' +import { BaseService } from '../shared/baseService' +import { gql, NormalizedCacheObject } from '@apollo/client' +import { ApolloClient } from '@apollo/client' +import { TransactionOrKnex } from 'objection' + +export interface TenantService { + get: (id: string) => Promise + create: (options: CreateTenantOptions) => Promise + update: (options: UpdateTenantOptions) => Promise + delete: (id: string) => Promise +} + +export interface ServiceDependencies extends BaseService { + knex: TransactionOrKnex + apolloClient: ApolloClient +} + +export async function createTenantService( + deps_: ServiceDependencies +): Promise { + const deps: ServiceDependencies = { + ...deps_, + logger: deps_.logger.child({ service: 'TenantService' }) + } + + return { + get: (id: string) => getTenant(deps, id), + create: (options) => createTenant(deps, options), + update: (options) => updateTenant(deps, options), + delete: (id) => deleteTenant(deps, id) + } +} + +async function getTenant( + deps: ServiceDependencies, + id: string +): Promise { + const tenant = await Tenant.query(deps.knex).findById(id) + if (!tenant) return undefined + + const query = gql` + query GetAuthTenant($input: GetTenantInput!) { + getTenant(input: $input) { + tenant { + id + idpConsentUrl + idpSecret + } + } + } + ` + const variables = { input: { id } } + // TODO: add type to this in https://github.com/interledger/rafiki/issues/3125 + const authTenantResponse = await deps.apolloClient.query({ query, variables }) + const authTenant = authTenantResponse.data.getTenant.tenant + if (!authTenant) { + deps.logger.error( + { tenantId: id }, + 'could not find auth tenant entry for existing backend entry' + ) + return undefined + } + + const { idpConsentUrl, idpSecret } = authTenant + return { + ...tenant, + idpConsentUrl, + idpSecret + } +} + +interface CreateTenantOptions { + email: string + apiSecret: string + idpSecret: string + idpConsentUrl: string + publicName?: string +} + +async function createTenant( + deps: ServiceDependencies, + options: CreateTenantOptions +): Promise { + const trx = await deps.knex.transaction() + try { + const { email, apiSecret, publicName, idpSecret, idpConsentUrl } = options + const tenant = await Tenant.query(trx).insertAndFetch({ + email, + publicName, + apiSecret + }) + + const mutation = gql` + mutation CreateAuthTenant($input: CreateTenantInput!) { + createTenant(input: $input) { + tenant { + id + } + } + } + ` + + const variables = { + input: { + id: tenant.id, + idpSecret, + idpConsentUrl + } + } + + // TODO: add type to this in https://github.com/interledger/rafiki/issues/3125 + await deps.apolloClient.mutate({ mutation, variables }) + await trx.commit() + return tenant + } catch (err) { + await trx.rollback() + throw err + } +} + +interface UpdateTenantOptions { + id: string + email?: string + publicName?: string + apiSecret?: string + idpConsentUrl: string | null + idpSecret: string | null +} + +async function updateTenant( + deps: ServiceDependencies, + options: UpdateTenantOptions +): Promise { + const trx = await deps.knex.transaction() + + try { + const { id, apiSecret, email, publicName, idpConsentUrl, idpSecret } = + options + const tenant = await Tenant.query(trx) + .patchAndFetchById(options.id, { + email, + publicName, + apiSecret + }) + .throwIfNotFound() + + if (idpConsentUrl || idpSecret) { + const mutation = gql` + mutation UpdateAuthTenant($input: UpdateTenantInput!) { + updateTenant(input: $input) { + tenant { + id + } + } + } + ` + + const variables = { + input: { + id, + idpConsentUrl, + idpSecret + } + } + + // TODO: add types to this in https://github.com/interledger/rafiki/issues/3125 + await deps.apolloClient.mutate({ mutation, variables }) + } + + await trx.commit() + return tenant + } catch (err) { + await trx.rollback() + throw err + } +} + +async function deleteTenant( + deps: ServiceDependencies, + id: string +): Promise { + const trx = await deps.knex.transaction() + + try { + await Tenant.query(trx).deleteById(id) + const mutation = gql` + mutation DeleteAuthTenantMutation($input: DeleteTenantInput!) { + deleteTenant(input: $input) { + sucess + } + } + ` + const variables = { input: { id } } + // TODO: add types to this in https://github.com/interledger/rafiki/issues/3125 + await deps.apolloClient.mutate({ mutation, variables }) + await trx.commit() + } catch (err) { + await trx.rollback() + throw err + } +} From 61d45f71b0b00410a90d66793a3ea417e3cdcb4d Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Wed, 27 Nov 2024 13:30:48 -0800 Subject: [PATCH 02/66] fix: integration tests --- packages/backend/package.json | 2 +- pnpm-lock.yaml | 6 +++--- .../testenv/cloud-nine-wallet/docker-compose.yml | 2 ++ test/integration/testenv/happy-life-bank/docker-compose.yml | 2 ++ 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 9c515931dd..5e0d1edc6a 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -16,7 +16,6 @@ "dev": "ts-node-dev --inspect=0.0.0.0:9229 --respawn --transpile-only --require ./src/telemetry/index.ts src/index.ts" }, "devDependencies": { - "@apollo/client": "^3.11.8", "@graphql-codegen/cli": "5.0.2", "@graphql-codegen/introspection": "4.0.3", "@graphql-codegen/typescript": "4.0.6", @@ -46,6 +45,7 @@ }, "dependencies": { "@adonisjs/fold": "^8.2.0", + "@apollo/client": "^3.11.8", "@apollo/server": "^4.11.2", "@as-integrations/koa": "^1.1.1", "@escape.tech/graphql-armor": "^2.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 641900d859..3c8c80cad1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -312,6 +312,9 @@ importers: '@adonisjs/fold': specifier: ^8.2.0 version: 8.2.0 + '@apollo/client': + specifier: ^3.11.8 + version: 3.11.8(@types/react@18.2.73)(graphql@16.8.1)(react-dom@18.2.0)(react@18.2.0) '@apollo/server': specifier: ^4.11.2 version: 4.11.2(graphql@16.8.1) @@ -472,9 +475,6 @@ importers: specifier: ^9.0.1 version: 9.0.1 devDependencies: - '@apollo/client': - specifier: ^3.11.8 - version: 3.11.8(@types/react@18.2.73)(graphql@16.8.1)(react-dom@18.2.0)(react@18.2.0) '@graphql-codegen/cli': specifier: 5.0.2 version: 5.0.2(@babel/core@7.26.0)(@types/node@18.19.64)(graphql@16.8.1) diff --git a/test/integration/testenv/cloud-nine-wallet/docker-compose.yml b/test/integration/testenv/cloud-nine-wallet/docker-compose.yml index e0cf08b12b..e205c052cc 100644 --- a/test/integration/testenv/cloud-nine-wallet/docker-compose.yml +++ b/test/integration/testenv/cloud-nine-wallet/docker-compose.yml @@ -31,6 +31,8 @@ services: PRIVATE_KEY_FILE: /workspace/private-key.pem AUTH_SERVER_INTROSPECTION_URL: http://cloud-nine-wallet-test-auth:3107 AUTH_SERVER_GRANT_URL: http://cloud-nine-wallet-test-auth:3106 + AUTH_ADMIN_API_URL: 'http://cloud-nine-wallet-test-auth:3003/graphql' + AUTH_ADMIN_API_SECRET: 'test-secret' ILP_ADDRESS: test.cloud-nine-wallet-test ILP_CONNECTOR_URL: http://cloud-nine-wallet-test-backend:3102 STREAM_SECRET: BjPXtnd00G2mRQwP/8ZpwyZASOch5sUXT5o0iR5b5wU= diff --git a/test/integration/testenv/happy-life-bank/docker-compose.yml b/test/integration/testenv/happy-life-bank/docker-compose.yml index 9cba1bc0c7..6fcb2e6f39 100644 --- a/test/integration/testenv/happy-life-bank/docker-compose.yml +++ b/test/integration/testenv/happy-life-bank/docker-compose.yml @@ -26,6 +26,8 @@ services: DATABASE_URL: postgresql://happy_life_bank_test_backend:happy_life_bank_test_backend@shared-database/happy_life_bank_test_backend AUTH_SERVER_GRANT_URL: http://happy-life-bank-test-auth:4106 AUTH_SERVER_INTROSPECTION_URL: http://happy-life-bank-test-auth:4107 + AUTH_ADMIN_API_URL: 'http://happy-life-bank-test-auth:4003/graphql' + AUTH_ADMIN_API_SECRET: 'test-secret' # matches pfry key id KEY_ID: keyid-97a3a431-8ee1-48fc-ac85-70e2f5eba8e5 PRIVATE_KEY_FILE: /workspace/private-key.pem From d57bcc6628be8ef82b1bf70eae6a6af2b79360e5 Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Wed, 27 Nov 2024 15:26:45 -0800 Subject: [PATCH 03/66] feat: use soft delete --- .../20241125224212_create_tenants_table.js | 3 +- packages/backend/src/tenants/model.ts | 11 +++++ packages/backend/src/tenants/service.test.ts | 44 ++++++++++++++++++- packages/backend/src/tenants/service.ts | 11 +++-- 4 files changed, 63 insertions(+), 6 deletions(-) diff --git a/packages/backend/migrations/20241125224212_create_tenants_table.js b/packages/backend/migrations/20241125224212_create_tenants_table.js index df567d29af..5c2579f538 100644 --- a/packages/backend/migrations/20241125224212_create_tenants_table.js +++ b/packages/backend/migrations/20241125224212_create_tenants_table.js @@ -6,11 +6,12 @@ exports.up = function (knex) { return knex.schema.createTable('tenants', function (table) { table.uuid('id').notNullable().primary() table.string('email').notNullable() + table.string('apiSecret').notNullable() table.string('publicName') - table.string('apiSecret') table.timestamp('createdAt').defaultTo(knex.fn.now()) table.timestamp('updatedAt').defaultTo(knex.fn.now()) + table.timestamp('deletedAt') }) } diff --git a/packages/backend/src/tenants/model.ts b/packages/backend/src/tenants/model.ts index 961de413f9..5af79794c0 100644 --- a/packages/backend/src/tenants/model.ts +++ b/packages/backend/src/tenants/model.ts @@ -1,4 +1,5 @@ import { BaseModel } from '../shared/baseModel' +import { Pojo } from 'objection' export class Tenant extends BaseModel { public static get tableName(): string { @@ -8,6 +9,16 @@ export class Tenant extends BaseModel { public email!: string public apiSecret!: string public publicName?: string + + public deletedAt?: Date + + $formatJson(json: Pojo): Pojo { + json = super.$formatJson(json) + return { + ...json, + deletedAt: json.deletedAt.toISOString() + } + } } export type TenantWithIdpConfig = Pick< diff --git a/packages/backend/src/tenants/service.test.ts b/packages/backend/src/tenants/service.test.ts index f2159b40bf..bee7d574ab 100644 --- a/packages/backend/src/tenants/service.test.ts +++ b/packages/backend/src/tenants/service.test.ts @@ -2,6 +2,7 @@ import assert from 'assert' import { faker } from '@faker-js/faker' import { IocContract } from '@adonisjs/fold' import nock from 'nock' +import { Knex } from 'knex' import { AppServices } from '../app' import { initIocContainer } from '..' import { createTestApp, TestContainer } from '../tests/app' @@ -17,6 +18,7 @@ describe('Tenant Service', (): void => { let tenantService: TenantService let config: IAppConfig let apolloClient: ApolloClient + let knex: Knex beforeAll(async (): Promise => { deps = initIocContainer(Config) @@ -24,6 +26,7 @@ describe('Tenant Service', (): void => { tenantService = await deps.use('tenantService') config = await deps.use('config') apolloClient = await deps.use('apolloClient') + knex = await deps.use('knex') }) afterEach(async (): Promise => { @@ -120,6 +123,17 @@ describe('Tenant Service', (): void => { } getScope.done() }) + + test('returns undefined if tenant is deleted', async (): Promise => { + const dbTenant = await Tenant.query(knex).insertAndFetch({ + apiSecret: 'test-secret', + email: faker.internet.email(), + deletedAt: new Date() + }) + + const tenant = await tenantService.get(dbTenant.id) + expect(tenant).toBeUndefined() + }) }) describe('create', (): void => { @@ -291,6 +305,31 @@ describe('Tenant Service', (): void => { nock.cleanAll() }) + + test('Cannot update deleted tenant', async (): Promise => { + const originalSecret = 'test-secret' + const dbTenant = await Tenant.query(knex).insertAndFetch({ + email: faker.internet.url(), + apiSecret: originalSecret, + deletedAt: new Date() + }) + + const apolloSpy = jest.spyOn(apolloClient, 'mutate') + try { + await tenantService.update({ + id: dbTenant.id, + apiSecret: 'test-secret-2' + }) + } catch (err) { + const dbTenantAfterUpdate = await Tenant.query(knex).findById( + dbTenant.id + ) + + assert.ok(dbTenantAfterUpdate) + expect(dbTenantAfterUpdate.apiSecret).toEqual(originalSecret) + expect(apolloSpy).toHaveBeenCalledTimes(0) + } + }) }) describe('Delete Tenant', (): void => { @@ -313,7 +352,9 @@ describe('Tenant Service', (): void => { await tenantService.delete(tenant.id) const dbTenant = await Tenant.query().findById(tenant.id) - expect(dbTenant).toBeUndefined() + expect(dbTenant?.deletedAt?.getTime()).toBeLessThanOrEqual( + new Date(Date.now()).getTime() + ) expect(apolloSpy).toHaveBeenCalledWith( expect.objectContaining({ variables: { @@ -349,6 +390,7 @@ describe('Tenant Service', (): void => { const dbTenant = await Tenant.query().findById(tenant.id) assert.ok(dbTenant) expect(dbTenant.id).toEqual(tenant.id) + expect(dbTenant.deletedAt).toBeNull() expect(apolloSpy).toHaveBeenCalledWith( expect.objectContaining({ variables: { diff --git a/packages/backend/src/tenants/service.ts b/packages/backend/src/tenants/service.ts index be4a6b67f8..0676e6957c 100644 --- a/packages/backend/src/tenants/service.ts +++ b/packages/backend/src/tenants/service.ts @@ -37,7 +37,7 @@ async function getTenant( id: string ): Promise { const tenant = await Tenant.query(deps.knex).findById(id) - if (!tenant) return undefined + if (!tenant || !!tenant.deletedAt) return undefined const query = gql` query GetAuthTenant($input: GetTenantInput!) { @@ -124,8 +124,8 @@ interface UpdateTenantOptions { email?: string publicName?: string apiSecret?: string - idpConsentUrl: string | null - idpSecret: string | null + idpConsentUrl?: string + idpSecret?: string } async function updateTenant( @@ -143,6 +143,7 @@ async function updateTenant( publicName, apiSecret }) + .whereNull('deletedAt') .throwIfNotFound() if (idpConsentUrl || idpSecret) { @@ -183,7 +184,9 @@ async function deleteTenant( const trx = await deps.knex.transaction() try { - await Tenant.query(trx).deleteById(id) + await Tenant.query(trx).patchAndFetchById(id, { + deletedAt: new Date(Date.now()) + }) const mutation = gql` mutation DeleteAuthTenantMutation($input: DeleteTenantInput!) { deleteTenant(input: $input) { From 57a663d8e41981d1796a0a1ce679ef4961ac40f0 Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Wed, 27 Nov 2024 15:37:28 -0800 Subject: [PATCH 04/66] refactor: compare whole object in test --- packages/backend/src/tenants/service.test.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/tenants/service.test.ts b/packages/backend/src/tenants/service.test.ts index bee7d574ab..e51d73dd1f 100644 --- a/packages/backend/src/tenants/service.test.ts +++ b/packages/backend/src/tenants/service.test.ts @@ -70,12 +70,11 @@ describe('Tenant Service', (): void => { const apolloSpy = jest.spyOn(apolloClient, 'query') const tenant = await tenantService.get(createdTenant.id) assert.ok(tenant) - expect(tenant.id).toEqual(createdTenant.id) - expect(tenant.email).toEqual(createOptions.email) - expect(tenant.publicName).toEqual(createOptions.publicName) - expect(tenant.apiSecret).toEqual(createOptions.apiSecret) - expect(tenant.idpConsentUrl).toEqual(createOptions.idpConsentUrl) - expect(tenant.idpSecret).toEqual(createOptions.idpSecret) + expect(tenant).toEqual({ + ...createdTenant, + idpConsentUrl: createOptions.idpConsentUrl, + idpSecret: createOptions.idpSecret + }) expect(apolloSpy).toHaveBeenCalledWith( expect.objectContaining({ variables: { From fb3d702a7845bf83aa4df799745d68f455ae4d1e Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Mon, 2 Dec 2024 09:35:49 -0800 Subject: [PATCH 05/66] fix: better gql errors in tests --- packages/backend/src/tenants/service.test.ts | 54 ++++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/tenants/service.test.ts b/packages/backend/src/tenants/service.test.ts index e51d73dd1f..39b723b7a3 100644 --- a/packages/backend/src/tenants/service.test.ts +++ b/packages/backend/src/tenants/service.test.ts @@ -12,6 +12,44 @@ import { truncateTables } from '../tests/tableManager' import { ApolloClient, NormalizedCacheObject } from '@apollo/client' import { Tenant } from './model' +const generateMutateGqlError = (path: string = 'createTenant') => ({ + errors: [ + { + message: 'invalid input syntax', + locations: [ + { + line: 1, + column: 1 + } + ], + path: [path], + extensions: { + code: 'INTERNAl_SERVER_ERROR' + } + } + ], + data: null +}) + +const queryGqlError = { + errors: [ + { + message: 'unknown peer', + locations: [ + { + line: 1, + column: 1 + } + ], + path: ['tenant'], + extensions: { + code: 'NOT_FOUND' + } + } + ], + data: null +} + describe('Tenant Service', (): void => { let deps: IocContract let appContainer: TestContainer @@ -103,7 +141,9 @@ describe('Tenant Service', (): void => { const createdTenant = await tenantService.create(createOptions) createScope.done() - const getScope = nock(config.authAdminApiUrl).post('').reply(400) + const getScope = nock(config.authAdminApiUrl) + .post('') + .reply(200, queryGqlError) const apolloSpy = jest.spyOn(apolloClient, 'query') let tenant try { @@ -180,7 +220,9 @@ describe('Tenant Service', (): void => { idpSecret: 'test-idp-secret' } - const scope = nock(config.authAdminApiUrl).post('').reply(400) + const scope = nock(config.authAdminApiUrl) + .post('') + .reply(200, generateMutateGqlError('createTenant')) const apolloSpy = jest.spyOn(apolloClient, 'mutate') let tenant @@ -277,7 +319,9 @@ describe('Tenant Service', (): void => { nock.cleanAll() - nock(config.authAdminApiUrl).post('').reply(400) + nock(config.authAdminApiUrl) + .post('') + .reply(200, generateMutateGqlError('updateTenant')) const apolloSpy = jest.spyOn(apolloClient, 'mutate') let updatedTenant try { @@ -382,7 +426,9 @@ describe('Tenant Service', (): void => { nock.cleanAll() const apolloSpy = jest.spyOn(apolloClient, 'mutate') - const deleteScope = nock(config.authAdminApiUrl).post('').reply(400) + const deleteScope = nock(config.authAdminApiUrl) + .post('') + .reply(200, generateMutateGqlError('deleteTenant')) try { await tenantService.delete(tenant.id) } catch (err) { From e5cc2b5db3e01b0a230ef972c8b5a6e247867db6 Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Mon, 2 Dec 2024 16:19:53 -0800 Subject: [PATCH 06/66] feat: add idp columns to tenant model --- .../20241125224212_create_tenants_table.js | 2 ++ packages/backend/src/tenants/model.ts | 2 ++ packages/backend/src/tenants/service.test.ts | 16 +++++++--------- packages/backend/src/tenants/service.ts | 8 ++++++-- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/backend/migrations/20241125224212_create_tenants_table.js b/packages/backend/migrations/20241125224212_create_tenants_table.js index 5c2579f538..7cb7ed1508 100644 --- a/packages/backend/migrations/20241125224212_create_tenants_table.js +++ b/packages/backend/migrations/20241125224212_create_tenants_table.js @@ -7,6 +7,8 @@ exports.up = function (knex) { table.uuid('id').notNullable().primary() table.string('email').notNullable() table.string('apiSecret').notNullable() + table.string('idpConsentUrl').notNullable() + table.string('idpSecret').notNullable() table.string('publicName') table.timestamp('createdAt').defaultTo(knex.fn.now()) diff --git a/packages/backend/src/tenants/model.ts b/packages/backend/src/tenants/model.ts index 5af79794c0..2515e9e66a 100644 --- a/packages/backend/src/tenants/model.ts +++ b/packages/backend/src/tenants/model.ts @@ -8,6 +8,8 @@ export class Tenant extends BaseModel { public email!: string public apiSecret!: string + public idpConsentUrl!: string + public idpSecret!: string public publicName?: string public deletedAt?: Date diff --git a/packages/backend/src/tenants/service.test.ts b/packages/backend/src/tenants/service.test.ts index 39b723b7a3..d15f7c97bd 100644 --- a/packages/backend/src/tenants/service.test.ts +++ b/packages/backend/src/tenants/service.test.ts @@ -167,6 +167,8 @@ describe('Tenant Service', (): void => { const dbTenant = await Tenant.query(knex).insertAndFetch({ apiSecret: 'test-secret', email: faker.internet.email(), + idpConsentUrl: faker.internet.url(), + idpSecret: 'test-idp-secret', deletedAt: new Date() }) @@ -192,9 +194,7 @@ describe('Tenant Service', (): void => { const apolloSpy = jest.spyOn(apolloClient, 'mutate') const tenant = await tenantService.create(createOptions) - expect(tenant.email).toEqual(createOptions.email) - expect(tenant.publicName).toEqual(createOptions.publicName) - expect(tenant.apiSecret).toEqual(createOptions.apiSecret) + expect(tenant).toEqual(expect.objectContaining(createOptions)) expect(apolloSpy).toHaveBeenCalledWith( expect.objectContaining({ @@ -278,9 +278,7 @@ describe('Tenant Service', (): void => { const apolloSpy = jest.spyOn(apolloClient, 'mutate') const updatedTenant = await tenantService.update(updatedTenantInfo) - expect(updatedTenant.apiSecret).toEqual(updatedTenantInfo.apiSecret) - expect(updatedTenant.email).toEqual(updatedTenantInfo.email) - expect(updatedTenant.publicName).toEqual(updatedTenantInfo.publicName) + expect(updatedTenant).toEqual(expect.objectContaining(updatedTenantInfo)) expect(apolloSpy).toHaveBeenCalledWith( expect.objectContaining({ variables: { @@ -330,9 +328,7 @@ describe('Tenant Service', (): void => { expect(updatedTenant).toBeUndefined() const dbTenant = await Tenant.query().findById(tenant.id) assert.ok(dbTenant) - expect(dbTenant.apiSecret).toEqual(originalTenantInfo.apiSecret) - expect(dbTenant.email).toEqual(originalTenantInfo.email) - expect(dbTenant.publicName).toEqual(originalTenantInfo.publicName) + expect(dbTenant).toEqual(expect.objectContaining(originalTenantInfo)) expect(apolloSpy).toHaveBeenCalledWith( expect.objectContaining({ variables: { @@ -354,6 +350,8 @@ describe('Tenant Service', (): void => { const dbTenant = await Tenant.query(knex).insertAndFetch({ email: faker.internet.url(), apiSecret: originalSecret, + idpSecret: 'test-idp-secret', + idpConsentUrl: faker.internet.url(), deletedAt: new Date() }) diff --git a/packages/backend/src/tenants/service.ts b/packages/backend/src/tenants/service.ts index 0676e6957c..9315f9698b 100644 --- a/packages/backend/src/tenants/service.ts +++ b/packages/backend/src/tenants/service.ts @@ -88,7 +88,9 @@ async function createTenant( const tenant = await Tenant.query(trx).insertAndFetch({ email, publicName, - apiSecret + apiSecret, + idpSecret, + idpConsentUrl }) const mutation = gql` @@ -141,7 +143,9 @@ async function updateTenant( .patchAndFetchById(options.id, { email, publicName, - apiSecret + apiSecret, + idpConsentUrl, + idpSecret }) .whereNull('deletedAt') .throwIfNotFound() From c824c569746e5b60b566f90a70998d9bcabfbf9c Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Tue, 3 Dec 2024 10:47:36 -0800 Subject: [PATCH 07/66] feat: pagination tests, push deletedAt to auth api call --- packages/backend/src/tenants/model.ts | 8 -- packages/backend/src/tenants/service.test.ts | 98 ++++---------------- packages/backend/src/tenants/service.ts | 56 ++++------- packages/backend/src/tests/tenant.ts | 41 ++++++++ 4 files changed, 76 insertions(+), 127 deletions(-) create mode 100644 packages/backend/src/tests/tenant.ts diff --git a/packages/backend/src/tenants/model.ts b/packages/backend/src/tenants/model.ts index 2515e9e66a..78b7a16139 100644 --- a/packages/backend/src/tenants/model.ts +++ b/packages/backend/src/tenants/model.ts @@ -22,11 +22,3 @@ export class Tenant extends BaseModel { } } } - -export type TenantWithIdpConfig = Pick< - Tenant, - 'id' | 'email' | 'apiSecret' | 'publicName' | 'createdAt' | 'updatedAt' -> & { - idpConsentUrl: string - idpSecret: string -} diff --git a/packages/backend/src/tenants/service.test.ts b/packages/backend/src/tenants/service.test.ts index d15f7c97bd..69b807de87 100644 --- a/packages/backend/src/tenants/service.test.ts +++ b/packages/backend/src/tenants/service.test.ts @@ -11,6 +11,9 @@ import { Config, IAppConfig } from '../config/app' import { truncateTables } from '../tests/tableManager' import { ApolloClient, NormalizedCacheObject } from '@apollo/client' import { Tenant } from './model' +import { getPageTests } from '../shared/baseModel.test' +import { Pagination, SortOrder } from '../shared/baseModel' +import { createTenant } from '../tests/tenant' const generateMutateGqlError = (path: string = 'createTenant') => ({ errors: [ @@ -31,25 +34,6 @@ const generateMutateGqlError = (path: string = 'createTenant') => ({ data: null }) -const queryGqlError = { - errors: [ - { - message: 'unknown peer', - locations: [ - { - line: 1, - column: 1 - } - ], - path: ['tenant'], - extensions: { - code: 'NOT_FOUND' - } - } - ], - data: null -} - describe('Tenant Service', (): void => { let deps: IocContract let appContainer: TestContainer @@ -76,6 +60,16 @@ describe('Tenant Service', (): void => { await appContainer.shutdown() }) + describe('Tenant pangination', (): void => { + describe('getPage', (): void => { + getPageTests({ + createModel: () => createTenant(deps), + getPage: (pagination?: Pagination, sortOrder?: SortOrder) => + tenantService.getPage(pagination, sortOrder) + }) + }) + }) + describe('get', (): void => { test('can get a tenant', async (): Promise => { const createOptions = { @@ -93,19 +87,6 @@ describe('Tenant Service', (): void => { const createdTenant = await tenantService.create(createOptions) createScope.done() - const getScope = nock(config.authAdminApiUrl) - .post('') - .reply(200, { - data: { - getTenant: { - tenant: { - idpConsentUrl: createOptions.idpConsentUrl, - idpSecret: createOptions.idpSecret - } - } - } - }) - const apolloSpy = jest.spyOn(apolloClient, 'query') const tenant = await tenantService.get(createdTenant.id) assert.ok(tenant) expect(tenant).toEqual({ @@ -113,54 +94,6 @@ describe('Tenant Service', (): void => { idpConsentUrl: createOptions.idpConsentUrl, idpSecret: createOptions.idpSecret }) - expect(apolloSpy).toHaveBeenCalledWith( - expect.objectContaining({ - variables: { - input: { - id: tenant.id - } - } - }) - ) - getScope.done() - }) - - test("returns undefined if auth tenant doesn't exist", async (): Promise => { - const createOptions = { - apiSecret: 'test-api-secret', - publicName: 'test tenant', - email: faker.internet.email(), - idpConsentUrl: faker.internet.url(), - idpSecret: 'test-idp-secret' - } - - const createScope = nock(config.authAdminApiUrl) - .post('') - .reply(200, { data: { createTenant: { id: 1234 } } }) - - const createdTenant = await tenantService.create(createOptions) - createScope.done() - - const getScope = nock(config.authAdminApiUrl) - .post('') - .reply(200, queryGqlError) - const apolloSpy = jest.spyOn(apolloClient, 'query') - let tenant - try { - tenant = await tenantService.get(createdTenant.id) - } catch (err) { - expect(tenant).toBeUndefined() - expect(apolloSpy).toHaveBeenCalledWith( - expect.objectContaining({ - variables: { - input: { - id: createdTenant.id - } - } - }) - ) - } - getScope.done() }) test('returns undefined if tenant is deleted', async (): Promise => { @@ -399,7 +332,7 @@ describe('Tenant Service', (): void => { expect(apolloSpy).toHaveBeenCalledWith( expect.objectContaining({ variables: { - input: { id: tenant.id } + input: { id: tenant.id, deletedAt: dbTenant?.deletedAt } } }) ) @@ -438,7 +371,8 @@ describe('Tenant Service', (): void => { expect.objectContaining({ variables: { input: { - id: tenant.id + id: tenant.id, + deletedAt: expect.any(Date) } } }) diff --git a/packages/backend/src/tenants/service.ts b/packages/backend/src/tenants/service.ts index 9315f9698b..453739c177 100644 --- a/packages/backend/src/tenants/service.ts +++ b/packages/backend/src/tenants/service.ts @@ -1,14 +1,16 @@ -import { Tenant, TenantWithIdpConfig } from './model' +import { Tenant } from './model' import { BaseService } from '../shared/baseService' import { gql, NormalizedCacheObject } from '@apollo/client' import { ApolloClient } from '@apollo/client' import { TransactionOrKnex } from 'objection' +import { Pagination, SortOrder } from '../shared/baseModel' export interface TenantService { - get: (id: string) => Promise + get: (id: string) => Promise create: (options: CreateTenantOptions) => Promise update: (options: UpdateTenantOptions) => Promise delete: (id: string) => Promise + getPage: (pagination?: Pagination, sortOrder?: SortOrder) => Promise } export interface ServiceDependencies extends BaseService { @@ -28,46 +30,25 @@ export async function createTenantService( get: (id: string) => getTenant(deps, id), create: (options) => createTenant(deps, options), update: (options) => updateTenant(deps, options), - delete: (id) => deleteTenant(deps, id) + delete: (id) => deleteTenant(deps, id), + getPage: (pagination, sortOrder) => + getTenantPage(deps, pagination, sortOrder) } } async function getTenant( deps: ServiceDependencies, id: string -): Promise { - const tenant = await Tenant.query(deps.knex).findById(id) - if (!tenant || !!tenant.deletedAt) return undefined - - const query = gql` - query GetAuthTenant($input: GetTenantInput!) { - getTenant(input: $input) { - tenant { - id - idpConsentUrl - idpSecret - } - } - } - ` - const variables = { input: { id } } - // TODO: add type to this in https://github.com/interledger/rafiki/issues/3125 - const authTenantResponse = await deps.apolloClient.query({ query, variables }) - const authTenant = authTenantResponse.data.getTenant.tenant - if (!authTenant) { - deps.logger.error( - { tenantId: id }, - 'could not find auth tenant entry for existing backend entry' - ) - return undefined - } +): Promise { + return await Tenant.query(deps.knex).findById(id).whereNull('deletedAt') +} - const { idpConsentUrl, idpSecret } = authTenant - return { - ...tenant, - idpConsentUrl, - idpSecret - } +async function getTenantPage( + deps: ServiceDependencies, + pagination?: Pagination, + sortOrder?: SortOrder +): Promise { + return await Tenant.query(deps.knex).getPage(pagination, sortOrder) } interface CreateTenantOptions { @@ -188,8 +169,9 @@ async function deleteTenant( const trx = await deps.knex.transaction() try { + const deletedAt = new Date(Date.now()) await Tenant.query(trx).patchAndFetchById(id, { - deletedAt: new Date(Date.now()) + deletedAt }) const mutation = gql` mutation DeleteAuthTenantMutation($input: DeleteTenantInput!) { @@ -198,7 +180,7 @@ async function deleteTenant( } } ` - const variables = { input: { id } } + const variables = { input: { id, deletedAt } } // TODO: add types to this in https://github.com/interledger/rafiki/issues/3125 await deps.apolloClient.mutate({ mutation, variables }) await trx.commit() diff --git a/packages/backend/src/tests/tenant.ts b/packages/backend/src/tests/tenant.ts new file mode 100644 index 0000000000..4ac1488b84 --- /dev/null +++ b/packages/backend/src/tests/tenant.ts @@ -0,0 +1,41 @@ +import { IocContract } from '@adonisjs/fold' +import { faker } from '@faker-js/faker' +import { AppServices } from '../app' +import { Tenant } from '../tenants/model' + +interface CreateOptions { + email: string + publicName?: string + apiSecret: string + idpConsentUrl: string + idpSecret: string +} + +const nock = (global as unknown as { nock: typeof import('nock') }).nock + +export async function createTenant( + deps: IocContract, + options?: CreateOptions +): Promise { + const tenantService = await deps.use('tenantService') + const config = await deps.use('config') + const scope = nock(config.authAdminApiUrl) + .post('') + .reply(200, { data: { createTenant: { id: 1234 } } }) + const tenant = await tenantService.create( + options || { + email: faker.internet.email(), + apiSecret: 'test-api-secret', + publicName: faker.company.name(), + idpConsentUrl: faker.internet.url(), + idpSecret: 'test-idp-secret' + } + ) + scope.done() + + if (!tenant) { + throw Error('Failed to create test tenant') + } + + return tenant +} From 9211ca3b29a3127592c48242c93ee2241b954679 Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Tue, 3 Dec 2024 14:35:59 -0800 Subject: [PATCH 08/66] feat: add cache --- packages/backend/src/index.ts | 7 +- packages/backend/src/tenants/service.test.ts | 100 +++++++++++++++++-- packages/backend/src/tenants/service.ts | 15 ++- 3 files changed, 109 insertions(+), 13 deletions(-) diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 4734ace48e..b167410756 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -216,11 +216,16 @@ export function initIocContainer( return client }) + container.singleton('tenantCache', async () => { + return createInMemoryDataStore(config.localCacheDuration) + }) + container.singleton('tenantService', async (deps) => { return createTenantService({ logger: await deps.use('logger'), knex: await deps.use('knex'), - apolloClient: await deps.use('apolloClient') + apolloClient: await deps.use('apolloClient'), + tenantCache: await deps.use('tenantCache') }) }) diff --git a/packages/backend/src/tenants/service.test.ts b/packages/backend/src/tenants/service.test.ts index 69b807de87..84fad049ca 100644 --- a/packages/backend/src/tenants/service.test.ts +++ b/packages/backend/src/tenants/service.test.ts @@ -14,6 +14,7 @@ import { Tenant } from './model' import { getPageTests } from '../shared/baseModel.test' import { Pagination, SortOrder } from '../shared/baseModel' import { createTenant } from '../tests/tenant' +import { CacheDataStore } from '../middleware/cache/data-stores' const generateMutateGqlError = (path: string = 'createTenant') => ({ errors: [ @@ -80,20 +81,12 @@ describe('Tenant Service', (): void => { idpSecret: 'test-idp-secret' } - const createScope = nock(config.authAdminApiUrl) - .post('') - .reply(200, { data: { createTenant: { id: 1234 } } }) - - const createdTenant = await tenantService.create(createOptions) - createScope.done() + const createdTenant = + await Tenant.query(knex).insertAndFetch(createOptions) const tenant = await tenantService.get(createdTenant.id) assert.ok(tenant) - expect(tenant).toEqual({ - ...createdTenant, - idpConsentUrl: createOptions.idpConsentUrl, - idpSecret: createOptions.idpSecret - }) + expect(tenant).toEqual(createdTenant) }) test('returns undefined if tenant is deleted', async (): Promise => { @@ -382,4 +375,89 @@ describe('Tenant Service', (): void => { deleteScope.done() }) }) + + describe('Tenant Service using cache', (): void => { + let deps: IocContract + let appContainer: TestContainer + let config: IAppConfig + let tenantService: TenantService + let tenantCache: CacheDataStore + + beforeAll(async (): Promise => { + deps = initIocContainer({ + ...Config, + localCacheDuration: 5_000 // 5-second default. + }) + appContainer = await createTestApp(deps) + config = await deps.use('config') + tenantService = await deps.use('tenantService') + tenantCache = await deps.use('tenantCache') + }) + + afterEach(async (): Promise => { + await truncateTables(appContainer.knex) + }) + + afterAll(async (): Promise => { + await appContainer.shutdown() + }) + + describe('create, update, and retrieve tenant using cache', (): void => { + test('Tenant can be created, updated, and fetched', async (): Promise => { + const createOptions = { + email: faker.internet.email(), + publicName: faker.company.name(), + apiSecret: 'test-api-secret', + idpConsentUrl: faker.internet.url(), + idpSecret: 'test-idp-secret' + } + + const scope = nock(config.authAdminApiUrl) + .post('') + .reply(200, { data: { createTenant: { tenant: { id: 1234 } } } }) + .persist() + + const spyCacheSet = jest.spyOn(tenantCache, 'set') + const tenant = await tenantService.create(createOptions) + expect(tenant).toMatchObject({ + ...createOptions, + id: tenant.id + }) + + // Ensure that the cache was set for create + expect(spyCacheSet).toHaveBeenCalledTimes(1) + + const spyCacheGet = jest.spyOn(tenantCache, 'get') + await expect(tenantService.get(tenant.id)).resolves.toEqual(tenant) + + expect(spyCacheGet).toHaveBeenCalledTimes(1) + expect(spyCacheGet).toHaveBeenCalledWith(tenant.id) + + const spyCacheUpdateSet = jest.spyOn(tenantCache, 'set') + const updatedTenant = await tenantService.update({ + id: tenant.id, + apiSecret: 'test-api-secret-2' + }) + + await expect(tenantService.get(tenant.id)).resolves.toEqual( + updatedTenant + ) + + // Ensure that cache was set for update + expect(spyCacheUpdateSet).toHaveBeenCalledTimes(2) + expect(spyCacheUpdateSet).toHaveBeenCalledWith(tenant.id, updatedTenant) + + const spyCacheDelete = jest.spyOn(tenantCache, 'delete') + await tenantService.delete(tenant.id) + + await expect(tenantService.get(tenant.id)).resolves.toBeUndefined() + + // Ensure that cache was set for deletion + expect(spyCacheDelete).toHaveBeenCalledTimes(1) + expect(spyCacheDelete).toHaveBeenCalledWith(tenant.id) + + scope.done() + }) + }) + }) }) diff --git a/packages/backend/src/tenants/service.ts b/packages/backend/src/tenants/service.ts index 453739c177..6c06253684 100644 --- a/packages/backend/src/tenants/service.ts +++ b/packages/backend/src/tenants/service.ts @@ -4,6 +4,7 @@ import { gql, NormalizedCacheObject } from '@apollo/client' import { ApolloClient } from '@apollo/client' import { TransactionOrKnex } from 'objection' import { Pagination, SortOrder } from '../shared/baseModel' +import { CacheDataStore } from '../middleware/cache/data-stores' export interface TenantService { get: (id: string) => Promise @@ -16,6 +17,7 @@ export interface TenantService { export interface ServiceDependencies extends BaseService { knex: TransactionOrKnex apolloClient: ApolloClient + tenantCache: CacheDataStore } export async function createTenantService( @@ -40,7 +42,14 @@ async function getTenant( deps: ServiceDependencies, id: string ): Promise { - return await Tenant.query(deps.knex).findById(id).whereNull('deletedAt') + const inMem = await deps.tenantCache.get(id) + if (inMem) return inMem + const tenant = await Tenant.query(deps.knex) + .findById(id) + .whereNull('deletedAt') + if (tenant) await deps.tenantCache.set(tenant.id, tenant) + + return tenant } async function getTenantPage( @@ -95,6 +104,8 @@ async function createTenant( // TODO: add type to this in https://github.com/interledger/rafiki/issues/3125 await deps.apolloClient.mutate({ mutation, variables }) await trx.commit() + + await deps.tenantCache.set(tenant.id, tenant) return tenant } catch (err) { await trx.rollback() @@ -155,6 +166,7 @@ async function updateTenant( } await trx.commit() + await deps.tenantCache.set(tenant.id, tenant) return tenant } catch (err) { await trx.rollback() @@ -168,6 +180,7 @@ async function deleteTenant( ): Promise { const trx = await deps.knex.transaction() + await deps.tenantCache.delete(id) try { const deletedAt = new Date(Date.now()) await Tenant.query(trx).patchAndFetchById(id, { From e2bbc79882834cf61bc3029c648ec4ca27226586 Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Tue, 3 Dec 2024 15:25:39 -0800 Subject: [PATCH 09/66] fix: update localenv environment variables --- localenv/cloud-nine-wallet/docker-compose.yml | 2 ++ localenv/happy-life-bank/docker-compose.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/localenv/cloud-nine-wallet/docker-compose.yml b/localenv/cloud-nine-wallet/docker-compose.yml index b0a75933d8..a493be4b71 100644 --- a/localenv/cloud-nine-wallet/docker-compose.yml +++ b/localenv/cloud-nine-wallet/docker-compose.yml @@ -65,6 +65,8 @@ services: TIGERBEETLE_REPLICA_ADDRESSES: ${TIGERBEETLE_REPLICA_ADDRESSES-''} AUTH_SERVER_GRANT_URL: ${CLOUD_NINE_AUTH_SERVER_DOMAIN:-http://cloud-nine-wallet-auth:3006} AUTH_SERVER_INTROSPECTION_URL: http://cloud-nine-wallet-auth:3007 + AUTH_ADMIN_API_URL: 'http://cloud-nine-wallet-auth:3003/graphql' + AUTH_ADMIN_API_SECRET: 'rPoZpe9tVyBNCigm05QDco7WLcYa0xMao7lO5KG1XG4=' ILP_ADDRESS: ${ILP_ADDRESS:-test.cloud-nine-wallet} STREAM_SECRET: BjPXtnd00G2mRQwP/8ZpwyZASOch5sUXT5o0iR5b5wU= API_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964= diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index e72e1654bd..93475143f1 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -58,6 +58,8 @@ services: USE_TIGERBEETLE: false AUTH_SERVER_GRANT_URL: ${HAPPY_LIFE_BANK_AUTH_SERVER_DOMAIN:-http://happy-life-bank-auth:3006} AUTH_SERVER_INTROSPECTION_URL: http://happy-life-bank-auth:3007 + AUTH_ADMIN_API_URL: 'http://happy-life-bank-auth:4003/graphql' + AUTH_ADMIN_API_SECRET: 'rPoZpe9tVyBNCigm05QDco7WLcYa0xMao7lO5KG1XG4=' ILP_ADDRESS: test.happy-life-bank ILP_CONNECTOR_URL: http://happy-life-bank-backend:4002 STREAM_SECRET: BjPXtnd00G2mRQwP/8ZpwyZASOch5sUXT5o0iR5b5wU= From d6257fe12a0dbf12edc81b7404a99875f2706d76 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Thu, 5 Dec 2024 09:11:14 +0100 Subject: [PATCH 10/66] feat(3114): add tenant to wallet address. --- .../generated/graphql.ts | 2 ++ ...1203112902_add_tenant_to_wallet_address.js | 25 +++++++++++++++++++ packages/backend/src/asset/service.test.ts | 10 ++++++++ .../src/graphql/generated/graphql.schema.json | 16 ++++++++++++ .../backend/src/graphql/generated/graphql.ts | 2 ++ .../graphql/resolvers/wallet_address.test.ts | 6 +++++ .../src/graphql/resolvers/wallet_address.ts | 1 + packages/backend/src/graphql/schema.graphql | 2 ++ .../src/open_payments/wallet_address/model.ts | 12 +++++++++ .../wallet_address/service.test.ts | 5 +++- .../open_payments/wallet_address/service.ts | 18 ++++++++++--- packages/backend/src/tests/walletAddress.ts | 2 ++ packages/frontend/app/generated/graphql.ts | 2 ++ .../src/generated/graphql.ts | 2 ++ .../src/requesters.ts | 6 ++++- packages/mock-account-service-lib/src/seed.ts | 2 ++ test/integration/lib/generated/graphql.ts | 2 ++ 17 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 packages/backend/migrations/20241203112902_add_tenant_to_wallet_address.js diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 46dad2dedb..8ed0c6a6d9 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -373,6 +373,8 @@ export type CreateWalletAddressInput = { idempotencyKey?: InputMaybe; /** Public name associated with the wallet address. This is visible to anyone with the wallet address URL. */ publicName?: InputMaybe; + /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. */ + tenantId: Scalars['String']['input']; /** Wallet address URL. This cannot be changed. */ url: Scalars['String']['input']; }; diff --git a/packages/backend/migrations/20241203112902_add_tenant_to_wallet_address.js b/packages/backend/migrations/20241203112902_add_tenant_to_wallet_address.js new file mode 100644 index 0000000000..e0a4aec076 --- /dev/null +++ b/packages/backend/migrations/20241203112902_add_tenant_to_wallet_address.js @@ -0,0 +1,25 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return Promise.all([ + knex.schema.alterTable('walletAddresses', function (table) { + table.foreign(['tenantId']).references('tenants.id') + table.index(['tenantId']) + }) + ]) +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return Promise.all([ + knex.schema.alterTable('walletAddresses', function (table) { + table.foreign(['tenantId']).references('tenants.id') + table.index(['tenantId']) + }) + ]) +} diff --git a/packages/backend/src/asset/service.test.ts b/packages/backend/src/asset/service.test.ts index 6c05b221a2..ccff2233e8 100644 --- a/packages/backend/src/asset/service.test.ts +++ b/packages/backend/src/asset/service.test.ts @@ -21,11 +21,13 @@ import { isWalletAddressError } from '../open_payments/wallet_address/errors' import { PeerService } from '../payment-method/ilp/peer/service' import { isPeerError } from '../payment-method/ilp/peer/errors' import { CacheDataStore } from '../middleware/cache/data-stores' +import { TenantService } from '../tenants/service' describe('Asset Service', (): void => { let deps: IocContract let appContainer: TestContainer let assetService: AssetService + let tenantService: TenantService let peerService: PeerService let walletAddressService: WalletAddressService @@ -272,9 +274,17 @@ describe('Asset Service', (): void => { assert.ok(!isAssetError(newAsset)) const newAssetId = newAsset.id + const newTenant = await tenantService.create({ + email: 'test@tenent.za', + apiSecret: 'secret', + idpSecret: 'idpSecret', + idpConsentUrl: 'idpConsentUrl' + }) + // make sure there is at least 1 wallet address using asset const walletAddress = walletAddressService.create({ url: 'https://alice.me/.well-known/pay', + tenantId: newTenant.id, assetId: newAssetId }) assert.ok(!isWalletAddressError(walletAddress)) diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index 1745966aa2..7e56986212 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -2171,6 +2171,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "tenantId", + "description": "Unique identifier of the tenant associated with the wallet address. This cannot be changed.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "url", "description": "Wallet address URL. This cannot be changed.", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 46dad2dedb..8ed0c6a6d9 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -373,6 +373,8 @@ export type CreateWalletAddressInput = { idempotencyKey?: InputMaybe; /** Public name associated with the wallet address. This is visible to anyone with the wallet address URL. */ publicName?: InputMaybe; + /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. */ + tenantId: Scalars['String']['input']; /** Wallet address URL. This cannot be changed. */ url: Scalars['String']['input']; }; diff --git a/packages/backend/src/graphql/resolvers/wallet_address.test.ts b/packages/backend/src/graphql/resolvers/wallet_address.test.ts index 8f559d8a40..3c26433fa9 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.test.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.test.ts @@ -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' @@ -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 @@ -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 => { asset = await createAsset(deps) + tenant = await createTenant(deps) input = { + tenantId: tenant.id, assetId: asset.id, url: 'https://alice.me/.well-known/pay' } diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index d1f7172dab..2fdcb25439 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -99,6 +99,7 @@ export const createWalletAddress: MutationResolvers['createWallet const options: CreateOptions = { assetId: args.input.assetId, + tenantId: args.input.tenantId, additionalProperties: addProps, publicName: args.input.publicName, url: args.input.url diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index f8286e14d4..059ca5669c 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -1226,6 +1226,8 @@ type CreateReceiverResponse { } input CreateWalletAddressInput { + "Unique identifier of the tenant associated with the wallet address. This cannot be changed." + tenantId: String! "Unique identifier of the asset associated with the wallet address. This cannot be changed." assetId: String! "Wallet address URL. This cannot be changed." diff --git a/packages/backend/src/open_payments/wallet_address/model.ts b/packages/backend/src/open_payments/wallet_address/model.ts index 81dd603a1d..7b8200608d 100644 --- a/packages/backend/src/open_payments/wallet_address/model.ts +++ b/packages/backend/src/open_payments/wallet_address/model.ts @@ -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 @@ -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, @@ -53,6 +62,9 @@ export class WalletAddress public readonly assetId!: string public asset!: Asset + public readonly tenantId!: string + public tenant!: Tenant + // The cumulative received amount tracked by // `wallet_address.web_monetization` webhook events. // The value should be equivalent to the following query: diff --git a/packages/backend/src/open_payments/wallet_address/service.test.ts b/packages/backend/src/open_payments/wallet_address/service.test.ts index b2b7245010..387aa0c47f 100644 --- a/packages/backend/src/open_payments/wallet_address/service.test.ts +++ b/packages/backend/src/open_payments/wallet_address/service.test.ts @@ -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' @@ -61,9 +62,11 @@ describe('Open Payments Wallet Address Service', (): void => { beforeEach(async (): Promise => { const { id: assetId } = await createAsset(deps) + const { id: tenantId } = await createTenant(deps) options = { url: 'https://alice.me/.well-known/pay', - assetId + assetId, + tenantId } }) diff --git a/packages/backend/src/open_payments/wallet_address/service.ts b/packages/backend/src/open_payments/wallet_address/service.ts index b2b79bd5c3..14738467a4 100644 --- a/packages/backend/src/open_payments/wallet_address/service.ts +++ b/packages/backend/src/open_payments/wallet_address/service.ts @@ -28,6 +28,7 @@ import { poll } from '../../shared/utils' import { WalletAddressAdditionalProperty } from './additional_property/model' import { AssetService } from '../../asset/service' import { CacheDataStore } from '../../middleware/cache/data-stores' +import { Tenant } from '../../tenants/model' interface Options { publicName?: string @@ -41,6 +42,7 @@ export type WalletAddressAdditionalPropertyInput = Pick< export interface CreateOptions extends Options { url: string assetId: string + tenantId: string additionalProperties?: WalletAddressAdditionalPropertyInput[] } @@ -176,6 +178,7 @@ async function createWalletAddress( const walletAddress = await WalletAddress.query( deps.knex ).insertGraphAndFetch({ + tenantId: options.tenantId, url: options.url.toLowerCase(), publicName: options.publicName, assetId: options.assetId, @@ -184,6 +187,9 @@ async function createWalletAddress( const asset = await deps.assetService.get(walletAddress.assetId) if (asset) walletAddress.asset = asset + const tenant: Tenant | undefined = undefined //TODO need to lookup tenant here! + if (tenant) walletAddress.tenant = tenant + await deps.walletAddressCache.set(walletAddress.id, walletAddress) return walletAddress } catch (err) { @@ -340,9 +346,15 @@ async function getWalletAddressPage( pagination?: Pagination, sortOrder?: SortOrder ): Promise { - return await WalletAddress.query(deps.knex) - .getPage(pagination, sortOrder) - .withGraphFetched('asset') + const addresses = await WalletAddress.query(deps.knex).getPage( + pagination, + sortOrder + ) + for (const address of addresses) { + const asset = await deps.assetService.get(address.assetId) + if (asset) address.asset = asset + } + return addresses } // Returns the id of the processed wallet address (if any). diff --git a/packages/backend/src/tests/walletAddress.ts b/packages/backend/src/tests/walletAddress.ts index 3149d73a56..cf459ed6a8 100644 --- a/packages/backend/src/tests/walletAddress.ts +++ b/packages/backend/src/tests/walletAddress.ts @@ -6,6 +6,7 @@ import { URL } from 'url' import { testAccessToken } from './app' import { createAsset } from './asset' +import { createTenant } from './tenant' import { AppServices } from '../app' import { isWalletAddressError } from '../open_payments/wallet_address/errors' import { WalletAddress } from '../open_payments/wallet_address/model' @@ -32,6 +33,7 @@ export async function createWalletAddress( const walletAddressOrError = (await walletAddressService.create({ ...options, assetId: options.assetId || (await createAsset(deps)).id, + tenantId: options.tenantId || (await createTenant(deps)).id, url: options.url || `https://${faker.internet.domainName()}/.well-known/pay` })) as MockWalletAddress if (isWalletAddressError(walletAddressOrError)) { diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index 1116595860..e0602eccc5 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -373,6 +373,8 @@ export type CreateWalletAddressInput = { idempotencyKey?: InputMaybe; /** Public name associated with the wallet address. This is visible to anyone with the wallet address URL. */ publicName?: InputMaybe; + /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. */ + tenantId: Scalars['String']['input']; /** Wallet address URL. This cannot be changed. */ url: Scalars['String']['input']; }; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 46dad2dedb..8ed0c6a6d9 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -373,6 +373,8 @@ export type CreateWalletAddressInput = { idempotencyKey?: InputMaybe; /** Public name associated with the wallet address. This is visible to anyone with the wallet address URL. */ publicName?: InputMaybe; + /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. */ + tenantId: Scalars['String']['input']; /** Wallet address URL. This cannot be changed. */ url: Scalars['String']['input']; }; diff --git a/packages/mock-account-service-lib/src/requesters.ts b/packages/mock-account-service-lib/src/requesters.ts index 2f79324190..b95c763981 100644 --- a/packages/mock-account-service-lib/src/requesters.ts +++ b/packages/mock-account-service-lib/src/requesters.ts @@ -52,6 +52,7 @@ export function createRequesters( createWalletAddress: ( accountName: string, accountUrl: string, + tenantId: string, assetId: string ) => Promise createWalletAddressKey: ({ @@ -101,12 +102,13 @@ export function createRequesters( depositPeerLiquidity(apolloClient, logger, peerId, amount, transferUid), depositAssetLiquidity: (assetId, amount, transferId) => depositAssetLiquidity(apolloClient, logger, assetId, amount, transferId), - createWalletAddress: (accountName, accountUrl, assetId) => + createWalletAddress: (accountName, accountUrl, tenantId, assetId) => createWalletAddress( apolloClient, logger, accountName, accountUrl, + tenantId, assetId ), createWalletAddressKey: ({ walletAddressId, jwk }) => @@ -326,6 +328,7 @@ export async function createWalletAddress( logger: Logger, accountName: string, accountUrl: string, + tenantId: string, assetId: string ): Promise { const createWalletAddressMutation = gql` @@ -340,6 +343,7 @@ export async function createWalletAddress( } ` const createWalletAddressInput: CreateWalletAddressInput = { + tenantId, assetId, url: accountUrl, publicName: accountName, diff --git a/packages/mock-account-service-lib/src/seed.ts b/packages/mock-account-service-lib/src/seed.ts index 6ccc50681a..1c6d243100 100644 --- a/packages/mock-account-service-lib/src/seed.ts +++ b/packages/mock-account-service-lib/src/seed.ts @@ -140,11 +140,13 @@ export async function setupFromSeed( logger.debug('hostname: ', config.publicHost) const url = `${config.publicHost}/${account.path}` + const tenantId = 'TODO' //TODO @jason complete this: <- let walletAddress = await getWalletAddressByURL(url) if (!walletAddress) { walletAddress = await createWalletAddress( account.name, url, + tenantId, accountAsset.id ) } diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 46dad2dedb..8ed0c6a6d9 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -373,6 +373,8 @@ export type CreateWalletAddressInput = { idempotencyKey?: InputMaybe; /** Public name associated with the wallet address. This is visible to anyone with the wallet address URL. */ publicName?: InputMaybe; + /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. */ + tenantId: Scalars['String']['input']; /** Wallet address URL. This cannot be changed. */ url: Scalars['String']['input']; }; From 86d9a3973a99c7d46353eb3c406c315c3e44d392 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Thu, 5 Dec 2024 12:24:12 +0100 Subject: [PATCH 11/66] feat(3114): test fixes. --- ...1203112902_add_tenant_to_wallet_address.js | 5 ++- packages/backend/src/index.ts | 1 + .../open_payments/wallet_address/service.ts | 44 +++++++++++++++---- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/packages/backend/migrations/20241203112902_add_tenant_to_wallet_address.js b/packages/backend/migrations/20241203112902_add_tenant_to_wallet_address.js index e0a4aec076..c028ec3be6 100644 --- a/packages/backend/migrations/20241203112902_add_tenant_to_wallet_address.js +++ b/packages/backend/migrations/20241203112902_add_tenant_to_wallet_address.js @@ -5,6 +5,7 @@ exports.up = function (knex) { return Promise.all([ knex.schema.alterTable('walletAddresses', function (table) { + table.uuid('tenantId') table.foreign(['tenantId']).references('tenants.id') table.index(['tenantId']) }) @@ -18,8 +19,8 @@ exports.up = function (knex) { exports.down = function (knex) { return Promise.all([ knex.schema.alterTable('walletAddresses', function (table) { - table.foreign(['tenantId']).references('tenants.id') - table.index(['tenantId']) + table.dropIndex('tenantId') + table.dropColumn('tenantId') }) ]) } diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index b167410756..4919741053 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -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') }) diff --git a/packages/backend/src/open_payments/wallet_address/service.ts b/packages/backend/src/open_payments/wallet_address/service.ts index 14738467a4..7826079d11 100644 --- a/packages/backend/src/open_payments/wallet_address/service.ts +++ b/packages/backend/src/open_payments/wallet_address/service.ts @@ -28,7 +28,7 @@ import { poll } from '../../shared/utils' import { WalletAddressAdditionalProperty } from './additional_property/model' import { AssetService } from '../../asset/service' import { CacheDataStore } from '../../middleware/cache/data-stores' -import { Tenant } from '../../tenants/model' +import { TenantService } from '../../tenants/service' interface Options { publicName?: string @@ -82,6 +82,7 @@ interface ServiceDependencies extends BaseService { knex: TransactionOrKnex accountingService: AccountingService webhookService: WebhookService + tenantService: TenantService assetService: AssetService walletAddressCache: CacheDataStore } @@ -92,6 +93,7 @@ export async function createWalletAddressService({ knex, accountingService, webhookService, + tenantService, assetService, walletAddressCache }: ServiceDependencies): Promise { @@ -104,6 +106,7 @@ export async function createWalletAddressService({ knex, accountingService, webhookService, + tenantService, assetService, walletAddressCache } @@ -184,10 +187,12 @@ async function createWalletAddress( assetId: options.assetId, additionalProperties: additionalProperties }) - const asset = await deps.assetService.get(walletAddress.assetId) - if (asset) walletAddress.asset = asset - const tenant: Tenant | undefined = undefined //TODO need to lookup tenant here! + const [asset, tenant] = await Promise.all([ + deps.assetService.get(walletAddress.assetId), + deps.tenantService.get(walletAddress.tenantId) + ]) + if (asset) walletAddress.asset = asset if (tenant) walletAddress.tenant = tenant await deps.walletAddressCache.set(walletAddress.id, walletAddress) @@ -228,8 +233,12 @@ async function updateWalletAddress( .$query(trx) .patchAndFetch(update) .throwIfNotFound() - const asset = await deps.assetService.get(updatedWalletAddress.assetId) + const [asset, tenant] = await Promise.all([ + deps.assetService.get(updatedWalletAddress.assetId), + deps.tenantService.get(updatedWalletAddress.tenantId) + ]) if (asset) updatedWalletAddress.asset = asset + if (tenant) updatedWalletAddress.tenant = tenant // Override all existing additional properties if new ones are provided if (additionalProperties) { @@ -273,8 +282,13 @@ async function getWalletAddress( const walletAddress = await WalletAddress.query(deps.knex).findById(id) if (walletAddress) { - const asset = await deps.assetService.get(walletAddress.assetId) + const [asset, tenant] = await Promise.all([ + deps.assetService.get(walletAddress.assetId), + deps.tenantService.get(walletAddress.tenantId) + ]) if (asset) walletAddress.asset = asset + if (tenant) walletAddress.tenant = tenant + await deps.walletAddressCache.set(id, walletAddress) } return walletAddress @@ -335,8 +349,12 @@ async function getWalletAddressByUrl( url: url.toLowerCase() }) if (walletAddress) { - const asset = await deps.assetService.get(walletAddress.assetId) + const [asset, tenant] = await Promise.all([ + deps.assetService.get(walletAddress.assetId), + deps.tenantService.get(walletAddress.tenantId) + ]) if (asset) walletAddress.asset = asset + if (tenant) walletAddress.tenant = tenant } return walletAddress || undefined } @@ -351,8 +369,12 @@ async function getWalletAddressPage( sortOrder ) for (const address of addresses) { - const asset = await deps.assetService.get(address.assetId) + const [asset, tenant] = await Promise.all([ + deps.assetService.get(address.assetId), + deps.tenantService.get(address.tenantId) + ]) if (asset) address.asset = asset + if (tenant) address.tenant = tenant } return addresses } @@ -389,8 +411,12 @@ async function processNextWalletAddresses( .skipLocked() .where('processAt', '<=', now) for (const walletAddress of walletAddresses) { - const asset = await deps_.assetService.get(walletAddress.assetId) + const [asset, tenant] = await Promise.all([ + deps_.assetService.get(walletAddress.assetId), + deps_.tenantService.get(walletAddress.tenantId) + ]) if (asset) walletAddress.asset = asset + if (tenant) walletAddress.tenant = tenant } const deps = { From 3625938cdb5fa3ce8d8ab2b06da0e68fd30357b4 Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Fri, 6 Dec 2024 14:47:53 -0800 Subject: [PATCH 12/66] feat: make some tenants fields optional, small refactors --- .../migrations/20241125224212_create_tenants_table.js | 6 +++--- packages/backend/src/tenants/service.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/backend/migrations/20241125224212_create_tenants_table.js b/packages/backend/migrations/20241125224212_create_tenants_table.js index 7cb7ed1508..e6fc77e934 100644 --- a/packages/backend/migrations/20241125224212_create_tenants_table.js +++ b/packages/backend/migrations/20241125224212_create_tenants_table.js @@ -5,10 +5,10 @@ exports.up = function (knex) { return knex.schema.createTable('tenants', function (table) { table.uuid('id').notNullable().primary() - table.string('email').notNullable() + table.string('email') table.string('apiSecret').notNullable() - table.string('idpConsentUrl').notNullable() - table.string('idpSecret').notNullable() + table.string('idpConsentUrl') + table.string('idpSecret') table.string('publicName') table.timestamp('createdAt').defaultTo(knex.fn.now()) diff --git a/packages/backend/src/tenants/service.ts b/packages/backend/src/tenants/service.ts index 6c06253684..d1973471eb 100644 --- a/packages/backend/src/tenants/service.ts +++ b/packages/backend/src/tenants/service.ts @@ -182,7 +182,7 @@ async function deleteTenant( await deps.tenantCache.delete(id) try { - const deletedAt = new Date(Date.now()) + const deletedAt = new Date() await Tenant.query(trx).patchAndFetchById(id, { deletedAt }) From 490f7484eb79ee05f42e42b5841718147f03a5b5 Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Mon, 25 Nov 2024 15:55:31 -0800 Subject: [PATCH 13/66] feat(auth): tenants table v1 --- packages/auth/migrations/20241125233415_create_tenants_table.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/auth/migrations/20241125233415_create_tenants_table.js b/packages/auth/migrations/20241125233415_create_tenants_table.js index 9112108977..da05937be6 100644 --- a/packages/auth/migrations/20241125233415_create_tenants_table.js +++ b/packages/auth/migrations/20241125233415_create_tenants_table.js @@ -8,8 +8,6 @@ exports.up = function (knex) { table.string('idpConsentUrl').notNullable() table.string('idpSecret').notNullable() - table.timestamp('createdAt').defaultTo(knex.fn.now()) - table.timestamp('updatedAt').defaultTo(knex.fn.now()) table.timestamp('deletedAt') }) } From 10bc6ab0c4bf29bd2b768d3c8365bb18a33ee93c Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Wed, 27 Nov 2024 10:20:34 -0800 Subject: [PATCH 14/66] feat(backend): tenant service --- packages/backend/src/tenants/model.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/backend/src/tenants/model.ts b/packages/backend/src/tenants/model.ts index 78b7a16139..2515e9e66a 100644 --- a/packages/backend/src/tenants/model.ts +++ b/packages/backend/src/tenants/model.ts @@ -22,3 +22,11 @@ export class Tenant extends BaseModel { } } } + +export type TenantWithIdpConfig = Pick< + Tenant, + 'id' | 'email' | 'apiSecret' | 'publicName' | 'createdAt' | 'updatedAt' +> & { + idpConsentUrl: string + idpSecret: string +} From d5cc3145ac24972bdecc4c688383303b98f9c36b Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Wed, 27 Nov 2024 15:26:45 -0800 Subject: [PATCH 15/66] feat: use soft delete --- packages/backend/src/tenants/service.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/backend/src/tenants/service.test.ts b/packages/backend/src/tenants/service.test.ts index 84fad049ca..11bc579eac 100644 --- a/packages/backend/src/tenants/service.test.ts +++ b/packages/backend/src/tenants/service.test.ts @@ -101,6 +101,17 @@ describe('Tenant Service', (): void => { const tenant = await tenantService.get(dbTenant.id) expect(tenant).toBeUndefined() }) + + test('returns undefined if tenant is deleted', async (): Promise => { + const dbTenant = await Tenant.query(knex).insertAndFetch({ + apiSecret: 'test-secret', + email: faker.internet.email(), + deletedAt: new Date() + }) + + const tenant = await tenantService.get(dbTenant.id) + expect(tenant).toBeUndefined() + }) }) describe('create', (): void => { From fc01ef7130193a6c4e17a4b13b1b8968bc7ecf4f Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Mon, 2 Dec 2024 16:19:53 -0800 Subject: [PATCH 16/66] feat: add idp columns to tenant model --- packages/backend/src/tenants/service.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/backend/src/tenants/service.test.ts b/packages/backend/src/tenants/service.test.ts index 11bc579eac..55c38c7c1a 100644 --- a/packages/backend/src/tenants/service.test.ts +++ b/packages/backend/src/tenants/service.test.ts @@ -106,6 +106,8 @@ describe('Tenant Service', (): void => { const dbTenant = await Tenant.query(knex).insertAndFetch({ apiSecret: 'test-secret', email: faker.internet.email(), + idpConsentUrl: faker.internet.url(), + idpSecret: 'test-idp-secret', deletedAt: new Date() }) From 87213de07c644f7d2d88a5cf037ed9060416f347 Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Tue, 3 Dec 2024 10:47:36 -0800 Subject: [PATCH 17/66] feat: pagination tests, push deletedAt to auth api call --- packages/backend/src/tenants/model.ts | 8 -------- packages/backend/src/tenants/service.ts | 13 +++++++++++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/tenants/model.ts b/packages/backend/src/tenants/model.ts index 2515e9e66a..78b7a16139 100644 --- a/packages/backend/src/tenants/model.ts +++ b/packages/backend/src/tenants/model.ts @@ -22,11 +22,3 @@ export class Tenant extends BaseModel { } } } - -export type TenantWithIdpConfig = Pick< - Tenant, - 'id' | 'email' | 'apiSecret' | 'publicName' | 'createdAt' | 'updatedAt' -> & { - idpConsentUrl: string - idpSecret: string -} diff --git a/packages/backend/src/tenants/service.ts b/packages/backend/src/tenants/service.ts index d1973471eb..1c4af98e6e 100644 --- a/packages/backend/src/tenants/service.ts +++ b/packages/backend/src/tenants/service.ts @@ -4,7 +4,10 @@ import { gql, NormalizedCacheObject } from '@apollo/client' import { ApolloClient } from '@apollo/client' import { TransactionOrKnex } from 'objection' import { Pagination, SortOrder } from '../shared/baseModel' +<<<<<<< HEAD import { CacheDataStore } from '../middleware/cache/data-stores' +======= +>>>>>>> 1ce5bae0 (feat: pagination tests, push deletedAt to auth api call) export interface TenantService { get: (id: string) => Promise @@ -42,6 +45,7 @@ async function getTenant( deps: ServiceDependencies, id: string ): Promise { +<<<<<<< HEAD const inMem = await deps.tenantCache.get(id) if (inMem) return inMem const tenant = await Tenant.query(deps.knex) @@ -52,6 +56,11 @@ async function getTenant( return tenant } +======= + return await Tenant.query(deps.knex).findById(id).whereNull('deletedAt') +} + +>>>>>>> 1ce5bae0 (feat: pagination tests, push deletedAt to auth api call) async function getTenantPage( deps: ServiceDependencies, pagination?: Pagination, @@ -182,7 +191,11 @@ async function deleteTenant( await deps.tenantCache.delete(id) try { +<<<<<<< HEAD const deletedAt = new Date() +======= + const deletedAt = new Date(Date.now()) +>>>>>>> 1ce5bae0 (feat: pagination tests, push deletedAt to auth api call) await Tenant.query(trx).patchAndFetchById(id, { deletedAt }) From 0a37b3cd164bac37823ffc24c807bbfa1e1b0ea1 Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Tue, 3 Dec 2024 14:35:59 -0800 Subject: [PATCH 18/66] feat: add cache --- packages/backend/src/tenants/service.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/backend/src/tenants/service.ts b/packages/backend/src/tenants/service.ts index 1c4af98e6e..d1973471eb 100644 --- a/packages/backend/src/tenants/service.ts +++ b/packages/backend/src/tenants/service.ts @@ -4,10 +4,7 @@ import { gql, NormalizedCacheObject } from '@apollo/client' import { ApolloClient } from '@apollo/client' import { TransactionOrKnex } from 'objection' import { Pagination, SortOrder } from '../shared/baseModel' -<<<<<<< HEAD import { CacheDataStore } from '../middleware/cache/data-stores' -======= ->>>>>>> 1ce5bae0 (feat: pagination tests, push deletedAt to auth api call) export interface TenantService { get: (id: string) => Promise @@ -45,7 +42,6 @@ async function getTenant( deps: ServiceDependencies, id: string ): Promise { -<<<<<<< HEAD const inMem = await deps.tenantCache.get(id) if (inMem) return inMem const tenant = await Tenant.query(deps.knex) @@ -56,11 +52,6 @@ async function getTenant( return tenant } -======= - return await Tenant.query(deps.knex).findById(id).whereNull('deletedAt') -} - ->>>>>>> 1ce5bae0 (feat: pagination tests, push deletedAt to auth api call) async function getTenantPage( deps: ServiceDependencies, pagination?: Pagination, @@ -191,11 +182,7 @@ async function deleteTenant( await deps.tenantCache.delete(id) try { -<<<<<<< HEAD const deletedAt = new Date() -======= - const deletedAt = new Date(Date.now()) ->>>>>>> 1ce5bae0 (feat: pagination tests, push deletedAt to auth api call) await Tenant.query(trx).patchAndFetchById(id, { deletedAt }) From 3805b100f395ac5d465fef75a982d9cee7f5b6ca Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Fri, 6 Dec 2024 10:30:13 -0800 Subject: [PATCH 19/66] feat(backend): tenant signature validation for admin api --- packages/backend/src/app.ts | 28 +++- packages/backend/src/shared/utils.test.ts | 143 ++++++++++++++++++- packages/backend/src/shared/utils.ts | 72 +++++++++- packages/backend/src/tenants/service.test.ts | 2 +- packages/backend/src/tests/tenant.ts | 3 +- 5 files changed, 232 insertions(+), 16 deletions(-) diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 3bbe8d2662..c2f73b2396 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -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, @@ -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 @@ -144,6 +145,19 @@ export type HttpSigContext = AppContext & { client: string } +type TenantedHttpSigHeaders = HttpSigHeaders & Record<'tenantId', string> + +type TenantedHttpSigRequest = Omit & { + headers: TenantedHttpSigHeaders +} + +export type TenantedHttpSigContext = HttpSigContext & { + headers: TenantedHttpSigHeaders + request: TenantedHttpSigRequest + tenant?: Tenant + isOperator: boolean +} + export type HttpSigWithAuthenticatedStatusContext = HttpSigContext & AuthenticatedStatusContext @@ -384,12 +398,14 @@ export class App { ) if (this.config.adminApiSecret) { - koa.use(async (ctx, next: Koa.Next): Promise => { - if (!verifyApiSignature(ctx, this.config)) { - ctx.throw(401, 'Unauthorized') + koa.use( + async (ctx: TenantedHttpSigContext, next: Koa.Next): Promise => { + if (!verifyTenantOrOperatorApiSignature(ctx, this.config)) { + ctx.throw(401, 'Unauthorized') + } + return next() } - return next() - }) + ) } koa.use( diff --git a/packages/backend/src/shared/utils.test.ts b/packages/backend/src/shared/utils.test.ts index b786ef8498..5540b4a178 100644 --- a/packages/backend/src/shared/utils.test.ts +++ b/packages/backend/src/shared/utils.test.ts @@ -1,13 +1,23 @@ +import crypto from 'crypto' import { IocContract } from '@adonisjs/fold' import { Redis } from 'ioredis' -import { isValidHttpUrl, poll, requestWithTimeout, sleep } from './utils' -import { AppServices, AppContext } from '../app' +import { faker } from '@faker-js/faker' +import { + isValidHttpUrl, + poll, + requestWithTimeout, + sleep, + verifyTenantOrOperatorApiSignature +} from './utils' +import { AppServices, AppContext, TenantedHttpSigContext } from '../app' import { TestContainer, createTestApp } from '../tests/app' import { initIocContainer } from '..' import { verifyApiSignature } from './utils' import { generateApiSignature } from '../tests/apiSignature' -import { Config } from '../config/app' +import { Config, IAppConfig } from '../config/app' import { createContext } from '../tests/context' +import { Tenant } from '../tenants/model' +import { truncateTables } from '../tests/tableManager' describe('utils', (): void => { describe('isValidHttpUrl', (): void => { @@ -258,4 +268,131 @@ describe('utils', (): void => { expect(verified).toBe(false) }) }) + + describe('tenant/operator admin api signatures', (): void => { + let deps: IocContract + let appContainer: TestContainer + let tenant: Tenant + let config: IAppConfig + let redis: Redis + + const operatorApiSecret = 'test-operator-secret' + + beforeAll(async (): Promise => { + deps = initIocContainer({ + ...Config, + adminApiSecret: operatorApiSecret + }) + appContainer = await createTestApp(deps) + config = await deps.use('config') + redis = await deps.use('redis') + }) + + beforeEach(async (): Promise => { + tenant = await Tenant.query(appContainer.knex).insertAndFetch({ + email: faker.internet.email(), + publicName: faker.company.name(), + apiSecret: crypto.randomBytes(8).toString('base64'), + idpConsentUrl: faker.internet.url(), + idpSecret: 'test-idp-secret' + }) + }) + + afterEach(async (): Promise => { + await redis.flushall() + await truncateTables(appContainer.knex) + }) + + afterAll(async (): Promise => { + await appContainer.shutdown() + }) + + test.each` + tenanted | isOperator | description + ${true} | ${false} | ${'tenanted non-operator'} + ${true} | ${true} | ${'tenanted operator'} + ${false} | ${true} | ${'non-tenanted operator'} + `( + 'returns true if $description request has valid signature', + async ({ tenanted, isOperator }): Promise => { + const requestBody = { test: 'value' } + + const signature = isOperator + ? generateApiSignature( + config.adminApiSecret as string, + Config.adminApiSignatureVersion, + requestBody + ) + : generateApiSignature( + tenant.apiSecret, + Config.adminApiSignatureVersion, + requestBody + ) + + const ctx = createContext( + { + headers: { + Accept: 'application/json', + signature, + tenantId: tenanted ? tenant.id : undefined + }, + url: '/graphql' + }, + {}, + appContainer.container + ) + ctx.request.body = requestBody + + const result = await verifyTenantOrOperatorApiSignature(ctx, config) + expect(result).toEqual(true) + + if (tenanted) { + expect(ctx.tenant).toEqual(tenant) + } else { + expect(ctx.tenant).toBeUndefined() + } + + if (isOperator) { + expect(ctx.isOperator).toEqual(true) + } else { + expect(ctx.isOperator).toEqual(false) + } + } + ) + + test.each` + failurePoint + ${'tenant'} + ${'operator'} + `( + "returns false when $failurePoint signature isn't valid", + async ({ failurePoint }): Promise => { + const tenantedRequest = failurePoint === 'tenant' + const requestBody = { test: 'value' } + const signature = generateApiSignature( + 'wrongsecret', + Config.adminApiSignatureVersion, + requestBody + ) + const ctx = createContext( + { + headers: { + Accept: 'application/json', + signature, + tenantId: tenantedRequest ? tenant.id : undefined + }, + url: '/graphql' + }, + {}, + appContainer.container + ) + ctx.request.body = requestBody + + const result = await verifyTenantOrOperatorApiSignature(ctx, config) + expect(result).toEqual(false) + expect(ctx.tenant).toBeUndefined() + expect(ctx.isOperator).toEqual(false) + } + ) + }) }) diff --git a/packages/backend/src/shared/utils.ts b/packages/backend/src/shared/utils.ts index 3f34098523..62132c5116 100644 --- a/packages/backend/src/shared/utils.ts +++ b/packages/backend/src/shared/utils.ts @@ -3,7 +3,7 @@ import { URL, type URL as URLType } from 'url' import { createHmac } from 'crypto' import { canonicalize } from 'json-canonicalize' import { IAppConfig } from '../config/app' -import { AppContext } from '../app' +import { AppContext, TenantedHttpSigContext } from '../app' export function validateId(id: string): boolean { return validate(id) && version(id) === 4 @@ -126,7 +126,8 @@ function getSignatureParts(signature: string) { function verifyApiSignatureDigest( signature: string, request: AppContext['request'], - config: IAppConfig + config: IAppConfig, + secret: string ): boolean { const { body } = request const { @@ -140,7 +141,7 @@ function verifyApiSignatureDigest( } const payload = `${timestamp}.${canonicalize(body)}` - const hmac = createHmac('sha256', config.adminApiSecret as string) + const hmac = createHmac('sha256', secret) hmac.update(payload) const digest = hmac.digest('hex') @@ -171,6 +172,64 @@ async function canApiSignatureBeProcessed( return true } +/* + Verifies http signatures by first attempting to replicate it with a secret + associated with a tenant id in the headers, then with the configured admin secret. + + If a tenant secret can replicate the signature, the request is tenanted to that particular tenant. + If the environment admin secret replicates the signature, then it is an operator request with elevated permissions. + If neither can replicate the signature then it is unauthorized. +*/ +export async function verifyTenantOrOperatorApiSignature( + ctx: TenantedHttpSigContext, + config: IAppConfig +): Promise { + ctx.tenant = undefined + ctx.isOperator = false + const { headers } = ctx.request + const signature = headers['signature'] + if (!signature) { + return false + } + + const tenantService = await ctx.container.use('tenantService') + const tenantId = headers['tenantid'] + const tenant = tenantId ? await tenantService.get(tenantId) : undefined + + if (!(await canApiSignatureBeProcessed(signature as string, ctx, config))) + return false + + // First, try validating with the tenant api secret + if ( + tenant?.apiSecret && + verifyApiSignatureDigest( + signature as string, + ctx.request, + config, + tenant.apiSecret + ) + ) { + ctx.tenant = tenant + return true + } + + // Fall back on validating with operator api secret if prior validation fails + if ( + verifyApiSignatureDigest( + signature as string, + ctx.request, + config, + config.adminApiSecret as string + ) + ) { + ctx.tenant = tenant + ctx.isOperator = true + return true + } + + return false +} + export async function verifyApiSignature( ctx: AppContext, config: IAppConfig @@ -184,5 +243,10 @@ export async function verifyApiSignature( if (!(await canApiSignatureBeProcessed(signature as string, ctx, config))) return false - return verifyApiSignatureDigest(signature as string, ctx.request, config) + return verifyApiSignatureDigest( + signature as string, + ctx.request, + config, + config.adminApiSecret as string + ) } diff --git a/packages/backend/src/tenants/service.test.ts b/packages/backend/src/tenants/service.test.ts index 55c38c7c1a..da6d3b7009 100644 --- a/packages/backend/src/tenants/service.test.ts +++ b/packages/backend/src/tenants/service.test.ts @@ -61,7 +61,7 @@ describe('Tenant Service', (): void => { await appContainer.shutdown() }) - describe('Tenant pangination', (): void => { + describe('Tenant pagination', (): void => { describe('getPage', (): void => { getPageTests({ createModel: () => createTenant(deps), diff --git a/packages/backend/src/tests/tenant.ts b/packages/backend/src/tests/tenant.ts index 4ac1488b84..f174a58f2f 100644 --- a/packages/backend/src/tests/tenant.ts +++ b/packages/backend/src/tests/tenant.ts @@ -1,3 +1,4 @@ +import nock from 'nock' import { IocContract } from '@adonisjs/fold' import { faker } from '@faker-js/faker' import { AppServices } from '../app' @@ -11,8 +12,6 @@ interface CreateOptions { idpSecret: string } -const nock = (global as unknown as { nock: typeof import('nock') }).nock - export async function createTenant( deps: IocContract, options?: CreateOptions From 92fc1ac1887f50ebbcd2703b189b7789ead925fe Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Mon, 9 Dec 2024 10:50:02 -0800 Subject: [PATCH 20/66] fix: rebase errors --- packages/auth/migrations/20241125233415_create_tenants_table.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/auth/migrations/20241125233415_create_tenants_table.js b/packages/auth/migrations/20241125233415_create_tenants_table.js index da05937be6..9112108977 100644 --- a/packages/auth/migrations/20241125233415_create_tenants_table.js +++ b/packages/auth/migrations/20241125233415_create_tenants_table.js @@ -8,6 +8,8 @@ exports.up = function (knex) { table.string('idpConsentUrl').notNullable() table.string('idpSecret').notNullable() + table.timestamp('createdAt').defaultTo(knex.fn.now()) + table.timestamp('updatedAt').defaultTo(knex.fn.now()) table.timestamp('deletedAt') }) } From 645e1816b96fc14612a629d258bc504b5da04a0b Mon Sep 17 00:00:00 2001 From: koekiebox Date: Tue, 10 Dec 2024 11:25:06 +0100 Subject: [PATCH 21/66] feat(3114): update seed.ts --- packages/mock-account-service-lib/src/seed.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mock-account-service-lib/src/seed.ts b/packages/mock-account-service-lib/src/seed.ts index 1c6d243100..8fef87c6f7 100644 --- a/packages/mock-account-service-lib/src/seed.ts +++ b/packages/mock-account-service-lib/src/seed.ts @@ -140,7 +140,7 @@ export async function setupFromSeed( logger.debug('hostname: ', config.publicHost) const url = `${config.publicHost}/${account.path}` - const tenantId = 'TODO' //TODO @jason complete this: <- + const tenantId = '438fa74a-fa7d-4317-9ced-dde32ece1787' //TODO @jason complete this: <- let walletAddress = await getWalletAddressByURL(url) if (!walletAddress) { walletAddress = await createWalletAddress( From cdc2bda2267b61aa3a08a1f4976d891150e862fc Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Tue, 10 Dec 2024 09:59:32 -0800 Subject: [PATCH 22/66] fix: remove admin api secret check from app --- packages/backend/src/app.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index c2f73b2396..93e47d352c 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -397,16 +397,14 @@ export class App { } ) - if (this.config.adminApiSecret) { - koa.use( - async (ctx: TenantedHttpSigContext, next: Koa.Next): Promise => { - if (!verifyTenantOrOperatorApiSignature(ctx, this.config)) { - ctx.throw(401, 'Unauthorized') - } - return next() + koa.use( + async (ctx: TenantedHttpSigContext, next: Koa.Next): Promise => { + if (!verifyTenantOrOperatorApiSignature(ctx, this.config)) { + ctx.throw(401, 'Unauthorized') } - ) - } + return next() + } + ) koa.use( koaMiddleware(this.apolloServer, { From c44cd034d97e072d422a63fe359f0aac9cdc4855 Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Tue, 10 Dec 2024 14:21:01 -0800 Subject: [PATCH 23/66] fix: always expect tenant id in request --- packages/backend/src/shared/utils.test.ts | 157 +++++++++++++++------- packages/backend/src/shared/utils.ts | 23 +--- 2 files changed, 118 insertions(+), 62 deletions(-) diff --git a/packages/backend/src/shared/utils.test.ts b/packages/backend/src/shared/utils.test.ts index 5540b4a178..92171efe8c 100644 --- a/packages/backend/src/shared/utils.test.ts +++ b/packages/backend/src/shared/utils.test.ts @@ -2,6 +2,7 @@ import crypto from 'crypto' import { IocContract } from '@adonisjs/fold' import { Redis } from 'ioredis' import { faker } from '@faker-js/faker' +import { v4 } from 'uuid' import { isValidHttpUrl, poll, @@ -273,10 +274,11 @@ describe('utils', (): void => { let deps: IocContract let appContainer: TestContainer let tenant: Tenant + let operator: Tenant let config: IAppConfig let redis: Redis - const operatorApiSecret = 'test-operator-secret' + const operatorApiSecret = crypto.randomBytes(8).toString('base64') beforeAll(async (): Promise => { deps = initIocContainer({ @@ -296,6 +298,14 @@ describe('utils', (): void => { idpConsentUrl: faker.internet.url(), idpSecret: 'test-idp-secret' }) + + operator = await Tenant.query(appContainer.knex).insertAndFetch({ + email: faker.internet.email(), + publicName: faker.company.name(), + apiSecret: operatorApiSecret, + idpConsentUrl: faker.internet.url(), + idpSecret: 'test-idp-secret' + }) }) afterEach(async (): Promise => { @@ -308,18 +318,17 @@ describe('utils', (): void => { }) test.each` - tenanted | isOperator | description - ${true} | ${false} | ${'tenanted non-operator'} - ${true} | ${true} | ${'tenanted operator'} - ${false} | ${true} | ${'non-tenanted operator'} + isOperator | description + ${false} | ${'tenanted non-operator'} + ${true} | ${'tenanted operator'} `( 'returns true if $description request has valid signature', - async ({ tenanted, isOperator }): Promise => { + async ({ isOperator }): Promise => { const requestBody = { test: 'value' } const signature = isOperator ? generateApiSignature( - config.adminApiSecret as string, + operator.apiSecret, Config.adminApiSignatureVersion, requestBody ) @@ -334,7 +343,7 @@ describe('utils', (): void => { headers: { Accept: 'application/json', signature, - tenantId: tenanted ? tenant.id : undefined + 'tenant-id': isOperator ? operator.id : tenant.id }, url: '/graphql' }, @@ -343,14 +352,25 @@ describe('utils', (): void => { ) ctx.request.body = requestBody + if (isOperator) { + console.log( + 'tenant secret=', + operator.apiSecret, + 'config secret=', + config.adminApiSecret + ) + } else { + console.log( + 'tenant secret=', + tenant.apiSecret, + 'config secret=', + config.adminApiSecret + ) + } const result = await verifyTenantOrOperatorApiSignature(ctx, config) expect(result).toEqual(true) - if (tenanted) { - expect(ctx.tenant).toEqual(tenant) - } else { - expect(ctx.tenant).toBeUndefined() - } + expect(ctx.tenant).toEqual(isOperator ? operator : tenant) if (isOperator) { expect(ctx.isOperator).toEqual(true) @@ -360,39 +380,86 @@ describe('utils', (): void => { } ) - test.each` - failurePoint - ${'tenant'} - ${'operator'} - `( - "returns false when $failurePoint signature isn't valid", - async ({ failurePoint }): Promise => { - const tenantedRequest = failurePoint === 'tenant' - const requestBody = { test: 'value' } - const signature = generateApiSignature( - 'wrongsecret', - Config.adminApiSignatureVersion, - requestBody - ) - const ctx = createContext( - { - headers: { - Accept: 'application/json', - signature, - tenantId: tenantedRequest ? tenant.id : undefined - }, - url: '/graphql' + test("returns false when signature isn't signed with tenant secret", async (): Promise => { + const requestBody = { test: 'value' } + const signature = generateApiSignature( + 'wrongsecret', + Config.adminApiSignatureVersion, + requestBody + ) + const ctx = createContext( + { + headers: { + Accept: 'application/json', + signature, + 'tenant-id': tenant.id }, - {}, - appContainer.container - ) - ctx.request.body = requestBody + url: '/graphql' + }, + {}, + appContainer.container + ) + ctx.request.body = requestBody - const result = await verifyTenantOrOperatorApiSignature(ctx, config) - expect(result).toEqual(false) - expect(ctx.tenant).toBeUndefined() - expect(ctx.isOperator).toEqual(false) - } - ) + const result = await verifyTenantOrOperatorApiSignature(ctx, config) + expect(result).toEqual(false) + expect(ctx.tenant).toBeUndefined() + expect(ctx.isOperator).toEqual(false) + }) + + test('returns false if tenant id is not included', async (): Promise => { + const requestBody = { test: 'value' } + const signature = generateApiSignature( + tenant.apiSecret, + Config.adminApiSignatureVersion, + requestBody + ) + const ctx = createContext( + { + headers: { + Accept: 'application/json', + signature + }, + url: '/graphql' + }, + {}, + appContainer.container + ) + + ctx.request.body = requestBody + + const result = await verifyTenantOrOperatorApiSignature(ctx, config) + expect(result).toEqual(false) + expect(ctx.tenant).toBeUndefined() + expect(ctx.isOperator).toEqual(false) + }) + + test('returns false if tenant does not exist', async (): Promise => { + const requestBody = { test: 'value' } + const signature = generateApiSignature( + tenant.apiSecret, + Config.adminApiSignatureVersion, + requestBody + ) + const ctx = createContext( + { + headers: { + Accept: 'application/json', + signature, + 'tenant-id': v4() + }, + url: '/graphql' + }, + {}, + appContainer.container + ) + + ctx.request.body = requestBody + + const result = await verifyTenantOrOperatorApiSignature(ctx, config) + expect(result).toEqual(false) + expect(ctx.tenant).toBeUndefined() + expect(ctx.isOperator).toEqual(false) + }) }) }) diff --git a/packages/backend/src/shared/utils.ts b/packages/backend/src/shared/utils.ts index 62132c5116..cc63458130 100644 --- a/packages/backend/src/shared/utils.ts +++ b/packages/backend/src/shared/utils.ts @@ -177,7 +177,7 @@ async function canApiSignatureBeProcessed( associated with a tenant id in the headers, then with the configured admin secret. If a tenant secret can replicate the signature, the request is tenanted to that particular tenant. - If the environment admin secret replicates the signature, then it is an operator request with elevated permissions. + If the environment admin secret matches the tenant's secret, then it is an operator request with elevated permissions. If neither can replicate the signature then it is unauthorized. */ export async function verifyTenantOrOperatorApiSignature( @@ -193,15 +193,17 @@ export async function verifyTenantOrOperatorApiSignature( } const tenantService = await ctx.container.use('tenantService') - const tenantId = headers['tenantid'] + const tenantId = headers['tenant-id'] const tenant = tenantId ? await tenantService.get(tenantId) : undefined + if (!tenant) return false + if (!(await canApiSignatureBeProcessed(signature as string, ctx, config))) return false // First, try validating with the tenant api secret if ( - tenant?.apiSecret && + tenant.apiSecret && verifyApiSignatureDigest( signature as string, ctx.request, @@ -210,20 +212,7 @@ export async function verifyTenantOrOperatorApiSignature( ) ) { ctx.tenant = tenant - return true - } - - // Fall back on validating with operator api secret if prior validation fails - if ( - verifyApiSignatureDigest( - signature as string, - ctx.request, - config, - config.adminApiSecret as string - ) - ) { - ctx.tenant = tenant - ctx.isOperator = true + ctx.isOperator = tenant.apiSecret === config.adminApiSecret return true } From 4e17260447dfb6f486bb904aa2b36a03cf0e404d Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Tue, 10 Dec 2024 15:15:51 -0800 Subject: [PATCH 24/66] chore: remove some logs --- packages/backend/src/shared/utils.test.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/backend/src/shared/utils.test.ts b/packages/backend/src/shared/utils.test.ts index 92171efe8c..fc9077feef 100644 --- a/packages/backend/src/shared/utils.test.ts +++ b/packages/backend/src/shared/utils.test.ts @@ -352,21 +352,6 @@ describe('utils', (): void => { ) ctx.request.body = requestBody - if (isOperator) { - console.log( - 'tenant secret=', - operator.apiSecret, - 'config secret=', - config.adminApiSecret - ) - } else { - console.log( - 'tenant secret=', - tenant.apiSecret, - 'config secret=', - config.adminApiSecret - ) - } const result = await verifyTenantOrOperatorApiSignature(ctx, config) expect(result).toEqual(true) From 7d562b5d456f4c7ec7a9e92a936e8e66406d75da Mon Sep 17 00:00:00 2001 From: koekiebox Date: Wed, 11 Dec 2024 13:49:01 +0100 Subject: [PATCH 25/66] feat(3114): update for auth and resource server. --- .../app/lib/parse_config.server.ts | 4 +++- .../20241203112902_add_tenant_to_wallet_address.js | 5 ++--- .../src/open_payments/wallet_address/routes.test.ts | 12 ++++++++---- .../src/open_payments/wallet_address/routes.ts | 9 +++++++-- packages/mock-account-service-lib/src/seed.ts | 2 +- packages/mock-account-service-lib/src/types.ts | 1 + 6 files changed, 22 insertions(+), 11 deletions(-) diff --git a/localenv/mock-account-servicing-entity/app/lib/parse_config.server.ts b/localenv/mock-account-servicing-entity/app/lib/parse_config.server.ts index e4a934def3..b2897e9a9f 100644 --- a/localenv/mock-account-servicing-entity/app/lib/parse_config.server.ts +++ b/localenv/mock-account-servicing-entity/app/lib/parse_config.server.ts @@ -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' } diff --git a/packages/backend/migrations/20241203112902_add_tenant_to_wallet_address.js b/packages/backend/migrations/20241203112902_add_tenant_to_wallet_address.js index c028ec3be6..9415acdfa4 100644 --- a/packages/backend/migrations/20241203112902_add_tenant_to_wallet_address.js +++ b/packages/backend/migrations/20241203112902_add_tenant_to_wallet_address.js @@ -5,9 +5,8 @@ exports.up = function (knex) { return Promise.all([ knex.schema.alterTable('walletAddresses', function (table) { - table.uuid('tenantId') - table.foreign(['tenantId']).references('tenants.id') - table.index(['tenantId']) + table.uuid('tenantId').index().notNullable() + //table.foreign(['tenantId']).references('tenants.id') }) ]) } diff --git a/packages/backend/src/open_payments/wallet_address/routes.test.ts b/packages/backend/src/open_payments/wallet_address/routes.test.ts index ee4777f43b..87610952cc 100644 --- a/packages/backend/src/open_payments/wallet_address/routes.test.ts +++ b/packages/backend/src/open_payments/wallet_address/routes.test.ts @@ -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] }) @@ -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 } @@ -145,6 +147,7 @@ describe('Wallet Address Routes', (): void => { test('returns wallet address', async (): Promise => { const walletAddress = await createWalletAddress(deps, { + tenantId: config.operatorTenantId, publicName: faker.person.firstName() }) @@ -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}`, }) }) }) diff --git a/packages/backend/src/open_payments/wallet_address/routes.ts b/packages/backend/src/open_payments/wallet_address/routes.ts index 652394985e..c7094d87a4 100644 --- a/packages/backend/src/open_payments/wallet_address/routes.ts +++ b/packages/backend/src/open_payments/wallet_address/routes.ts @@ -60,11 +60,16 @@ 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}` }) } +function ensureTrailingSlash(str: string): string { + if (!str.endsWith('/')) return `${str}/` + return str +} + interface ListSubresourceOptions { ctx: ListContext getWalletAddressPage: WalletAddressSubresourceService['getWalletAddressPage'] diff --git a/packages/mock-account-service-lib/src/seed.ts b/packages/mock-account-service-lib/src/seed.ts index 8fef87c6f7..7694992f40 100644 --- a/packages/mock-account-service-lib/src/seed.ts +++ b/packages/mock-account-service-lib/src/seed.ts @@ -140,7 +140,7 @@ export async function setupFromSeed( logger.debug('hostname: ', config.publicHost) const url = `${config.publicHost}/${account.path}` - const tenantId = '438fa74a-fa7d-4317-9ced-dde32ece1787' //TODO @jason complete this: <- + const tenantId = config.operatorTenantId let walletAddress = await getWalletAddressByURL(url) if (!walletAddress) { walletAddress = await createWalletAddress( diff --git a/packages/mock-account-service-lib/src/types.ts b/packages/mock-account-service-lib/src/types.ts index 76c90bfda8..f48dad9b63 100644 --- a/packages/mock-account-service-lib/src/types.ts +++ b/packages/mock-account-service-lib/src/types.ts @@ -49,6 +49,7 @@ export interface Config { authServerDomain: string graphqlUrl: string idpSecret: string + operatorTenantId: string } export interface Webhook { id: string From b5a1667398c34f899b530214398acec9724f3aa6 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Wed, 11 Dec 2024 14:58:20 +0100 Subject: [PATCH 26/66] feat(3114): fix asset service.test.ts --- packages/backend/src/asset/service.test.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/backend/src/asset/service.test.ts b/packages/backend/src/asset/service.test.ts index ccff2233e8..64b4c79a5e 100644 --- a/packages/backend/src/asset/service.test.ts +++ b/packages/backend/src/asset/service.test.ts @@ -21,13 +21,11 @@ import { isWalletAddressError } from '../open_payments/wallet_address/errors' import { PeerService } from '../payment-method/ilp/peer/service' import { isPeerError } from '../payment-method/ilp/peer/errors' import { CacheDataStore } from '../middleware/cache/data-stores' -import { TenantService } from '../tenants/service' describe('Asset Service', (): void => { let deps: IocContract let appContainer: TestContainer let assetService: AssetService - let tenantService: TenantService let peerService: PeerService let walletAddressService: WalletAddressService @@ -274,17 +272,10 @@ describe('Asset Service', (): void => { assert.ok(!isAssetError(newAsset)) const newAssetId = newAsset.id - const newTenant = await tenantService.create({ - email: 'test@tenent.za', - apiSecret: 'secret', - idpSecret: 'idpSecret', - idpConsentUrl: 'idpConsentUrl' - }) - // make sure there is at least 1 wallet address using asset const walletAddress = walletAddressService.create({ url: 'https://alice.me/.well-known/pay', - tenantId: newTenant.id, + tenantId: Config.operatorTenantId, assetId: newAssetId }) assert.ok(!isWalletAddressError(walletAddress)) From f29862af430a0ae0ee43b9cb8e3de82c8358f2b3 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Wed, 11 Dec 2024 17:40:00 +0100 Subject: [PATCH 27/66] feat(3114): fix tests. --- .../open_payments/payment/incoming/service.test.ts | 5 ++++- .../open_payments/payment/outgoing/service.test.ts | 12 ++++++++++-- .../backend/src/open_payments/quote/service.test.ts | 6 ++++++ .../src/open_payments/wallet_address/routes.test.ts | 2 +- .../src/open_payments/wallet_address/routes.ts | 6 +----- packages/backend/src/shared/utils.ts | 5 +++++ packages/backend/src/tests/quote.ts | 1 + 7 files changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/open_payments/payment/incoming/service.test.ts b/packages/backend/src/open_payments/payment/incoming/service.test.ts index 6826f46b00..6fa47cef0d 100644 --- a/packages/backend/src/open_payments/payment/incoming/service.test.ts +++ b/packages/backend/src/open_payments/payment/incoming/service.test.ts @@ -53,7 +53,10 @@ describe('Incoming Payment Service', (): void => { beforeEach(async (): Promise => { 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 }) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.test.ts b/packages/backend/src/open_payments/payment/outgoing/service.test.ts index b3e98b502c..622fc53ad3 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.test.ts @@ -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 }) @@ -408,8 +410,12 @@ describe('OutgoingPaymentService', (): void => { let outgoingPayment: OutgoingPayment let otherOutgoingPayment: OutgoingPayment beforeEach(async (): Promise => { - 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, { @@ -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() }) diff --git a/packages/backend/src/open_payments/quote/service.test.ts b/packages/backend/src/open_payments/quote/service.test.ts index 3840212c2e..0de56023aa 100644 --- a/packages/backend/src/open_payments/quote/service.test.ts +++ b/packages/backend/src/open_payments/quote/service.test.ts @@ -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 }) @@ -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 }) }) @@ -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 }) }) diff --git a/packages/backend/src/open_payments/wallet_address/routes.test.ts b/packages/backend/src/open_payments/wallet_address/routes.test.ts index 87610952cc..0962da639b 100644 --- a/packages/backend/src/open_payments/wallet_address/routes.test.ts +++ b/packages/backend/src/open_payments/wallet_address/routes.test.ts @@ -165,7 +165,7 @@ describe('Wallet Address Routes', (): void => { assetScale: walletAddress.asset.scale, // Ensure the tenant id is returned for auth and resource server: authServer: `${config.authServerGrantUrl}/${config.operatorTenantId}`, - resourceServer: `${config.openPaymentsUrl}/${config.operatorTenantId}`, + resourceServer: `${config.openPaymentsUrl}/${config.operatorTenantId}` }) }) }) diff --git a/packages/backend/src/open_payments/wallet_address/routes.ts b/packages/backend/src/open_payments/wallet_address/routes.ts index c7094d87a4..339ef3c789 100644 --- a/packages/backend/src/open_payments/wallet_address/routes.ts +++ b/packages/backend/src/open_payments/wallet_address/routes.ts @@ -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 @@ -65,11 +66,6 @@ export async function getWalletAddress( }) } -function ensureTrailingSlash(str: string): string { - if (!str.endsWith('/')) return `${str}/` - return str -} - interface ListSubresourceOptions { ctx: ListContext getWalletAddressPage: WalletAddressSubresourceService['getWalletAddressPage'] diff --git a/packages/backend/src/shared/utils.ts b/packages/backend/src/shared/utils.ts index 3f34098523..4b466261cd 100644 --- a/packages/backend/src/shared/utils.ts +++ b/packages/backend/src/shared/utils.ts @@ -186,3 +186,8 @@ export async function verifyApiSignature( return verifyApiSignatureDigest(signature as string, ctx.request, config) } + +export function ensureTrailingSlash(str: string): string { + if (!str.endsWith('/')) return `${str}/` + return str +} diff --git a/packages/backend/src/tests/quote.ts b/packages/backend/src/tests/quote.ts index 26c0928619..714a55e3c8 100644 --- a/packages/backend/src/tests/quote.ts +++ b/packages/backend/src/tests/quote.ts @@ -168,6 +168,7 @@ export async function createQuote( const withGraphFetchedArray = [ 'asset', 'walletAddress', + 'walletAddress.tenant', 'walletAddress.asset' ] if (withFee) { From 5477b5ae14658e525f72a5f869e5f7565ebfe158 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Mon, 6 Jan 2025 18:28:48 +0100 Subject: [PATCH 28/66] feat(3114): merged with latest tenant changes. --- packages/backend/src/shared/utils.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/backend/src/shared/utils.ts b/packages/backend/src/shared/utils.ts index abe2c1917d..fb10ffec98 100644 --- a/packages/backend/src/shared/utils.ts +++ b/packages/backend/src/shared/utils.ts @@ -240,3 +240,8 @@ export async function verifyApiSignature( config.adminApiSecret as string ) } + +export function ensureTrailingSlash(str: string): string { + if (!str.endsWith('/')) return `${str}/` + return str +} From 47d08f157acbaa18b3963a9f11ea6e474d98a39c Mon Sep 17 00:00:00 2001 From: koekiebox Date: Tue, 7 Jan 2025 17:06:01 +0100 Subject: [PATCH 29/66] feat(3114): extract tenant id from tenant context instead of admin input variable. --- .../generated/graphql.ts | 2 -- packages/backend/src/app.ts | 4 ++++ .../src/graphql/generated/graphql.schema.json | 16 ---------------- .../backend/src/graphql/generated/graphql.ts | 2 -- .../src/graphql/resolvers/wallet_address.test.ts | 6 ------ .../src/graphql/resolvers/wallet_address.ts | 6 +++--- packages/backend/src/graphql/schema.graphql | 2 -- packages/backend/src/shared/utils.ts | 12 ++++-------- packages/frontend/app/generated/graphql.ts | 2 -- .../src/generated/graphql.ts | 2 -- .../mock-account-service-lib/src/requesters.ts | 6 +----- packages/mock-account-service-lib/src/seed.ts | 2 -- test/integration/lib/generated/graphql.ts | 2 -- 13 files changed, 12 insertions(+), 52 deletions(-) diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 8ed0c6a6d9..46dad2dedb 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -373,8 +373,6 @@ export type CreateWalletAddressInput = { idempotencyKey?: InputMaybe; /** Public name associated with the wallet address. This is visible to anyone with the wallet address URL. */ publicName?: InputMaybe; - /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. */ - tenantId: Scalars['String']['input']; /** Wallet address URL. This cannot be changed. */ url: Scalars['String']['input']; }; diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 03fbbcf1b6..e152109c93 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -406,6 +406,10 @@ export class App { } return next() }) + } else { + const tenantService = await this.container.use('tenantService') + const tenant = await tenantService.get(this.config.operatorTenantId) + tenantApiSignatureResult = { tenant, isOperator: true } } koa.use( diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index 7e56986212..1745966aa2 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -2171,22 +2171,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "tenantId", - "description": "Unique identifier of the tenant associated with the wallet address. This cannot be changed.", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "url", "description": "Wallet address URL. This cannot be changed.", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 8ed0c6a6d9..46dad2dedb 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -373,8 +373,6 @@ export type CreateWalletAddressInput = { idempotencyKey?: InputMaybe; /** Public name associated with the wallet address. This is visible to anyone with the wallet address URL. */ publicName?: InputMaybe; - /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. */ - tenantId: Scalars['String']['input']; /** Wallet address URL. This cannot be changed. */ url: Scalars['String']['input']; }; diff --git a/packages/backend/src/graphql/resolvers/wallet_address.test.ts b/packages/backend/src/graphql/resolvers/wallet_address.test.ts index 3c26433fa9..8f559d8a40 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.test.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.test.ts @@ -7,7 +7,6 @@ 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' @@ -36,8 +35,6 @@ 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 @@ -66,14 +63,11 @@ describe('Wallet Address Resolvers', (): void => { describe('Create Wallet Address', (): void => { let asset: Asset - let tenant: Tenant let input: CreateWalletAddressInput beforeEach(async (): Promise => { asset = await createAsset(deps) - tenant = await createTenant(deps) input = { - tenantId: tenant.id, assetId: asset.id, url: 'https://alice.me/.well-known/pay' } diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index 2fdcb25439..cea6f15486 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -8,7 +8,7 @@ import { MutationResolvers, WalletAddressStatus } from '../generated/graphql' -import { ApolloContext } from '../../app' +import { ApolloContext, TenantedApolloContext } from '../../app' import { WalletAddressError, isWalletAddressError, @@ -80,7 +80,7 @@ export const getWalletAddressByUrl: QueryResolvers['walletAddress return walletAddress ? walletAddressToGraphql(walletAddress) : null } -export const createWalletAddress: MutationResolvers['createWalletAddress'] = +export const createWalletAddress: MutationResolvers['createWalletAddress'] = async ( parent, args, @@ -99,7 +99,7 @@ export const createWalletAddress: MutationResolvers['createWallet const options: CreateOptions = { assetId: args.input.assetId, - tenantId: args.input.tenantId, + tenantId: ctx.tenant.id, additionalProperties: addProps, publicName: args.input.publicName, url: args.input.url diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 059ca5669c..f8286e14d4 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -1226,8 +1226,6 @@ type CreateReceiverResponse { } input CreateWalletAddressInput { - "Unique identifier of the tenant associated with the wallet address. This cannot be changed." - tenantId: String! "Unique identifier of the asset associated with the wallet address. This cannot be changed." assetId: String! "Wallet address URL. This cannot be changed." diff --git a/packages/backend/src/shared/utils.ts b/packages/backend/src/shared/utils.ts index fb10ffec98..cda429e451 100644 --- a/packages/backend/src/shared/utils.ts +++ b/packages/backend/src/shared/utils.ts @@ -98,7 +98,7 @@ export async function poll(args: PollArgs): Promise { } /** - * Omit distrubuted to all types in a union. + * Omit distributed to all types in a union. * @example * type WithoutA = UnionOmit<{ a: number; c: number } | { b: number }, 'a'> // { c: number } | { b: number } * const withoutAOK: WithoutA = { c: 1 } // OK @@ -114,14 +114,10 @@ function getSignatureParts(signature: string) { const signatureParts = signature.split(', ') const timestamp = signatureParts[0].split('=')[1] const signatureVersionAndDigest = signatureParts[1].split('=') - const signatureVersion = signatureVersionAndDigest[0].replace('v', '') - const signatureDigest = signatureVersionAndDigest[1] + const version = signatureVersionAndDigest[0].replace('v', '') + const digest = signatureVersionAndDigest[1] - return { - timestamp, - version: signatureVersion, - digest: signatureDigest - } + return { timestamp, version, digest } } function verifyApiSignatureDigest( diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index e0602eccc5..1116595860 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -373,8 +373,6 @@ export type CreateWalletAddressInput = { idempotencyKey?: InputMaybe; /** Public name associated with the wallet address. This is visible to anyone with the wallet address URL. */ publicName?: InputMaybe; - /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. */ - tenantId: Scalars['String']['input']; /** Wallet address URL. This cannot be changed. */ url: Scalars['String']['input']; }; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 8ed0c6a6d9..46dad2dedb 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -373,8 +373,6 @@ export type CreateWalletAddressInput = { idempotencyKey?: InputMaybe; /** Public name associated with the wallet address. This is visible to anyone with the wallet address URL. */ publicName?: InputMaybe; - /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. */ - tenantId: Scalars['String']['input']; /** Wallet address URL. This cannot be changed. */ url: Scalars['String']['input']; }; diff --git a/packages/mock-account-service-lib/src/requesters.ts b/packages/mock-account-service-lib/src/requesters.ts index b95c763981..2f79324190 100644 --- a/packages/mock-account-service-lib/src/requesters.ts +++ b/packages/mock-account-service-lib/src/requesters.ts @@ -52,7 +52,6 @@ export function createRequesters( createWalletAddress: ( accountName: string, accountUrl: string, - tenantId: string, assetId: string ) => Promise createWalletAddressKey: ({ @@ -102,13 +101,12 @@ export function createRequesters( depositPeerLiquidity(apolloClient, logger, peerId, amount, transferUid), depositAssetLiquidity: (assetId, amount, transferId) => depositAssetLiquidity(apolloClient, logger, assetId, amount, transferId), - createWalletAddress: (accountName, accountUrl, tenantId, assetId) => + createWalletAddress: (accountName, accountUrl, assetId) => createWalletAddress( apolloClient, logger, accountName, accountUrl, - tenantId, assetId ), createWalletAddressKey: ({ walletAddressId, jwk }) => @@ -328,7 +326,6 @@ export async function createWalletAddress( logger: Logger, accountName: string, accountUrl: string, - tenantId: string, assetId: string ): Promise { const createWalletAddressMutation = gql` @@ -343,7 +340,6 @@ export async function createWalletAddress( } ` const createWalletAddressInput: CreateWalletAddressInput = { - tenantId, assetId, url: accountUrl, publicName: accountName, diff --git a/packages/mock-account-service-lib/src/seed.ts b/packages/mock-account-service-lib/src/seed.ts index 7694992f40..6ccc50681a 100644 --- a/packages/mock-account-service-lib/src/seed.ts +++ b/packages/mock-account-service-lib/src/seed.ts @@ -140,13 +140,11 @@ export async function setupFromSeed( logger.debug('hostname: ', config.publicHost) const url = `${config.publicHost}/${account.path}` - const tenantId = config.operatorTenantId let walletAddress = await getWalletAddressByURL(url) if (!walletAddress) { walletAddress = await createWalletAddress( account.name, url, - tenantId, accountAsset.id ) } diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 8ed0c6a6d9..46dad2dedb 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -373,8 +373,6 @@ export type CreateWalletAddressInput = { idempotencyKey?: InputMaybe; /** Public name associated with the wallet address. This is visible to anyone with the wallet address URL. */ publicName?: InputMaybe; - /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. */ - tenantId: Scalars['String']['input']; /** Wallet address URL. This cannot be changed. */ url: Scalars['String']['input']; }; From ab17c20e66144878ec0c91b09f0b3e85f00a7f4b Mon Sep 17 00:00:00 2001 From: koekiebox Date: Tue, 7 Jan 2025 21:54:48 +0100 Subject: [PATCH 30/66] feat(3114): test case updates, obtain tenant from header. --- packages/backend/src/app.ts | 10 ++-- .../resolvers/combined_payments.test.ts | 4 ++ .../resolvers/incoming_payment.test.ts | 11 ++++- .../src/graphql/resolvers/liquidity.test.ts | 9 +++- .../resolvers/outgoing_payment.test.ts | 10 ++++ .../src/graphql/resolvers/quote.test.ts | 3 ++ .../resolvers/walletAddressKey.test.ts | 16 +++++-- .../graphql/resolvers/wallet_address.test.ts | 16 +++++-- .../src/graphql/resolvers/wallet_address.ts | 19 ++++---- .../src/open_payments/auth/middleware.test.ts | 4 +- .../payment/combined/service.test.ts | 5 +- .../payment/incoming/model.test.ts | 4 +- .../payment/incoming/routes.test.ts | 5 +- .../payment/outgoing/routes.test.ts | 5 +- .../src/open_payments/quote/routes.test.ts | 1 + .../src/open_payments/quote/service.test.ts | 4 +- .../src/open_payments/receiver/model.test.ts | 16 +++++-- .../open_payments/receiver/service.test.ts | 2 + .../wallet_address/key/routes.test.ts | 12 +++-- .../wallet_address/key/service.test.ts | 4 +- .../wallet_address/middleware.test.ts | 8 +++- .../wallet_address/model.test.ts | 4 +- .../wallet_address/routes.test.ts | 1 + .../wallet_address/service.test.ts | 48 +++++++++++++++---- .../open_payments/wallet_address/service.ts | 6 ++- .../payment-method/handler/service.test.ts | 4 ++ .../src/payment-method/ilp/service.test.ts | 2 + .../ilp/spsp/middleware.test.ts | 1 + .../ilp/stream-credentials/service.test.ts | 4 +- .../src/payment-method/local/service.test.ts | 3 ++ .../backend/src/shared/pagination.test.ts | 3 ++ packages/backend/src/webhook/service.test.ts | 8 +++- 32 files changed, 198 insertions(+), 54 deletions(-) diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index e152109c93..b26f5e861c 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -393,7 +393,11 @@ export class App { ) let tenantApiSignatureResult: TenantApiSignatureResult - if (this.config.env !== 'test') { + if (this.config.env === 'test') { + const tenantService = await this.container.use('tenantService') + const tenant = await tenantService.get(this.config.operatorTenantId) + tenantApiSignatureResult = { tenant, isOperator: true } + } else { koa.use(async (ctx, next: Koa.Next): Promise => { const result = await getTenantFromApiSignature(ctx, this.config) if (!result) { @@ -406,10 +410,6 @@ export class App { } return next() }) - } else { - const tenantService = await this.container.use('tenantService') - const tenant = await tenantService.get(this.config.operatorTenantId) - tenantApiSignatureResult = { tenant, isOperator: true } } koa.use( diff --git a/packages/backend/src/graphql/resolvers/combined_payments.test.ts b/packages/backend/src/graphql/resolvers/combined_payments.test.ts index 50af7a5d4c..0f2550a1b9 100644 --- a/packages/backend/src/graphql/resolvers/combined_payments.test.ts +++ b/packages/backend/src/graphql/resolvers/combined_payments.test.ts @@ -50,6 +50,7 @@ describe('Payment', (): void => { test('Can get payments', async (): Promise => { const { id: outWalletAddressId } = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) @@ -68,6 +69,7 @@ describe('Payment', (): void => { }) const { id: inWalletAddressId } = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) const incomingPayment = await createIncomingPayment(deps, { @@ -146,6 +148,7 @@ describe('Payment', (): void => { test('Can filter payments by type and wallet address', async (): Promise => { const { id: outWalletAddressId } = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) @@ -168,6 +171,7 @@ describe('Payment', (): void => { }) const { id: outWalletAddressId2 } = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) await createOutgoingPayment(deps, { diff --git a/packages/backend/src/graphql/resolvers/incoming_payment.test.ts b/packages/backend/src/graphql/resolvers/incoming_payment.test.ts index 0db5f7cf66..56c0c74ddd 100644 --- a/packages/backend/src/graphql/resolvers/incoming_payment.test.ts +++ b/packages/backend/src/graphql/resolvers/incoming_payment.test.ts @@ -55,8 +55,12 @@ describe('Incoming Payment Resolver', (): void => { describe('Wallet address incoming payments', (): void => { beforeEach(async (): Promise => { - walletAddressId = (await createWalletAddress(deps, { assetId: asset.id })) - .id + walletAddressId = ( + await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, + assetId: asset.id + }) + ).id }) getPageTests({ @@ -106,6 +110,7 @@ describe('Incoming Payment Resolver', (): void => { async ({ metadata, expiresAt, withAmount }): Promise => { const incomingAmount = withAmount ? amount : undefined const { id: walletAddressId } = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) const payment = await createIncomingPayment(deps, { @@ -305,6 +310,7 @@ describe('Incoming Payment Resolver', (): void => { } beforeEach(async (): Promise => { const { id: walletAddressId } = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) payment = await createPayment({ walletAddressId, metadata }) @@ -462,6 +468,7 @@ describe('Incoming Payment Resolver', (): void => { async ({ metadata }): Promise => { const incomingAmount = amount ? amount : undefined const { id: walletAddressId } = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) const payment = await createIncomingPayment(deps, { diff --git a/packages/backend/src/graphql/resolvers/liquidity.test.ts b/packages/backend/src/graphql/resolvers/liquidity.test.ts index e0968b27e1..84df10f1fc 100644 --- a/packages/backend/src/graphql/resolvers/liquidity.test.ts +++ b/packages/backend/src/graphql/resolvers/liquidity.test.ts @@ -1015,6 +1015,7 @@ describe('Liquidity Resolvers', (): void => { beforeEach(async (): Promise => { walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, createLiquidityAccount: true }) @@ -1747,7 +1748,9 @@ describe('Liquidity Resolvers', (): void => { let payment: OutgoingPayment beforeEach(async (): Promise => { - walletAddress = await createWalletAddress(deps) + walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) const walletAddressId = walletAddress.id incomingPayment = await createIncomingPayment(deps, { walletAddressId, @@ -2157,7 +2160,9 @@ describe('Liquidity Resolvers', (): void => { let outgoingPayment: OutgoingPayment beforeEach(async (): Promise => { - walletAddress = await createWalletAddress(deps) + walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) const walletAddressId = walletAddress.id incomingPayment = await createIncomingPayment(deps, { walletAddressId, diff --git a/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts b/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts index dd41c4eb89..00b4e0aa60 100644 --- a/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts +++ b/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts @@ -99,6 +99,7 @@ describe('OutgoingPayment Resolvers', (): void => { beforeEach(async (): Promise => { walletAddressId = ( await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) ).id @@ -135,13 +136,16 @@ describe('OutgoingPayment Resolvers', (): void => { beforeEach(async (): Promise => { const firstReceiverWalletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) const secondWalletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) const secondReceiverWalletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) @@ -326,6 +330,7 @@ describe('OutgoingPayment Resolvers', (): void => { const grantId = uuid() const { id: walletAddressId } = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) @@ -365,6 +370,7 @@ describe('OutgoingPayment Resolvers', (): void => { beforeEach(async (): Promise => { const { id: walletAddressId } = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) payment = await createPayment({ walletAddressId, metadata }) @@ -553,6 +559,7 @@ describe('OutgoingPayment Resolvers', (): void => { test('success (metadata)', async (): Promise => { const { id: walletAddressId } = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) const payment = await createPayment({ walletAddressId, metadata }) @@ -689,6 +696,7 @@ describe('OutgoingPayment Resolvers', (): void => { test('create', async (): Promise => { const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) const payment = await createPayment({ walletAddressId: walletAddress.id }) @@ -840,6 +848,7 @@ describe('OutgoingPayment Resolvers', (): void => { let payment: OutgoingPaymentModel beforeEach(async () => { const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) @@ -956,6 +965,7 @@ describe('OutgoingPayment Resolvers', (): void => { beforeEach(async (): Promise => { walletAddressId = ( await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) ).id diff --git a/packages/backend/src/graphql/resolvers/quote.test.ts b/packages/backend/src/graphql/resolvers/quote.test.ts index dcb703edc6..5132b5292b 100644 --- a/packages/backend/src/graphql/resolvers/quote.test.ts +++ b/packages/backend/src/graphql/resolvers/quote.test.ts @@ -71,6 +71,7 @@ describe('Quote Resolvers', (): void => { describe('Query.quote', (): void => { test('success', async (): Promise => { const { id: walletAddressId } = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) const quote = await createWalletAddressQuote(walletAddressId) @@ -189,6 +190,7 @@ describe('Quote Resolvers', (): void => { `('$type', async ({ withAmount, receiveAmount }): Promise => { const amount = withAmount ? debitAmount : undefined const { id: walletAddressId } = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) const input = { @@ -300,6 +302,7 @@ describe('Quote Resolvers', (): void => { beforeEach(async (): Promise => { walletAddressId = ( await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) ).id diff --git a/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts b/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts index 660ffbd5ba..9c51f9b594 100644 --- a/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts +++ b/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts @@ -45,7 +45,9 @@ describe('Wallet Address Key Resolvers', (): void => { describe('Create Wallet Address Keys', (): void => { test('Can create wallet address key', async (): Promise => { - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) const input: CreateWalletAddressKeyInput = { walletAddressId: walletAddress.id, @@ -106,7 +108,9 @@ describe('Wallet Address Key Resolvers', (): void => { throw new Error('unexpected') }) - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) const input = { walletAddressId: walletAddress.id, @@ -164,7 +168,9 @@ describe('Wallet Address Key Resolvers', (): void => { describe('Revoke key', (): void => { test('Can revoke a key', async (): Promise => { - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) const key = await walletAddressKeyService.create({ walletAddressId: walletAddress.id, @@ -267,7 +273,9 @@ describe('Wallet Address Key Resolvers', (): void => { describe('List Wallet Address Keys', (): void => { let walletAddressId: string beforeEach(async (): Promise => { - walletAddressId = (await createWalletAddress(deps)).id + walletAddressId = ( + await createWalletAddress(deps, { tenantId: Config.operatorTenantId }) + ).id }) getPageTests({ getClient: () => appContainer.apolloClient, diff --git a/packages/backend/src/graphql/resolvers/wallet_address.test.ts b/packages/backend/src/graphql/resolvers/wallet_address.test.ts index 8f559d8a40..2e2cf21bff 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.test.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.test.ts @@ -312,7 +312,9 @@ describe('Wallet Address Resolvers', (): void => { let walletAddress: WalletAddressModel beforeEach(async (): Promise => { - walletAddress = await createWalletAddress(deps) + walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) }) test('Can update a wallet address', async (): Promise => { @@ -426,6 +428,7 @@ describe('Wallet Address Resolvers', (): void => { }) test('New additional properties override previous additional properties', async (): Promise => { const createOptions = { + tenantId: Config.operatorTenantId, additionalProperties: [ { fieldKey: 'existingKey', @@ -492,6 +495,7 @@ describe('Wallet Address Resolvers', (): void => { }) test('Updating with empty additional properties deletes existing', async (): Promise => { const createOptions = { + tenantId: Config.operatorTenantId, additionalProperties: [ { fieldKey: 'existingKey', @@ -655,6 +659,7 @@ describe('Wallet Address Resolvers', (): void => { const additionalProperties = [walletProp01, walletProp02] const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, publicName, createLiquidityAccount: true, additionalProperties @@ -729,6 +734,7 @@ describe('Wallet Address Resolvers', (): void => { 'Can get a wallet address by its url (publicName: $publicName)', async ({ publicName }): Promise => { const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, publicName, createLiquidityAccount: true }) @@ -818,14 +824,17 @@ describe('Wallet Address Resolvers', (): void => { getPageTests({ getClient: () => appContainer.apolloClient, - createModel: () => createWalletAddress(deps), + createModel: () => + createWalletAddress(deps, { tenantId: Config.operatorTenantId }), pagedQuery: 'walletAddresses' }) test('Can get page of wallet addresses', async (): Promise => { const walletAddresses: WalletAddressModel[] = [] for (let i = 0; i < 2; i++) { - walletAddresses.push(await createWalletAddress(deps)) + walletAddresses.push( + await createWalletAddress(deps, { tenantId: Config.operatorTenantId }) + ) } walletAddresses.reverse() // Calling the default getPage will result in descending order const query = await appContainer.apolloClient @@ -889,6 +898,7 @@ describe('Wallet Address Resolvers', (): void => { const withdrawalAmount = BigInt(10) for (let i = 0; i < 3; i++) { const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, createLiquidityAccount: true }) if (i) { diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index cea6f15486..34d2e34727 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -8,7 +8,7 @@ import { MutationResolvers, WalletAddressStatus } from '../generated/graphql' -import { ApolloContext, TenantedApolloContext } from '../../app' +import { TenantedApolloContext } from '../../app' import { WalletAddressError, isWalletAddressError, @@ -24,7 +24,7 @@ import { UpdateOptions } from '../../open_payments/wallet_address/service' -export const getWalletAddresses: QueryResolvers['walletAddresses'] = +export const getWalletAddresses: QueryResolvers['walletAddresses'] = async ( parent, args, @@ -52,11 +52,11 @@ export const getWalletAddresses: QueryResolvers['walletAddresses' } } -export const getWalletAddress: QueryResolvers['walletAddress'] = +export const getWalletAddress: QueryResolvers['walletAddress'] = async (parent, args, ctx): Promise => { const walletAddressService = await ctx.container.use('walletAddressService') const walletAddress = await walletAddressService.get(args.id) - if (!walletAddress) { + if (!walletAddress || walletAddress.tenantId !== ctx.tenant.id) { throw new GraphQLError( errorToMessage[WalletAddressError.UnknownWalletAddress], { @@ -69,7 +69,7 @@ export const getWalletAddress: QueryResolvers['walletAddress'] = return walletAddressToGraphql(walletAddress) } -export const getWalletAddressByUrl: QueryResolvers['walletAddressByUrl'] = +export const getWalletAddressByUrl: QueryResolvers['walletAddressByUrl'] = async ( parent, args, @@ -77,7 +77,9 @@ export const getWalletAddressByUrl: QueryResolvers['walletAddress ): Promise => { const walletAddressService = await ctx.container.use('walletAddressService') const walletAddress = await walletAddressService.getByUrl(args.url) - return walletAddress ? walletAddressToGraphql(walletAddress) : null + return walletAddress && ctx.tenant.id === walletAddress.tenantId + ? walletAddressToGraphql(walletAddress) + : null } export const createWalletAddress: MutationResolvers['createWalletAddress'] = @@ -118,7 +120,7 @@ export const createWalletAddress: MutationResolvers['crea } } -export const updateWalletAddress: MutationResolvers['updateWalletAddress'] = +export const updateWalletAddress: MutationResolvers['updateWalletAddress'] = async ( parent, args, @@ -127,6 +129,7 @@ export const updateWalletAddress: MutationResolvers['updateWallet const walletAddressService = await ctx.container.use('walletAddressService') const { additionalProperties, ...rest } = args.input const updateOptions: UpdateOptions = { + tenantId: ctx.tenant.id, ...rest } if (additionalProperties) { @@ -154,7 +157,7 @@ export const updateWalletAddress: MutationResolvers['updateWallet } } -export const triggerWalletAddressEvents: MutationResolvers['triggerWalletAddressEvents'] = +export const triggerWalletAddressEvents: MutationResolvers['triggerWalletAddressEvents'] = async ( parent, args, diff --git a/packages/backend/src/open_payments/auth/middleware.test.ts b/packages/backend/src/open_payments/auth/middleware.test.ts index 26b06dc9da..6f066e4772 100644 --- a/packages/backend/src/open_payments/auth/middleware.test.ts +++ b/packages/backend/src/open_payments/auth/middleware.test.ts @@ -78,7 +78,9 @@ describe('Auth Middleware', (): void => { Authorization: `GNAP ${token}` } }, - walletAddress: await createWalletAddress(deps) + walletAddress: await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) }) ctx.container = deps }) diff --git a/packages/backend/src/open_payments/payment/combined/service.test.ts b/packages/backend/src/open_payments/payment/combined/service.test.ts index fddb5efd43..049f1b7661 100644 --- a/packages/backend/src/open_payments/payment/combined/service.test.ts +++ b/packages/backend/src/open_payments/payment/combined/service.test.ts @@ -43,7 +43,10 @@ describe('Combined Payment Service', (): void => { sendAsset = await createAsset(deps) receiveAsset = await createAsset(deps) sendWalletAddressId = ( - await createWalletAddress(deps, { assetId: sendAsset.id }) + await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, + assetId: sendAsset.id + }) ).id receiveWalletAddress = await createWalletAddress(deps, { assetId: receiveAsset.id diff --git a/packages/backend/src/open_payments/payment/incoming/model.test.ts b/packages/backend/src/open_payments/payment/incoming/model.test.ts index e4577af3ad..e25f524449 100644 --- a/packages/backend/src/open_payments/payment/incoming/model.test.ts +++ b/packages/backend/src/open_payments/payment/incoming/model.test.ts @@ -42,7 +42,9 @@ describe('Models', (): void => { let incomingPayment: IncomingPayment beforeEach(async (): Promise => { - walletAddress = await createWalletAddress(deps) + walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) baseUrl = new URL(walletAddress.url).origin incomingPayment = await createIncomingPayment(deps, { walletAddressId: walletAddress.id, diff --git a/packages/backend/src/open_payments/payment/incoming/routes.test.ts b/packages/backend/src/open_payments/payment/incoming/routes.test.ts index 72835eae3b..3a92e10ee9 100644 --- a/packages/backend/src/open_payments/payment/incoming/routes.test.ts +++ b/packages/backend/src/open_payments/payment/incoming/routes.test.ts @@ -54,6 +54,7 @@ describe('Incoming Payment Routes', (): void => { expiresAt = new Date(Date.now() + 30_000) asset = await createAsset(deps) walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) baseUrl = new URL(walletAddress.url).origin @@ -127,7 +128,9 @@ describe('Incoming Payment Routes', (): void => { test.each([IncomingPaymentState.Completed, IncomingPaymentState.Expired])( 'returns incoming payment with empty methods if payment state is %s', async (paymentState): Promise => { - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) const incomingPayment = await createIncomingPayment(deps, { walletAddressId: walletAddress.id }) diff --git a/packages/backend/src/open_payments/payment/outgoing/routes.test.ts b/packages/backend/src/open_payments/payment/outgoing/routes.test.ts index f30dab4641..270586fc22 100644 --- a/packages/backend/src/open_payments/payment/outgoing/routes.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/routes.test.ts @@ -77,7 +77,10 @@ describe('Outgoing Payment Routes', (): void => { beforeEach(async (): Promise => { const asset = await createAsset(deps) - walletAddress = await createWalletAddress(deps, { assetId: asset.id }) + walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, + assetId: asset.id + }) baseUrl = new URL(walletAddress.url).origin }) diff --git a/packages/backend/src/open_payments/quote/routes.test.ts b/packages/backend/src/open_payments/quote/routes.test.ts index 422759bd73..791b4f48b3 100644 --- a/packages/backend/src/open_payments/quote/routes.test.ts +++ b/packages/backend/src/open_payments/quote/routes.test.ts @@ -77,6 +77,7 @@ describe('Quote Routes', (): void => { scale: debitAmount.assetScale }) walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId }) baseUrl = new URL(walletAddress.url).origin diff --git a/packages/backend/src/open_payments/quote/service.test.ts b/packages/backend/src/open_payments/quote/service.test.ts index 0de56023aa..75e59ec365 100644 --- a/packages/backend/src/open_payments/quote/service.test.ts +++ b/packages/backend/src/open_payments/quote/service.test.ts @@ -436,7 +436,9 @@ describe('QuoteService', (): void => { }) test('fails on inactive wallet address', async () => { - 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() }) diff --git a/packages/backend/src/open_payments/receiver/model.test.ts b/packages/backend/src/open_payments/receiver/model.test.ts index 0255c615f0..5976465ad5 100644 --- a/packages/backend/src/open_payments/receiver/model.test.ts +++ b/packages/backend/src/open_payments/receiver/model.test.ts @@ -38,7 +38,9 @@ describe('Receiver Model', (): void => { describe('constructor', () => { test('creates receiver', async () => { - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) const incomingPayment = await createIncomingPayment(deps, { walletAddressId: walletAddress.id }) @@ -82,7 +84,9 @@ describe('Receiver Model', (): void => { }) test('throws if incoming payment is completed', async () => { - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) const incomingPayment = await createIncomingPayment(deps, { walletAddressId: walletAddress.id }) @@ -105,7 +109,9 @@ describe('Receiver Model', (): void => { }) test('throws if incoming payment is expired', async () => { - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) const incomingPayment = await createIncomingPayment(deps, { walletAddressId: walletAddress.id }) @@ -125,7 +131,9 @@ describe('Receiver Model', (): void => { }) test('throws if stream credentials has invalid ILP address', async () => { - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) const incomingPayment = await createIncomingPayment(deps, { walletAddressId: walletAddress.id }) diff --git a/packages/backend/src/open_payments/receiver/service.test.ts b/packages/backend/src/open_payments/receiver/service.test.ts index ed3d05ebd7..debc5c8a56 100644 --- a/packages/backend/src/open_payments/receiver/service.test.ts +++ b/packages/backend/src/open_payments/receiver/service.test.ts @@ -81,6 +81,7 @@ describe('Receiver Service', (): void => { describe('local incoming payment', () => { test('resolves local incoming payment', async () => { const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, mockServerPort: Config.openPaymentsPort }) const incomingPayment = await createIncomingPayment(deps, { @@ -289,6 +290,7 @@ describe('Receiver Service', (): void => { }) walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, mockServerPort: Config.openPaymentsPort, assetId: asset.id }) diff --git a/packages/backend/src/open_payments/wallet_address/key/routes.test.ts b/packages/backend/src/open_payments/wallet_address/key/routes.test.ts index 056e1d54a0..a70be4e974 100644 --- a/packages/backend/src/open_payments/wallet_address/key/routes.test.ts +++ b/packages/backend/src/open_payments/wallet_address/key/routes.test.ts @@ -45,7 +45,9 @@ describe('Wallet Address Keys Routes', (): void => { describe('get', (): void => { test('returns 200 with all keys for a wallet address', async (): Promise => { - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) const keyOption = { walletAddressId: walletAddress.id, @@ -67,7 +69,9 @@ describe('Wallet Address Keys Routes', (): void => { }) test('returns 200 with empty array if no keys for a wallet address', async (): Promise => { - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) const ctx = createContext({ headers: { Accept: 'application/json' }, @@ -119,7 +123,9 @@ describe('Wallet Address Keys Routes', (): void => { }) test('throws 404 error for inactive wallet address', async (): Promise => { - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) await walletAddress.$query().patch({ deactivatedAt: new Date() }) diff --git a/packages/backend/src/open_payments/wallet_address/key/service.test.ts b/packages/backend/src/open_payments/wallet_address/key/service.test.ts index 6cf6972a0b..c2417ca06f 100644 --- a/packages/backend/src/open_payments/wallet_address/key/service.test.ts +++ b/packages/backend/src/open_payments/wallet_address/key/service.test.ts @@ -29,7 +29,9 @@ describe('Wallet Address Key Service', (): void => { }) beforeEach(async (): Promise => { - walletAddress = await createWalletAddress(deps) + walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) }) afterEach(async (): Promise => { diff --git a/packages/backend/src/open_payments/wallet_address/middleware.test.ts b/packages/backend/src/open_payments/wallet_address/middleware.test.ts index 7aeb1f3ea5..72d0b0dfc5 100644 --- a/packages/backend/src/open_payments/wallet_address/middleware.test.ts +++ b/packages/backend/src/open_payments/wallet_address/middleware.test.ts @@ -355,7 +355,9 @@ describe('Wallet Address Middleware', (): void => { }) test('throws error for deactivated wallet address', async (): Promise => { - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) ctx.walletAddressUrl = walletAddress.url await walletAddress.$query().patch({ deactivatedAt: new Date() }) @@ -372,7 +374,9 @@ describe('Wallet Address Middleware', (): void => { }) test('sets walletAddress on context and calls next', async (): Promise => { - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) ctx.walletAddressUrl = walletAddress.url await expect( diff --git a/packages/backend/src/open_payments/wallet_address/model.test.ts b/packages/backend/src/open_payments/wallet_address/model.test.ts index 6aaeb79184..3dd586873d 100644 --- a/packages/backend/src/open_payments/wallet_address/model.test.ts +++ b/packages/backend/src/open_payments/wallet_address/model.test.ts @@ -413,7 +413,9 @@ describe('Models', (): void => { test.each(deactivatedAtCases)( '$description', async ({ value, expectedIsActive }) => { - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) if (value) { await walletAddress .$query(appContainer.knex) diff --git a/packages/backend/src/open_payments/wallet_address/routes.test.ts b/packages/backend/src/open_payments/wallet_address/routes.test.ts index 0962da639b..c75e5041b9 100644 --- a/packages/backend/src/open_payments/wallet_address/routes.test.ts +++ b/packages/backend/src/open_payments/wallet_address/routes.test.ts @@ -62,6 +62,7 @@ describe('Wallet Address Routes', (): void => { test('throws 404 error for inactive wallet address', async (): Promise => { const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, publicName: faker.person.firstName() }) diff --git a/packages/backend/src/open_payments/wallet_address/service.test.ts b/packages/backend/src/open_payments/wallet_address/service.test.ts index 387aa0c47f..ce509f50bc 100644 --- a/packages/backend/src/open_payments/wallet_address/service.test.ts +++ b/packages/backend/src/open_payments/wallet_address/service.test.ts @@ -179,13 +179,16 @@ describe('Open Payments Wallet Address Service', (): void => { `( 'Wallet address with initial isActive of $initialIsActive can be updated with $status status ', async ({ initialIsActive, status, expectedIsActive }): Promise => { - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) if (!initialIsActive) { await walletAddress.$query(knex).patch({ deactivatedAt: new Date() }) } const updatedWalletAddress = await walletAddressService.update({ + tenantId: Config.operatorTenantId, id: walletAddress.id, status }) @@ -201,10 +204,12 @@ describe('Open Payments Wallet Address Service', (): void => { test('publicName', async (): Promise => { const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, publicName: 'Initial Name' }) const newName = 'New Name' const updatedWalletAddress = await walletAddressService.update({ + tenantId: Config.operatorTenantId, id: walletAddress.id, publicName: newName }) @@ -226,7 +231,9 @@ describe('Open Payments Wallet Address Service', (): void => { incomingPaymentExpiryMaxMs: 2592000000 * 3 }, async (): Promise => { - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) const now = new Date('2023-06-01T00:00:00Z').getTime() jest.useFakeTimers({ now }) @@ -249,6 +256,7 @@ describe('Open Payments Wallet Address Service', (): void => { }) await walletAddressService.update({ + tenantId: Config.operatorTenantId, id: walletAddress.id, status: 'INACTIVE' }) @@ -269,7 +277,9 @@ describe('Open Payments Wallet Address Service', (): void => { () => config, { walletAddressDeactivationPaymentGracePeriodMs: 2592000000 }, async (): Promise => { - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) const now = new Date('2023-06-01T00:00:00Z').getTime() jest.useFakeTimers({ now }) @@ -291,6 +301,7 @@ describe('Open Payments Wallet Address Service', (): void => { }) await walletAddressService.update({ + tenantId: Config.operatorTenantId, id: walletAddress.id, status: 'INACTIVE' }) @@ -305,6 +316,7 @@ describe('Open Payments Wallet Address Service', (): void => { describe('additionalProperties', (): void => { test('should do nothing if additionalProperties is undefined', async (): Promise => { const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, publicName: 'Initial Name', additionalProperties: [ { @@ -316,6 +328,7 @@ describe('Open Payments Wallet Address Service', (): void => { }) const updatedWalletAddress = await walletAddressService.update({ + tenantId: Config.operatorTenantId, id: walletAddress.id, status: walletAddress.isActive ? 'ACTIVE' : 'INACTIVE', publicName: 'Updated Name', @@ -334,6 +347,7 @@ describe('Open Payments Wallet Address Service', (): void => { test('should update to [] (deleting all) when additionalProperties is []', async (): Promise => { const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, additionalProperties: [ { fieldKey: 'key1', @@ -350,6 +364,7 @@ describe('Open Payments Wallet Address Service', (): void => { const publicName = 'Updated Name' const updatedWalletAddress = await walletAddressService.update({ + tenantId: Config.operatorTenantId, id: walletAddress.id, publicName, additionalProperties: [] @@ -366,6 +381,7 @@ describe('Open Payments Wallet Address Service', (): void => { }) test('should replace existing additionalProperties', async (): Promise => { const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, additionalProperties: [ { fieldKey: 'key1', @@ -394,6 +410,7 @@ describe('Open Payments Wallet Address Service', (): void => { ] const updatedWalletAddress = await walletAddressService.update({ + tenantId: Config.operatorTenantId, id: walletAddress.id, additionalProperties: newProperties }) @@ -417,6 +434,7 @@ describe('Open Payments Wallet Address Service', (): void => { test('Cannot update unknown wallet address', async (): Promise => { await expect( walletAddressService.update({ + tenantId: Config.operatorTenantId, id: uuid(), status: 'INACTIVE', publicName: 'Some Public Name' @@ -427,7 +445,9 @@ describe('Open Payments Wallet Address Service', (): void => { describe('Get Wallet Address By Url', (): void => { test('can retrieve wallet address by url', async (): Promise => { - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) await expect( walletAddressService.getByUrl(walletAddress.url) ).resolves.toEqual(walletAddress) @@ -458,7 +478,9 @@ describe('Open Payments Wallet Address Service', (): void => { describe('Get Or Poll Wallet Addres By Url', (): void => { describe('existing wallet address', (): void => { test('can retrieve wallet address by url', async (): Promise => { - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) await expect( walletAddressService.getOrPollByUrl(walletAddress.url) ).resolves.toEqual(walletAddress) @@ -504,6 +526,7 @@ describe('Open Payments Wallet Address Service', (): void => { (async () => { await sleep(5) return createWalletAddress(deps, { + tenantId: Config.operatorTenantId, url: walletAddressUrl }) })() @@ -533,7 +556,8 @@ describe('Open Payments Wallet Address Service', (): void => { describe('Wallet Address pagination', (): void => { describe('getPage', (): void => { getPageTests({ - createModel: () => createWalletAddress(deps), + createModel: () => + createWalletAddress(deps, { tenantId: Config.operatorTenantId }), getPage: (pagination?: Pagination, sortOrder?: SortOrder) => walletAddressService.getPage(pagination, sortOrder) }) @@ -544,7 +568,9 @@ describe('Open Payments Wallet Address Service', (): void => { let walletAddress: WalletAddress beforeEach(async (): Promise => { - walletAddress = await createWalletAddress(deps) + walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) }) describe.each` @@ -664,6 +690,7 @@ describe('Open Payments Wallet Address Service', (): void => { beforeEach(async (): Promise => { walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, createLiquidityAccount: true }) }) @@ -737,6 +764,7 @@ describe('Open Payments Wallet Address Service', (): void => { for (let i = 0; i < 5; i++) { walletAddresses.push( await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId, createLiquidityAccount: true }) @@ -848,7 +876,9 @@ describe('Open Payments Wallet Address Service using Cache', (): void => { expectedCallCount }): Promise => { const spyCacheSet = jest.spyOn(walletAddressCache, 'set') - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) expect(spyCacheSet).toHaveBeenCalledTimes(1) if (!initialIsActive) { @@ -863,12 +893,14 @@ describe('Open Payments Wallet Address Service using Cache', (): void => { // Update through the service, will also update the wallet-address cache: await walletAddressService.update({ + tenantId: Config.operatorTenantId, id: walletAddress.id, status: 'INACTIVE' }) } const updatedWalletAddress = await walletAddressService.update({ + tenantId: Config.operatorTenantId, id: walletAddress.id, status }) diff --git a/packages/backend/src/open_payments/wallet_address/service.ts b/packages/backend/src/open_payments/wallet_address/service.ts index 7826079d11..d1069ec9b2 100644 --- a/packages/backend/src/open_payments/wallet_address/service.ts +++ b/packages/backend/src/open_payments/wallet_address/service.ts @@ -49,6 +49,7 @@ export interface CreateOptions extends Options { type Status = 'ACTIVE' | 'INACTIVE' export interface UpdateOptions extends Options { + tenantId: string id: string status?: Status additionalProperties?: WalletAddressAdditionalPropertyInput[] @@ -213,13 +214,14 @@ async function createWalletAddress( async function updateWalletAddress( deps: ServiceDependencies, - { id, status, publicName, additionalProperties }: UpdateOptions + { id, tenantId, status, publicName, additionalProperties }: UpdateOptions ): Promise { const trx = await WalletAddress.startTransaction() try { const update: UpdateInput = { publicName } const walletAddress = await WalletAddress.query(trx) - .findById(id) + .where({ id, tenantId }) + .first() .throwIfNotFound() if (status === 'INACTIVE' && walletAddress.isActive) { diff --git a/packages/backend/src/payment-method/handler/service.test.ts b/packages/backend/src/payment-method/handler/service.test.ts index 254963967c..6398f06b6f 100644 --- a/packages/backend/src/payment-method/handler/service.test.ts +++ b/packages/backend/src/payment-method/handler/service.test.ts @@ -47,6 +47,7 @@ describe('PaymentMethodHandlerService', (): void => { test('calls ilpPaymentService for ILP payment type', async (): Promise => { const asset = await createAsset(deps) const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) @@ -75,6 +76,7 @@ describe('PaymentMethodHandlerService', (): void => { test('calls localPaymentService for local payment type', async (): Promise => { const asset = await createAsset(deps) const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) @@ -105,6 +107,7 @@ describe('PaymentMethodHandlerService', (): void => { test('calls ilpPaymentService for ILP payment type', async (): Promise => { const asset = await createAsset(deps) const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) const { receiver, outgoingPayment } = @@ -139,6 +142,7 @@ describe('PaymentMethodHandlerService', (): void => { test('calls localPaymentService for local payment type', async (): Promise => { const asset = await createAsset(deps) const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) const { receiver, outgoingPayment } = diff --git a/packages/backend/src/payment-method/ilp/service.test.ts b/packages/backend/src/payment-method/ilp/service.test.ts index b2f37b7025..bba27b8315 100644 --- a/packages/backend/src/payment-method/ilp/service.test.ts +++ b/packages/backend/src/payment-method/ilp/service.test.ts @@ -67,10 +67,12 @@ describe('IlpPaymentService', (): void => { }) walletAddressMap['USD'] = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: assetMap['USD'].id }) walletAddressMap['EUR'] = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: assetMap['EUR'].id }) }) diff --git a/packages/backend/src/payment-method/ilp/spsp/middleware.test.ts b/packages/backend/src/payment-method/ilp/spsp/middleware.test.ts index 9a4705f9c6..896c30dac6 100644 --- a/packages/backend/src/payment-method/ilp/spsp/middleware.test.ts +++ b/packages/backend/src/payment-method/ilp/spsp/middleware.test.ts @@ -37,6 +37,7 @@ describe('SPSP Middleware', (): void => { beforeEach(async (): Promise => { const asset = await createAsset(deps) walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) ctx = setup({ diff --git a/packages/backend/src/payment-method/ilp/stream-credentials/service.test.ts b/packages/backend/src/payment-method/ilp/stream-credentials/service.test.ts index 22c4f4842b..8b479ac893 100644 --- a/packages/backend/src/payment-method/ilp/stream-credentials/service.test.ts +++ b/packages/backend/src/payment-method/ilp/stream-credentials/service.test.ts @@ -27,7 +27,9 @@ describe('Stream Credentials Service', (): void => { }) beforeEach(async (): Promise => { - const { id: walletAddressId } = await createWalletAddress(deps) + const { id: walletAddressId } = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) incomingPayment = await createIncomingPayment(deps, { walletAddressId }) diff --git a/packages/backend/src/payment-method/local/service.test.ts b/packages/backend/src/payment-method/local/service.test.ts index 72196b298d..c92cb4449d 100644 --- a/packages/backend/src/payment-method/local/service.test.ts +++ b/packages/backend/src/payment-method/local/service.test.ts @@ -69,14 +69,17 @@ describe('LocalPaymentService', (): void => { }) walletAddressMap['USD'] = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: assetMap['USD'].id }) walletAddressMap['USD_9'] = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: assetMap['USD_9'].id }) walletAddressMap['EUR'] = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: assetMap['EUR'].id }) }) diff --git a/packages/backend/src/shared/pagination.test.ts b/packages/backend/src/shared/pagination.test.ts index 18be4c5541..d4996a1cae 100644 --- a/packages/backend/src/shared/pagination.test.ts +++ b/packages/backend/src/shared/pagination.test.ts @@ -47,6 +47,7 @@ describe('Pagination', (): void => { beforeEach(async (): Promise => { const asset = await createAsset(deps) walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) }) @@ -84,9 +85,11 @@ describe('Pagination', (): void => { const asset = await createAsset(deps) defaultWalletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) secondaryWalletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId, assetId: asset.id }) debitAmount = { diff --git a/packages/backend/src/webhook/service.test.ts b/packages/backend/src/webhook/service.test.ts index bb33fdec72..4d91d50e02 100644 --- a/packages/backend/src/webhook/service.test.ts +++ b/packages/backend/src/webhook/service.test.ts @@ -116,8 +116,12 @@ describe('Webhook Service', (): void => { let events: WebhookEvent[] = [] beforeEach(async (): Promise => { - walletAddressIn = await createWalletAddress(deps) - walletAddressOut = await createWalletAddress(deps) + walletAddressIn = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) + walletAddressOut = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + }) incomingPaymentIds = [ ( await createIncomingPayment(deps, { From aef947104e682aeb7e33ada9ab24bc7f7f00af3c Mon Sep 17 00:00:00 2001 From: koekiebox Date: Tue, 7 Jan 2025 23:15:40 +0100 Subject: [PATCH 31/66] feat(3114): fix test cases for wallet address. --- packages/backend/src/app.ts | 14 +++++++++++++- .../src/graphql/resolvers/wallet_address.test.ts | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index b26f5e861c..4c121c2d2a 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -105,6 +105,7 @@ import { getTenantFromApiSignature, TenantApiSignatureResult } from './shared/utils' +import { faker } from '@faker-js/faker' export interface AppContextData { logger: Logger container: AppContainer @@ -395,7 +396,18 @@ export class App { let tenantApiSignatureResult: TenantApiSignatureResult if (this.config.env === 'test') { const tenantService = await this.container.use('tenantService') - const tenant = await tenantService.get(this.config.operatorTenantId) + let tenant = await tenantService.get(this.config.operatorTenantId) + if (!tenant) { + const createOptions = { + id: this.config.operatorTenantId, + email: faker.internet.email(), + publicName: faker.company.name(), + apiSecret: 'test-api-secret', + idpConsentUrl: faker.internet.url(), + idpSecret: 'test-idp-secret' + } + tenant = await Tenant.query().insertAndFetch(createOptions) + } tenantApiSignatureResult = { tenant, isOperator: true } } else { koa.use(async (ctx, next: Koa.Next): Promise => { diff --git a/packages/backend/src/graphql/resolvers/wallet_address.test.ts b/packages/backend/src/graphql/resolvers/wallet_address.test.ts index 2e2cf21bff..7a46a91c2e 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.test.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.test.ts @@ -43,7 +43,7 @@ describe('Wallet Address Resolvers', (): void => { let walletAddressService: WalletAddressService beforeAll(async (): Promise => { - deps = await initIocContainer({ + deps = initIocContainer({ ...Config, localCacheDuration: 0 }) From 6510bed175f1933829a9efbf4343c5428340e769 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Wed, 8 Jan 2025 13:24:57 +0100 Subject: [PATCH 32/66] feat(3114): address review comments. --- packages/backend/src/app.ts | 5 +-- packages/backend/src/index.ts | 1 - .../src/open_payments/wallet_address/model.ts | 1 - .../open_payments/wallet_address/service.ts | 42 +++---------------- 4 files changed, 8 insertions(+), 41 deletions(-) diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 4c121c2d2a..a5a0bcd77e 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -398,15 +398,14 @@ export class App { const tenantService = await this.container.use('tenantService') let tenant = await tenantService.get(this.config.operatorTenantId) if (!tenant) { - const createOptions = { + tenant = await Tenant.query().insertAndFetch({ id: this.config.operatorTenantId, email: faker.internet.email(), publicName: faker.company.name(), apiSecret: 'test-api-secret', idpConsentUrl: faker.internet.url(), idpSecret: 'test-idp-secret' - } - tenant = await Tenant.query().insertAndFetch(createOptions) + }) } tenantApiSignatureResult = { tenant, isOperator: true } } else { diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 4919741053..b167410756 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -392,7 +392,6 @@ 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') }) diff --git a/packages/backend/src/open_payments/wallet_address/model.ts b/packages/backend/src/open_payments/wallet_address/model.ts index 7b8200608d..c6de194da7 100644 --- a/packages/backend/src/open_payments/wallet_address/model.ts +++ b/packages/backend/src/open_payments/wallet_address/model.ts @@ -63,7 +63,6 @@ export class WalletAddress public asset!: Asset public readonly tenantId!: string - public tenant!: Tenant // The cumulative received amount tracked by // `wallet_address.web_monetization` webhook events. diff --git a/packages/backend/src/open_payments/wallet_address/service.ts b/packages/backend/src/open_payments/wallet_address/service.ts index d1069ec9b2..2495148cdd 100644 --- a/packages/backend/src/open_payments/wallet_address/service.ts +++ b/packages/backend/src/open_payments/wallet_address/service.ts @@ -28,7 +28,6 @@ import { poll } from '../../shared/utils' import { WalletAddressAdditionalProperty } from './additional_property/model' import { AssetService } from '../../asset/service' import { CacheDataStore } from '../../middleware/cache/data-stores' -import { TenantService } from '../../tenants/service' interface Options { publicName?: string @@ -83,7 +82,6 @@ interface ServiceDependencies extends BaseService { knex: TransactionOrKnex accountingService: AccountingService webhookService: WebhookService - tenantService: TenantService assetService: AssetService walletAddressCache: CacheDataStore } @@ -94,7 +92,6 @@ export async function createWalletAddressService({ knex, accountingService, webhookService, - tenantService, assetService, walletAddressCache }: ServiceDependencies): Promise { @@ -107,7 +104,6 @@ export async function createWalletAddressService({ knex, accountingService, webhookService, - tenantService, assetService, walletAddressCache } @@ -188,13 +184,8 @@ async function createWalletAddress( assetId: options.assetId, additionalProperties: additionalProperties }) - - const [asset, tenant] = await Promise.all([ - deps.assetService.get(walletAddress.assetId), - deps.tenantService.get(walletAddress.tenantId) - ]) + const asset = await deps.assetService.get(walletAddress.assetId) if (asset) walletAddress.asset = asset - if (tenant) walletAddress.tenant = tenant await deps.walletAddressCache.set(walletAddress.id, walletAddress) return walletAddress @@ -235,12 +226,8 @@ async function updateWalletAddress( .$query(trx) .patchAndFetch(update) .throwIfNotFound() - const [asset, tenant] = await Promise.all([ - deps.assetService.get(updatedWalletAddress.assetId), - deps.tenantService.get(updatedWalletAddress.tenantId) - ]) + const asset = await deps.assetService.get(updatedWalletAddress.assetId) if (asset) updatedWalletAddress.asset = asset - if (tenant) updatedWalletAddress.tenant = tenant // Override all existing additional properties if new ones are provided if (additionalProperties) { @@ -284,13 +271,8 @@ async function getWalletAddress( const walletAddress = await WalletAddress.query(deps.knex).findById(id) if (walletAddress) { - const [asset, tenant] = await Promise.all([ - deps.assetService.get(walletAddress.assetId), - deps.tenantService.get(walletAddress.tenantId) - ]) + const asset = await deps.assetService.get(walletAddress.assetId) if (asset) walletAddress.asset = asset - if (tenant) walletAddress.tenant = tenant - await deps.walletAddressCache.set(id, walletAddress) } return walletAddress @@ -351,12 +333,8 @@ async function getWalletAddressByUrl( url: url.toLowerCase() }) if (walletAddress) { - const [asset, tenant] = await Promise.all([ - deps.assetService.get(walletAddress.assetId), - deps.tenantService.get(walletAddress.tenantId) - ]) + const asset = await deps.assetService.get(walletAddress.assetId) if (asset) walletAddress.asset = asset - if (tenant) walletAddress.tenant = tenant } return walletAddress || undefined } @@ -371,12 +349,8 @@ async function getWalletAddressPage( sortOrder ) for (const address of addresses) { - const [asset, tenant] = await Promise.all([ - deps.assetService.get(address.assetId), - deps.tenantService.get(address.tenantId) - ]) + const asset = await deps.assetService.get(address.assetId) if (asset) address.asset = asset - if (tenant) address.tenant = tenant } return addresses } @@ -413,12 +387,8 @@ async function processNextWalletAddresses( .skipLocked() .where('processAt', '<=', now) for (const walletAddress of walletAddresses) { - const [asset, tenant] = await Promise.all([ - deps_.assetService.get(walletAddress.assetId), - deps_.tenantService.get(walletAddress.tenantId) - ]) + const asset = await deps_.assetService.get(walletAddress.assetId) if (asset) walletAddress.asset = asset - if (tenant) walletAddress.tenant = tenant } const deps = { From 2558836fae57f79cb7479b70dd35463641303004 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Sat, 11 Jan 2025 13:57:35 +0100 Subject: [PATCH 33/66] feat(3114): rework tenantId to not be mandatory for wallet address service layer. --- .../generated/graphql.ts | 4 ++ packages/backend/src/app.ts | 13 +--- .../src/graphql/generated/graphql.schema.json | 24 +++++++ .../backend/src/graphql/generated/graphql.ts | 4 ++ .../src/graphql/resolvers/wallet_address.ts | 67 +++++++++++++++++-- packages/backend/src/graphql/schema.graphql | 4 ++ .../open_payments/wallet_address/errors.ts | 10 ++- .../wallet_address/service.test.ts | 10 --- .../open_payments/wallet_address/service.ts | 14 ++-- packages/backend/src/shared/utils.ts | 23 +++++++ packages/backend/src/tests/tableManager.ts | 4 +- packages/frontend/app/generated/graphql.ts | 4 ++ .../src/generated/graphql.ts | 4 ++ test/integration/lib/generated/graphql.ts | 4 ++ 14 files changed, 151 insertions(+), 38 deletions(-) diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 46dad2dedb..ad84539d43 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -373,6 +373,8 @@ export type CreateWalletAddressInput = { idempotencyKey?: InputMaybe; /** Public name associated with the wallet address. This is visible to anyone with the wallet address URL. */ publicName?: InputMaybe; + /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature. */ + tenantId?: InputMaybe; /** Wallet address URL. This cannot be changed. */ url: Scalars['String']['input']; }; @@ -1459,6 +1461,8 @@ export type UpdateWalletAddressInput = { publicName?: InputMaybe; /** New status to set the wallet address to, either active or inactive. */ status?: InputMaybe; + /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature. */ + tenantId?: InputMaybe; }; export type UpdateWalletAddressMutationResponse = { diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index a5a0bcd77e..b26f5e861c 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -105,7 +105,6 @@ import { getTenantFromApiSignature, TenantApiSignatureResult } from './shared/utils' -import { faker } from '@faker-js/faker' export interface AppContextData { logger: Logger container: AppContainer @@ -396,17 +395,7 @@ export class App { let tenantApiSignatureResult: TenantApiSignatureResult if (this.config.env === 'test') { const tenantService = await this.container.use('tenantService') - let tenant = await tenantService.get(this.config.operatorTenantId) - if (!tenant) { - tenant = await Tenant.query().insertAndFetch({ - id: this.config.operatorTenantId, - email: faker.internet.email(), - publicName: faker.company.name(), - apiSecret: 'test-api-secret', - idpConsentUrl: faker.internet.url(), - idpSecret: 'test-idp-secret' - }) - } + const tenant = await tenantService.get(this.config.operatorTenantId) tenantApiSignatureResult = { tenant, isOperator: true } } else { koa.use(async (ctx, next: Koa.Next): Promise => { diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index 1745966aa2..fd0e1cf0b3 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -2171,6 +2171,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "tenantId", + "description": "Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "url", "description": "Wallet address URL. This cannot be changed.", @@ -8069,6 +8081,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "tenantId", + "description": "Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "interfaces": null, diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 46dad2dedb..ad84539d43 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -373,6 +373,8 @@ export type CreateWalletAddressInput = { idempotencyKey?: InputMaybe; /** Public name associated with the wallet address. This is visible to anyone with the wallet address URL. */ publicName?: InputMaybe; + /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature. */ + tenantId?: InputMaybe; /** Wallet address URL. This cannot be changed. */ url: Scalars['String']['input']; }; @@ -1459,6 +1461,8 @@ export type UpdateWalletAddressInput = { publicName?: InputMaybe; /** New status to set the wallet address to, either active or inactive. */ status?: InputMaybe; + /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature. */ + tenantId?: InputMaybe; }; export type UpdateWalletAddressMutationResponse = { diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index 34d2e34727..d919d3247c 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -23,6 +23,7 @@ import { CreateOptions, UpdateOptions } from '../../open_payments/wallet_address/service' +import { tenantIdToProceed } from '../../shared/utils' export const getWalletAddresses: QueryResolvers['walletAddresses'] = async ( @@ -56,7 +57,7 @@ export const getWalletAddress: QueryResolvers['walletAddr async (parent, args, ctx): Promise => { const walletAddressService = await ctx.container.use('walletAddressService') const walletAddress = await walletAddressService.get(args.id) - if (!walletAddress || walletAddress.tenantId !== ctx.tenant.id) { + if (!walletAddress) { throw new GraphQLError( errorToMessage[WalletAddressError.UnknownWalletAddress], { @@ -66,6 +67,19 @@ export const getWalletAddress: QueryResolvers['walletAddr } ) } + const tenantId = tenantIdToProceed( + ctx.isOperator, + ctx.tenant.id, + walletAddress.tenantId + ) + if (!tenantId) { + const err = WalletAddressError.InvalidTenantIdNotAllowed + throw new GraphQLError(errorToMessage[err], { + extensions: { + code: errorToCode[err] + } + }) + } return walletAddressToGraphql(walletAddress) } @@ -77,9 +91,22 @@ export const getWalletAddressByUrl: QueryResolvers['walle ): Promise => { const walletAddressService = await ctx.container.use('walletAddressService') const walletAddress = await walletAddressService.getByUrl(args.url) - return walletAddress && ctx.tenant.id === walletAddress.tenantId - ? walletAddressToGraphql(walletAddress) - : null + if (walletAddress) { + const tenantId = tenantIdToProceed( + ctx.isOperator, + ctx.tenant.id, + walletAddress.tenantId + ) + if (!tenantId) { + const err = WalletAddressError.InvalidTenantIdNotAllowed + throw new GraphQLError(errorToMessage[err], { + extensions: { + code: errorToCode[err] + } + }) + } + return walletAddressToGraphql(walletAddress) + } else return null } export const createWalletAddress: MutationResolvers['createWalletAddress'] = @@ -99,9 +126,23 @@ export const createWalletAddress: MutationResolvers['crea addProps.push(toAdd) }) + const tenantId = tenantIdToProceed( + ctx.isOperator, + ctx.tenant.id, + args.input.tenantId + ) + if (!tenantId) { + const err = WalletAddressError.InvalidTenantIdNotAllowed + throw new GraphQLError(errorToMessage[err], { + extensions: { + code: errorToCode[err] + } + }) + } + const options: CreateOptions = { assetId: args.input.assetId, - tenantId: ctx.tenant.id, + tenantId, additionalProperties: addProps, publicName: args.input.publicName, url: args.input.url @@ -128,8 +169,22 @@ export const updateWalletAddress: MutationResolvers['upda ): Promise => { const walletAddressService = await ctx.container.use('walletAddressService') const { additionalProperties, ...rest } = args.input + + const tenantId = tenantIdToProceed( + ctx.isOperator, + ctx.tenant.id, + args.input.tenantId + ) + if (!tenantId) { + const err = WalletAddressError.InvalidTenantIdNotAllowed + throw new GraphQLError(errorToMessage[err], { + extensions: { + code: errorToCode[err] + } + }) + } + const updateOptions: UpdateOptions = { - tenantId: ctx.tenant.id, ...rest } if (additionalProperties) { diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index f8286e14d4..635a15784b 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -1226,6 +1226,8 @@ type CreateReceiverResponse { } input CreateWalletAddressInput { + "Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature." + tenantId: String "Unique identifier of the asset associated with the wallet address. This cannot be changed." assetId: String! "Wallet address URL. This cannot be changed." @@ -1248,6 +1250,8 @@ input AdditionalPropertyInput { } input UpdateWalletAddressInput { + "Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature." + tenantId: String "Unique identifier of the wallet address to update. This cannot be changed." id: ID! "New public name for the wallet address. This is visible to anyone with the wallet address URL." diff --git a/packages/backend/src/open_payments/wallet_address/errors.ts b/packages/backend/src/open_payments/wallet_address/errors.ts index 03d672762c..0c0acfe2db 100644 --- a/packages/backend/src/open_payments/wallet_address/errors.ts +++ b/packages/backend/src/open_payments/wallet_address/errors.ts @@ -4,7 +4,8 @@ export enum WalletAddressError { InvalidUrl = 'InvalidUrl', UnknownAsset = 'UnknownAsset', UnknownWalletAddress = 'UnknownWalletAddress', - DuplicateWalletAddress = 'DuplicateWalletAddress' + DuplicateWalletAddress = 'DuplicateWalletAddress', + InvalidTenantIdNotAllowed = 'InvalidTenantIdNotAllowed' } // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types @@ -17,7 +18,8 @@ export const errorToCode: { [WalletAddressError.InvalidUrl]: GraphQLErrorCode.BadUserInput, [WalletAddressError.UnknownAsset]: GraphQLErrorCode.BadUserInput, [WalletAddressError.UnknownWalletAddress]: GraphQLErrorCode.NotFound, - [WalletAddressError.DuplicateWalletAddress]: GraphQLErrorCode.Duplicate + [WalletAddressError.DuplicateWalletAddress]: GraphQLErrorCode.Duplicate, + [WalletAddressError.InvalidTenantIdNotAllowed]: GraphQLErrorCode.BadUserInput } export const errorToMessage: { @@ -27,5 +29,7 @@ export const errorToMessage: { [WalletAddressError.UnknownAsset]: 'unknown asset', [WalletAddressError.UnknownWalletAddress]: 'unknown wallet address', [WalletAddressError.DuplicateWalletAddress]: - 'Duplicate wallet address found with the same url' + 'Duplicate wallet address found with the same url', + [WalletAddressError.InvalidTenantIdNotAllowed]: + 'Assignment to the specified tenant is not permitted.' } diff --git a/packages/backend/src/open_payments/wallet_address/service.test.ts b/packages/backend/src/open_payments/wallet_address/service.test.ts index ce509f50bc..0477ecab48 100644 --- a/packages/backend/src/open_payments/wallet_address/service.test.ts +++ b/packages/backend/src/open_payments/wallet_address/service.test.ts @@ -188,7 +188,6 @@ describe('Open Payments Wallet Address Service', (): void => { } const updatedWalletAddress = await walletAddressService.update({ - tenantId: Config.operatorTenantId, id: walletAddress.id, status }) @@ -209,7 +208,6 @@ describe('Open Payments Wallet Address Service', (): void => { }) const newName = 'New Name' const updatedWalletAddress = await walletAddressService.update({ - tenantId: Config.operatorTenantId, id: walletAddress.id, publicName: newName }) @@ -256,7 +254,6 @@ describe('Open Payments Wallet Address Service', (): void => { }) await walletAddressService.update({ - tenantId: Config.operatorTenantId, id: walletAddress.id, status: 'INACTIVE' }) @@ -301,7 +298,6 @@ describe('Open Payments Wallet Address Service', (): void => { }) await walletAddressService.update({ - tenantId: Config.operatorTenantId, id: walletAddress.id, status: 'INACTIVE' }) @@ -328,7 +324,6 @@ describe('Open Payments Wallet Address Service', (): void => { }) const updatedWalletAddress = await walletAddressService.update({ - tenantId: Config.operatorTenantId, id: walletAddress.id, status: walletAddress.isActive ? 'ACTIVE' : 'INACTIVE', publicName: 'Updated Name', @@ -364,7 +359,6 @@ describe('Open Payments Wallet Address Service', (): void => { const publicName = 'Updated Name' const updatedWalletAddress = await walletAddressService.update({ - tenantId: Config.operatorTenantId, id: walletAddress.id, publicName, additionalProperties: [] @@ -410,7 +404,6 @@ describe('Open Payments Wallet Address Service', (): void => { ] const updatedWalletAddress = await walletAddressService.update({ - tenantId: Config.operatorTenantId, id: walletAddress.id, additionalProperties: newProperties }) @@ -434,7 +427,6 @@ describe('Open Payments Wallet Address Service', (): void => { test('Cannot update unknown wallet address', async (): Promise => { await expect( walletAddressService.update({ - tenantId: Config.operatorTenantId, id: uuid(), status: 'INACTIVE', publicName: 'Some Public Name' @@ -893,14 +885,12 @@ describe('Open Payments Wallet Address Service using Cache', (): void => { // Update through the service, will also update the wallet-address cache: await walletAddressService.update({ - tenantId: Config.operatorTenantId, id: walletAddress.id, status: 'INACTIVE' }) } const updatedWalletAddress = await walletAddressService.update({ - tenantId: Config.operatorTenantId, id: walletAddress.id, status }) diff --git a/packages/backend/src/open_payments/wallet_address/service.ts b/packages/backend/src/open_payments/wallet_address/service.ts index 2495148cdd..a4abd49f43 100644 --- a/packages/backend/src/open_payments/wallet_address/service.ts +++ b/packages/backend/src/open_payments/wallet_address/service.ts @@ -39,16 +39,15 @@ export type WalletAddressAdditionalPropertyInput = Pick< > export interface CreateOptions extends Options { + tenantId?: string url: string assetId: string - tenantId: string additionalProperties?: WalletAddressAdditionalPropertyInput[] } type Status = 'ACTIVE' | 'INACTIVE' export interface UpdateOptions extends Options { - tenantId: string id: string status?: Status additionalProperties?: WalletAddressAdditionalPropertyInput[] @@ -175,10 +174,14 @@ async function createWalletAddress( ? cleanAdditionalProperties(options.additionalProperties) : undefined + const tenantId = options.tenantId + ? options.tenantId + : deps.config.operatorTenantId + const walletAddress = await WalletAddress.query( deps.knex ).insertGraphAndFetch({ - tenantId: options.tenantId, + tenantId, url: options.url.toLowerCase(), publicName: options.publicName, assetId: options.assetId, @@ -205,14 +208,13 @@ async function createWalletAddress( async function updateWalletAddress( deps: ServiceDependencies, - { id, tenantId, status, publicName, additionalProperties }: UpdateOptions + { id, status, publicName, additionalProperties }: UpdateOptions ): Promise { const trx = await WalletAddress.startTransaction() try { const update: UpdateInput = { publicName } const walletAddress = await WalletAddress.query(trx) - .where({ id, tenantId }) - .first() + .findById(id) .throwIfNotFound() if (status === 'INACTIVE' && walletAddress.isActive) { diff --git a/packages/backend/src/shared/utils.ts b/packages/backend/src/shared/utils.ts index cda429e451..9752705975 100644 --- a/packages/backend/src/shared/utils.ts +++ b/packages/backend/src/shared/utils.ts @@ -241,3 +241,26 @@ export function ensureTrailingSlash(str: string): string { if (!str.endsWith('/')) return `${str}/` return str } + +/** + * The tenantId to use will be determined as follows: + * - When an operator and the {tenantId} is present, return {tenantId} + * - When an operator and {tenantId} is not present, return {signatureTenantId} + * - When NOT an operator and {tenantId} is present, but does not match {signatureTenantId}, return {undefined} + * - Otherwise return {signatureTenantId} + * + * @param isOperator is operator + * @param signatureTenantId the signature tenantId + * @param tenantId the intended tenantId + */ +export function tenantIdToProceed( + isOperator: boolean, + signatureTenantId: string, + tenantId?: string +): string | undefined { + if (isOperator && tenantId) return tenantId + else if (isOperator) return signatureTenantId + return tenantId && tenantId !== signatureTenantId + ? undefined + : signatureTenantId +} diff --git a/packages/backend/src/tests/tableManager.ts b/packages/backend/src/tests/tableManager.ts index 26f07d5d2d..a279de224f 100644 --- a/packages/backend/src/tests/tableManager.ts +++ b/packages/backend/src/tests/tableManager.ts @@ -14,7 +14,9 @@ export async function truncateTables( 'knex_migrations', 'knex_migrations_lock', 'knex_migrations_backend', - 'knex_migrations_backend_lock' + 'knex_migrations_backend_lock', + // We always keep the [cf5fd7d3-1eb1-4041-8e43-ba45747e9e5d] tenant for our test case. + 'tenants' ] ): Promise { const tables = await getTables(knex, ignoreTables) diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index 1116595860..7a5fd40ea0 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -373,6 +373,8 @@ export type CreateWalletAddressInput = { idempotencyKey?: InputMaybe; /** Public name associated with the wallet address. This is visible to anyone with the wallet address URL. */ publicName?: InputMaybe; + /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature. */ + tenantId?: InputMaybe; /** Wallet address URL. This cannot be changed. */ url: Scalars['String']['input']; }; @@ -1459,6 +1461,8 @@ export type UpdateWalletAddressInput = { publicName?: InputMaybe; /** New status to set the wallet address to, either active or inactive. */ status?: InputMaybe; + /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature. */ + tenantId?: InputMaybe; }; export type UpdateWalletAddressMutationResponse = { diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 46dad2dedb..ad84539d43 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -373,6 +373,8 @@ export type CreateWalletAddressInput = { idempotencyKey?: InputMaybe; /** Public name associated with the wallet address. This is visible to anyone with the wallet address URL. */ publicName?: InputMaybe; + /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature. */ + tenantId?: InputMaybe; /** Wallet address URL. This cannot be changed. */ url: Scalars['String']['input']; }; @@ -1459,6 +1461,8 @@ export type UpdateWalletAddressInput = { publicName?: InputMaybe; /** New status to set the wallet address to, either active or inactive. */ status?: InputMaybe; + /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature. */ + tenantId?: InputMaybe; }; export type UpdateWalletAddressMutationResponse = { diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 46dad2dedb..ad84539d43 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -373,6 +373,8 @@ export type CreateWalletAddressInput = { idempotencyKey?: InputMaybe; /** Public name associated with the wallet address. This is visible to anyone with the wallet address URL. */ publicName?: InputMaybe; + /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature. */ + tenantId?: InputMaybe; /** Wallet address URL. This cannot be changed. */ url: Scalars['String']['input']; }; @@ -1459,6 +1461,8 @@ export type UpdateWalletAddressInput = { publicName?: InputMaybe; /** New status to set the wallet address to, either active or inactive. */ status?: InputMaybe; + /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature. */ + tenantId?: InputMaybe; }; export type UpdateWalletAddressMutationResponse = { From 86f86e689fd3632db1f065a57e6be6b0885c3787 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Sat, 11 Jan 2025 16:12:03 +0100 Subject: [PATCH 34/66] feat(3114): rework tenantId to not be mandatory for wallet address service layer. --- .../src/graphql/resolvers/wallet_address.ts | 80 +++++++++---------- 1 file changed, 36 insertions(+), 44 deletions(-) diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index d919d3247c..33a8b572c6 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -6,7 +6,8 @@ import { ResolversTypes, WalletAddress as SchemaWalletAddress, MutationResolvers, - WalletAddressStatus + WalletAddressStatus, + Receiver as SchemaReceiver } from '../generated/graphql' import { TenantedApolloContext } from '../../app' import { @@ -24,6 +25,7 @@ import { UpdateOptions } from '../../open_payments/wallet_address/service' import { tenantIdToProceed } from '../../shared/utils' +import { Receiver } from '../../open_payments/receiver/model' export const getWalletAddresses: QueryResolvers['walletAddresses'] = async ( @@ -48,7 +50,7 @@ export const getWalletAddresses: QueryResolvers['walletAd pageInfo, edges: walletAddresses.map((walletAddress: WalletAddress) => ({ cursor: walletAddress.id, - node: walletAddressToGraphql(walletAddress) + node: walletAddressToGraphql(walletAddress, ctx) })) } } @@ -67,20 +69,7 @@ export const getWalletAddress: QueryResolvers['walletAddr } ) } - const tenantId = tenantIdToProceed( - ctx.isOperator, - ctx.tenant.id, - walletAddress.tenantId - ) - if (!tenantId) { - const err = WalletAddressError.InvalidTenantIdNotAllowed - throw new GraphQLError(errorToMessage[err], { - extensions: { - code: errorToCode[err] - } - }) - } - return walletAddressToGraphql(walletAddress) + return walletAddressToGraphql(walletAddress, ctx) } export const getWalletAddressByUrl: QueryResolvers['walletAddressByUrl'] = @@ -91,22 +80,7 @@ export const getWalletAddressByUrl: QueryResolvers['walle ): Promise => { const walletAddressService = await ctx.container.use('walletAddressService') const walletAddress = await walletAddressService.getByUrl(args.url) - if (walletAddress) { - const tenantId = tenantIdToProceed( - ctx.isOperator, - ctx.tenant.id, - walletAddress.tenantId - ) - if (!tenantId) { - const err = WalletAddressError.InvalidTenantIdNotAllowed - throw new GraphQLError(errorToMessage[err], { - extensions: { - code: errorToCode[err] - } - }) - } - return walletAddressToGraphql(walletAddress) - } else return null + return walletAddress ? walletAddressToGraphql(walletAddress, ctx) : null } export const createWalletAddress: MutationResolvers['createWalletAddress'] = @@ -225,15 +199,33 @@ export const triggerWalletAddressEvents: MutationResolvers ({ - id: walletAddress.id, - url: walletAddress.url, - asset: assetToGraphql(walletAddress.asset), - publicName: walletAddress.publicName ?? undefined, - createdAt: new Date(+walletAddress.createdAt).toISOString(), - status: walletAddress.isActive - ? WalletAddressStatus.Active - : WalletAddressStatus.Inactive -}) +export function walletAddressToGraphql( + walletAddress: WalletAddress, + ctx?: TenantedApolloContext +): SchemaWalletAddress { + if (ctx) { + const tenantId = tenantIdToProceed( + ctx.isOperator, + ctx.tenant.id, + walletAddress.tenantId + ) + if (!tenantId) { + const err = WalletAddressError.InvalidTenantIdNotAllowed + throw new GraphQLError(errorToMessage[err], { + extensions: { + code: errorToCode[err] + } + }) + } + } + return { + id: walletAddress.id, + url: walletAddress.url, + asset: assetToGraphql(walletAddress.asset), + publicName: walletAddress.publicName ?? undefined, + createdAt: new Date(+walletAddress.createdAt).toISOString(), + status: walletAddress.isActive + ? WalletAddressStatus.Active + : WalletAddressStatus.Inactive + } +} From 78bd769b3e11120237b8e03716f1045c4aa1dc67 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Sat, 11 Jan 2025 16:18:11 +0100 Subject: [PATCH 35/66] feat(3114): rework tenantId to not be mandatory for wallet address service layer. --- packages/backend/src/graphql/resolvers/wallet_address.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index 33a8b572c6..db69afa9cd 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -6,8 +6,7 @@ import { ResolversTypes, WalletAddress as SchemaWalletAddress, MutationResolvers, - WalletAddressStatus, - Receiver as SchemaReceiver + WalletAddressStatus } from '../generated/graphql' import { TenantedApolloContext } from '../../app' import { @@ -25,7 +24,6 @@ import { UpdateOptions } from '../../open_payments/wallet_address/service' import { tenantIdToProceed } from '../../shared/utils' -import { Receiver } from '../../open_payments/receiver/model' export const getWalletAddresses: QueryResolvers['walletAddresses'] = async ( From 6defb40e0ed124711de405e7b2d10efc07640290 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Mon, 13 Jan 2025 12:45:11 +0100 Subject: [PATCH 36/66] feat(3114): fix tenant service test case. --- packages/backend/src/tenants/service.test.ts | 12 ++++++++++++ packages/backend/src/tests/tableManager.ts | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/tenants/service.test.ts b/packages/backend/src/tenants/service.test.ts index da6d3b7009..cb64cd5c0b 100644 --- a/packages/backend/src/tenants/service.test.ts +++ b/packages/backend/src/tenants/service.test.ts @@ -54,9 +54,21 @@ describe('Tenant Service', (): void => { afterEach(async (): Promise => { await truncateTables(appContainer.knex) + + const RAW = `TRUNCATE TABLE "${Tenant.tableName}" RESTART IDENTITY` + await knex.raw(RAW) }) afterAll(async (): Promise => { + const OPERATOR_TENANT_ID = process.env['OPERATOR_TENANT_ID'] + const OPERATOR_API_SECRET = process.env['API_SECRET'] + + knex.raw( + ` + INSERT INTO "tenants" ("id", "apiSecret") + VALUES ('${OPERATOR_TENANT_ID}', '${OPERATOR_API_SECRET}') + ` + ) nock.cleanAll() await appContainer.shutdown() }) diff --git a/packages/backend/src/tests/tableManager.ts b/packages/backend/src/tests/tableManager.ts index a279de224f..b44f830b58 100644 --- a/packages/backend/src/tests/tableManager.ts +++ b/packages/backend/src/tests/tableManager.ts @@ -1,4 +1,5 @@ import { Knex } from 'knex' +import { Tenant } from '../tenants/model' export async function truncateTable( knex: Knex, @@ -16,7 +17,7 @@ export async function truncateTables( 'knex_migrations_backend', 'knex_migrations_backend_lock', // We always keep the [cf5fd7d3-1eb1-4041-8e43-ba45747e9e5d] tenant for our test case. - 'tenants' + Tenant.tableName ] ): Promise { const tables = await getTables(knex, ignoreTables) From cede83c590ca8324e7d0d8563e242c81f842a787 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Mon, 13 Jan 2025 15:11:01 +0100 Subject: [PATCH 37/66] feat(3114): fix tenant service test case. --- .../migrations/20241203112902_add_tenant_to_wallet_address.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/migrations/20241203112902_add_tenant_to_wallet_address.js b/packages/backend/migrations/20241203112902_add_tenant_to_wallet_address.js index 9415acdfa4..fe069c35ce 100644 --- a/packages/backend/migrations/20241203112902_add_tenant_to_wallet_address.js +++ b/packages/backend/migrations/20241203112902_add_tenant_to_wallet_address.js @@ -5,8 +5,8 @@ exports.up = function (knex) { return Promise.all([ knex.schema.alterTable('walletAddresses', function (table) { - table.uuid('tenantId').index().notNullable() - //table.foreign(['tenantId']).references('tenants.id') + table.uuid('tenantId').notNullable() + table.foreign(['tenantId']).references('tenants.id') }) ]) } From c2caf6544c3bb75836e8269d9c35ab26b12f30d5 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Tue, 14 Jan 2025 11:21:59 +0100 Subject: [PATCH 38/66] feat(3114): fix tenant service test case. --- packages/backend/src/app.ts | 2 +- .../src/graphql/resolvers/wallet_address.ts | 48 ++----------------- packages/backend/src/shared/utils.ts | 26 +++++++++- 3 files changed, 30 insertions(+), 46 deletions(-) diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index b26f5e861c..4922c7c753 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -405,7 +405,7 @@ export class App { } else { tenantApiSignatureResult = { tenant: result.tenant, - isOperator: result.isOperator ? true : false + isOperator: result.isOperator } } return next() diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index db69afa9cd..558309ec60 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -23,7 +23,7 @@ import { CreateOptions, UpdateOptions } from '../../open_payments/wallet_address/service' -import { tenantIdToProceed } from '../../shared/utils' +import { tenantIdToUseAndValidate } from '../../shared/utils' export const getWalletAddresses: QueryResolvers['walletAddresses'] = async ( @@ -98,20 +98,7 @@ export const createWalletAddress: MutationResolvers['crea addProps.push(toAdd) }) - const tenantId = tenantIdToProceed( - ctx.isOperator, - ctx.tenant.id, - args.input.tenantId - ) - if (!tenantId) { - const err = WalletAddressError.InvalidTenantIdNotAllowed - throw new GraphQLError(errorToMessage[err], { - extensions: { - code: errorToCode[err] - } - }) - } - + const tenantId = tenantIdToUseAndValidate(ctx, args.input.tenantId) const options: CreateOptions = { assetId: args.input.assetId, tenantId, @@ -142,19 +129,7 @@ export const updateWalletAddress: MutationResolvers['upda const walletAddressService = await ctx.container.use('walletAddressService') const { additionalProperties, ...rest } = args.input - const tenantId = tenantIdToProceed( - ctx.isOperator, - ctx.tenant.id, - args.input.tenantId - ) - if (!tenantId) { - const err = WalletAddressError.InvalidTenantIdNotAllowed - throw new GraphQLError(errorToMessage[err], { - extensions: { - code: errorToCode[err] - } - }) - } + tenantIdToUseAndValidate(ctx, args.input.tenantId) const updateOptions: UpdateOptions = { ...rest @@ -201,21 +176,8 @@ export function walletAddressToGraphql( walletAddress: WalletAddress, ctx?: TenantedApolloContext ): SchemaWalletAddress { - if (ctx) { - const tenantId = tenantIdToProceed( - ctx.isOperator, - ctx.tenant.id, - walletAddress.tenantId - ) - if (!tenantId) { - const err = WalletAddressError.InvalidTenantIdNotAllowed - throw new GraphQLError(errorToMessage[err], { - extensions: { - code: errorToCode[err] - } - }) - } - } + if (ctx) tenantIdToUseAndValidate(ctx, walletAddress.tenantId) + return { id: walletAddress.id, url: walletAddress.url, diff --git a/packages/backend/src/shared/utils.ts b/packages/backend/src/shared/utils.ts index 9752705975..6fd9118a78 100644 --- a/packages/backend/src/shared/utils.ts +++ b/packages/backend/src/shared/utils.ts @@ -3,8 +3,14 @@ import { URL, type URL as URLType } from 'url' import { createHmac } from 'crypto' import { canonicalize } from 'json-canonicalize' import { IAppConfig } from '../config/app' -import { AppContext } from '../app' +import { AppContext, TenantedApolloContext } from '../app' import { Tenant } from '../tenants/model' +import { + errorToCode, + errorToMessage, + WalletAddressError +} from '../open_payments/wallet_address/errors' +import { GraphQLError } from 'graphql/index' export function validateId(id: string): boolean { return validate(id) && version(id) === 4 @@ -253,7 +259,7 @@ export function ensureTrailingSlash(str: string): string { * @param signatureTenantId the signature tenantId * @param tenantId the intended tenantId */ -export function tenantIdToProceed( +function tenantIdToProceed( isOperator: boolean, signatureTenantId: string, tenantId?: string @@ -264,3 +270,19 @@ export function tenantIdToProceed( ? undefined : signatureTenantId } + +export function tenantIdToUseAndValidate( + ctx: TenantedApolloContext, + tenantId?: string +): string { + const returnVal = tenantIdToProceed(ctx.isOperator, ctx.tenant.id, tenantId) + if (!returnVal) { + const err = WalletAddressError.InvalidTenantIdNotAllowed + throw new GraphQLError(errorToMessage[err], { + extensions: { + code: errorToCode[err] + } + }) + } + return returnVal +} From 8dfa1fb168ad1c4667dee6e693182c0c147c834d Mon Sep 17 00:00:00 2001 From: koekiebox Date: Wed, 15 Jan 2025 14:57:25 +0100 Subject: [PATCH 39/66] feat(3114): fix tenant service test case. --- packages/backend/src/app.ts | 9 ++- packages/backend/src/config/app.ts | 3 +- .../backend/src/graphql/middleware/index.ts | 20 ++++++ .../graphql/resolvers/wallet_address.test.ts | 71 ++++++++++++++++++- .../backend/src/middleware/tenant/index.ts | 58 +++++++++++++++ 5 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 packages/backend/src/middleware/tenant/index.ts diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index acb6e23068..98b5fd9273 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -70,7 +70,8 @@ import { applyMiddleware } from 'graphql-middleware' import { Redis } from 'ioredis' import { idempotencyGraphQLMiddleware, - lockGraphQLMutationMiddleware + lockGraphQLMutationMiddleware, + tenantValidateGraphQLMutationMiddleware } from './graphql/middleware' import { createRedisDataStore } from './middleware/cache/data-stores/redis' import { createRedisLock } from './middleware/lock/redis' @@ -335,7 +336,8 @@ export class App { ), idempotencyGraphQLMiddleware( createRedisDataStore(redis, this.config.graphQLIdempotencyKeyTtlMs) - ) + ), + tenantValidateGraphQLMutationMiddleware() ) // Setup Armor @@ -398,7 +400,8 @@ export class App { if (this.config.env === 'test') { const tenantService = await this.container.use('tenantService') const tenant = await tenantService.get(this.config.operatorTenantId) - tenantApiSignatureResult = { tenant, isOperator: true } + const isOperator = this.config.isTestTenantOperator + tenantApiSignatureResult = { tenant, isOperator } } else { koa.use(async (ctx, next: Koa.Next): Promise => { const result = await getTenantFromApiSignature(ctx, this.config) diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index 0d51cc501f..25e49fcfe8 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -197,7 +197,8 @@ export const Config = { 5 ), localCacheDuration: envInt('LOCAL_CACHE_DURATION_MS', 15_000), - operatorTenantId: envString('OPERATOR_TENANT_ID') + operatorTenantId: envString('OPERATOR_TENANT_ID'), + isTestTenantOperator: envBool('IS_TEST_TENANT_OPERATOR', true) } function parseRedisTlsConfig( diff --git a/packages/backend/src/graphql/middleware/index.ts b/packages/backend/src/graphql/middleware/index.ts index d8e490e4c4..5064299e44 100644 --- a/packages/backend/src/graphql/middleware/index.ts +++ b/packages/backend/src/graphql/middleware/index.ts @@ -4,6 +4,7 @@ import { ApolloContext } from '../../app' import { CacheDataStore } from '../../middleware/cache/data-stores' import { lockMiddleware, Lock } from '../../middleware/lock' import { cacheMiddleware } from '../../middleware/cache' +import { validateTenantMiddleware } from '../../middleware/tenant' export function lockGraphQLMutationMiddleware(lock: Lock): { Mutation: IMiddleware @@ -46,3 +47,22 @@ export function idempotencyGraphQLMiddleware( } } } + +export function tenantValidateGraphQLMutationMiddleware(): { + Mutation: IMiddleware +} { + return { + Mutation: async (resolve, root, args, context: ApolloContext, info) => { + return validateTenantMiddleware({ + deps: { ctx: context }, + next: () => resolve(root, args, context, info), + tenantIdInput: args?.input?.idempotencyKey, + onFailValidation: () => { + throw new GraphQLError( + `Assignment to the specified tenant is not permitted` + ) + } + }) + } + } +} diff --git a/packages/backend/src/graphql/resolvers/wallet_address.test.ts b/packages/backend/src/graphql/resolvers/wallet_address.test.ts index 7a46a91c2e..a583dd98b6 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.test.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.test.ts @@ -45,7 +45,8 @@ describe('Wallet Address Resolvers', (): void => { beforeAll(async (): Promise => { deps = initIocContainer({ ...Config, - localCacheDuration: 0 + localCacheDuration: 0, + isTestTenantOperator: false }) appContainer = await createTestApp(deps) knex = appContainer.knex @@ -306,6 +307,74 @@ describe('Wallet Address Resolvers', (): void => { ) } }) + + test('bad input data when not allowed to perform cross tenant create', async (): Promise => { + /*await appContainer.apolloClient.stop() + await appContainer.shutdown() + appContainer = await createTestApp( + initIocContainer({ + ...Config, + localCacheDuration: 0, + isTestTenantOperator: false + }) + ) + knex = appContainer.knex*/ + const badInputData = { + tenantId: 'ae4950b6-3e1b-4e50-ad24-25c065bdd3a9', + assetId: input.assetId, + url: input.url + } + try { + await appContainer.apolloClient + .mutate({ + mutation: gql` + mutation CreateWalletAddress( + $badInputData: CreateWalletAddressInput! + ) { + createWalletAddress(input: $badInputData) { + walletAddress { + id + asset { + code + scale + } + } + } + } + `, + variables: { + badInputData + } + }) + .then((query): CreateWalletAddressMutationResponse => { + if (query.data) { + return query.data.createWalletAddress + } else { + throw new Error('Data was empty') + } + }) + } catch (error) { + expect(error).toBeInstanceOf(ApolloError) + expect((error as ApolloError).graphQLErrors).toContainEqual( + expect.objectContaining({ + message: 'Assignment to the specified tenant is not permitted.', + extensions: expect.objectContaining({ + code: GraphQLErrorCode.BadUserInput + }) + }) + ) + } + + // Recover with default: + /*await appContainer.apolloClient.stop() + await appContainer.shutdown() + deps = initIocContainer({ + ...Config, + localCacheDuration: 0 + }) + appContainer = await createTestApp(deps) + knex = appContainer.knex*/ + }) }) describe('Update Wallet Address', (): void => { diff --git a/packages/backend/src/middleware/tenant/index.ts b/packages/backend/src/middleware/tenant/index.ts new file mode 100644 index 0000000000..fafa6d34ba --- /dev/null +++ b/packages/backend/src/middleware/tenant/index.ts @@ -0,0 +1,58 @@ +import { ApolloContext, TenantedApolloContext } from '../../app' + +type Request = () => Promise + +interface TenantValidateMiddlewareArgs { + deps: { ctx: ApolloContext } + tenantIdInput: string | undefined + onFailValidation: Request + next: Request +} + +export async function validateTenantMiddleware( + args: TenantValidateMiddlewareArgs +): ReturnType { + const { + deps: { ctx }, + tenantIdInput, + onFailValidation, + next + } = args + if (!('tenant' in ctx && 'isOperator' in ctx) || !tenantIdInput) return next() + + const tenantCtx = ctx as TenantedApolloContext + const returnVal = tenantIdToProceed( + tenantCtx.isOperator, + tenantCtx.tenant.id, + tenantIdInput + ) + if (!returnVal) { + ctx.logger.error('Tenant validation error') + return onFailValidation() + } + + return next() +} + +/** + * The tenantId to use will be determined as follows: + * - When an operator and the {tenantId} is present, return {tenantId} + * - When an operator and {tenantId} is not present, return {signatureTenantId} + * - When NOT an operator and {tenantId} is present, but does not match {signatureTenantId}, return {undefined} + * - Otherwise return {signatureTenantId} + * + * @param isOperator is operator + * @param signatureTenantId the signature tenantId + * @param tenantId the intended tenantId + */ +function tenantIdToProceed( + isOperator: boolean, + signatureTenantId: string, + tenantId?: string +): string | undefined { + if (isOperator && tenantId) return tenantId + else if (isOperator) return signatureTenantId + return tenantId && tenantId !== signatureTenantId + ? undefined + : signatureTenantId +} From 562d7e32568e9fe99bd026c8e5fe994b8ee29402 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Wed, 15 Jan 2025 16:21:23 +0100 Subject: [PATCH 40/66] feat(3114): fix tenant service test case. --- packages/backend/src/app.ts | 1 + packages/backend/src/graphql/middleware/index.ts | 10 ++++++++-- .../src/graphql/resolvers/wallet_address.test.ts | 3 ++- .../backend/src/graphql/resolvers/wallet_address.ts | 6 ++---- packages/backend/src/middleware/tenant/index.ts | 12 +++++++++--- .../src/open_payments/wallet_address/service.ts | 2 +- 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 98b5fd9273..1ab455790f 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -223,6 +223,7 @@ const WALLET_ADDRESS_PATH = '/:walletAddressPath+' export interface TenantedApolloContext extends ApolloContext { tenant: Tenant isOperator: boolean + forTenantId?: string } export interface AppServices { diff --git a/packages/backend/src/graphql/middleware/index.ts b/packages/backend/src/graphql/middleware/index.ts index 5064299e44..714854692d 100644 --- a/packages/backend/src/graphql/middleware/index.ts +++ b/packages/backend/src/graphql/middleware/index.ts @@ -5,6 +5,7 @@ import { CacheDataStore } from '../../middleware/cache/data-stores' import { lockMiddleware, Lock } from '../../middleware/lock' import { cacheMiddleware } from '../../middleware/cache' import { validateTenantMiddleware } from '../../middleware/tenant' +import { GraphQLErrorCode } from '../errors' export function lockGraphQLMutationMiddleware(lock: Lock): { Mutation: IMiddleware @@ -56,10 +57,15 @@ export function tenantValidateGraphQLMutationMiddleware(): { return validateTenantMiddleware({ deps: { ctx: context }, next: () => resolve(root, args, context, info), - tenantIdInput: args?.input?.idempotencyKey, + tenantIdInput: args?.input?.tenantId, onFailValidation: () => { throw new GraphQLError( - `Assignment to the specified tenant is not permitted` + `Assignment to the specified tenant is not permitted`, + { + extensions: { + code: GraphQLErrorCode.BadUserInput + } + } ) } }) diff --git a/packages/backend/src/graphql/resolvers/wallet_address.test.ts b/packages/backend/src/graphql/resolvers/wallet_address.test.ts index a583dd98b6..813e2af0bf 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.test.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.test.ts @@ -70,6 +70,7 @@ describe('Wallet Address Resolvers', (): void => { asset = await createAsset(deps) input = { assetId: asset.id, + tenantId: Config.operatorTenantId, url: 'https://alice.me/.well-known/pay' } }) @@ -357,7 +358,7 @@ describe('Wallet Address Resolvers', (): void => { expect(error).toBeInstanceOf(ApolloError) expect((error as ApolloError).graphQLErrors).toContainEqual( expect.objectContaining({ - message: 'Assignment to the specified tenant is not permitted.', + message: 'Assignment to the specified tenant is not permitted', extensions: expect.objectContaining({ code: GraphQLErrorCode.BadUserInput }) diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index 558309ec60..caf8e4da77 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -98,10 +98,10 @@ export const createWalletAddress: MutationResolvers['crea addProps.push(toAdd) }) - const tenantId = tenantIdToUseAndValidate(ctx, args.input.tenantId) const options: CreateOptions = { assetId: args.input.assetId, - tenantId, + // We always have a tenant for [TenantedApolloContext]. + tenantId: ctx.forTenantId!, additionalProperties: addProps, publicName: args.input.publicName, url: args.input.url @@ -129,8 +129,6 @@ export const updateWalletAddress: MutationResolvers['upda const walletAddressService = await ctx.container.use('walletAddressService') const { additionalProperties, ...rest } = args.input - tenantIdToUseAndValidate(ctx, args.input.tenantId) - const updateOptions: UpdateOptions = { ...rest } diff --git a/packages/backend/src/middleware/tenant/index.ts b/packages/backend/src/middleware/tenant/index.ts index fafa6d34ba..0181d06f91 100644 --- a/packages/backend/src/middleware/tenant/index.ts +++ b/packages/backend/src/middleware/tenant/index.ts @@ -18,19 +18,25 @@ export async function validateTenantMiddleware( onFailValidation, next } = args - if (!('tenant' in ctx && 'isOperator' in ctx) || !tenantIdInput) return next() + if (!('tenant' in ctx && 'isOperator' in ctx)) return next() const tenantCtx = ctx as TenantedApolloContext - const returnVal = tenantIdToProceed( + if (!tenantIdInput) { + tenantCtx.forTenantId = tenantCtx.tenant.id + return next() + } + + const forTenantId = tenantIdToProceed( tenantCtx.isOperator, tenantCtx.tenant.id, tenantIdInput ) - if (!returnVal) { + if (!forTenantId) { ctx.logger.error('Tenant validation error') return onFailValidation() } + tenantCtx.forTenantId = forTenantId return next() } diff --git a/packages/backend/src/open_payments/wallet_address/service.ts b/packages/backend/src/open_payments/wallet_address/service.ts index a4abd49f43..bfd22f2352 100644 --- a/packages/backend/src/open_payments/wallet_address/service.ts +++ b/packages/backend/src/open_payments/wallet_address/service.ts @@ -39,7 +39,7 @@ export type WalletAddressAdditionalPropertyInput = Pick< > export interface CreateOptions extends Options { - tenantId?: string + tenantId: string url: string assetId: string additionalProperties?: WalletAddressAdditionalPropertyInput[] From c1254f47bf436873529c7f335a0f14e5e3ce161a Mon Sep 17 00:00:00 2001 From: koekiebox Date: Wed, 15 Jan 2025 16:29:17 +0100 Subject: [PATCH 41/66] feat(3114): force 'forTenantId'. --- packages/backend/src/app.ts | 5 +++-- packages/backend/src/graphql/resolvers/wallet_address.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 1ab455790f..b76c0e8782 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -223,7 +223,7 @@ const WALLET_ADDRESS_PATH = '/:walletAddressPath+' export interface TenantedApolloContext extends ApolloContext { tenant: Tenant isOperator: boolean - forTenantId?: string + forTenantId: string } export interface AppServices { @@ -424,7 +424,8 @@ export class App { return { ...tenantApiSignatureResult, container: this.container, - logger: await this.container.use('logger') + logger: await this.container.use('logger'), + forTenantId: this.config.operatorTenantId } } }) diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index caf8e4da77..4818e13671 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -101,7 +101,7 @@ export const createWalletAddress: MutationResolvers['crea const options: CreateOptions = { assetId: args.input.assetId, // We always have a tenant for [TenantedApolloContext]. - tenantId: ctx.forTenantId!, + tenantId: ctx.forTenantId, additionalProperties: addProps, publicName: args.input.publicName, url: args.input.url From d4d48990072a51070d66a0ea7f906b48f9890604 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Fri, 17 Jan 2025 15:50:56 +0100 Subject: [PATCH 42/66] feat(3114): force 'forTenantId'. --- .../generated/graphql.ts | 2 - packages/backend/src/app.ts | 3 +- packages/backend/src/config/app.ts | 1 - .../src/graphql/generated/graphql.schema.json | 12 --- .../backend/src/graphql/generated/graphql.ts | 2 - .../graphql/resolvers/wallet_address.test.ts | 89 ++++++++++++++----- .../src/graphql/resolvers/wallet_address.ts | 47 +++++++--- packages/backend/src/graphql/schema.graphql | 2 - .../backend/src/middleware/tenant/index.ts | 24 +---- packages/backend/src/shared/utils.test.ts | 21 ++++- packages/backend/src/shared/utils.ts | 26 +----- packages/frontend/app/generated/graphql.ts | 2 - .../src/generated/graphql.ts | 2 - test/integration/lib/generated/graphql.ts | 2 - 14 files changed, 128 insertions(+), 107 deletions(-) diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index ad84539d43..8e67bbe7d7 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -1461,8 +1461,6 @@ export type UpdateWalletAddressInput = { publicName?: InputMaybe; /** New status to set the wallet address to, either active or inactive. */ status?: InputMaybe; - /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature. */ - tenantId?: InputMaybe; }; export type UpdateWalletAddressMutationResponse = { diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 9fcb3b2614..ea468e4cf7 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -453,7 +453,8 @@ export class App { return { ...tenantApiSignatureResult, container: this.container, - logger: await this.container.use('logger') + logger: await this.container.use('logger'), + forTenantId: this.config.operatorTenantId } } }) diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index 447c87ecf8..8b972cfe99 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -198,7 +198,6 @@ export const Config = { ), localCacheDuration: envInt('LOCAL_CACHE_DURATION_MS', 15_000), operatorTenantId: envString('OPERATOR_TENANT_ID'), - isTestTenantOperator: envBool('IS_TEST_TENANT_OPERATOR', true), dbSchema: undefined as string | undefined } diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index fd0e1cf0b3..1e4bd6584a 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -8081,18 +8081,6 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null - }, - { - "name": "tenantId", - "description": "Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null } ], "interfaces": null, diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index ad84539d43..8e67bbe7d7 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -1461,8 +1461,6 @@ export type UpdateWalletAddressInput = { publicName?: InputMaybe; /** New status to set the wallet address to, either active or inactive. */ status?: InputMaybe; - /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature. */ - tenantId?: InputMaybe; }; export type UpdateWalletAddressMutationResponse = { diff --git a/packages/backend/src/graphql/resolvers/wallet_address.test.ts b/packages/backend/src/graphql/resolvers/wallet_address.test.ts index 813e2af0bf..7ac9dc17c6 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.test.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.test.ts @@ -35,22 +35,27 @@ import { import { getPageTests } from './page.test' import { WalletAddressAdditionalProperty } from '../../open_payments/wallet_address/additional_property/model' import { GraphQLErrorCode } from '../errors' +import { AssetService } from '../../asset/service' +import { faker } from '@faker-js/faker' +import { Tenant } from '../../tenants/model' describe('Wallet Address Resolvers', (): void => { let deps: IocContract let appContainer: TestContainer let knex: Knex let walletAddressService: WalletAddressService + let assetService: AssetService beforeAll(async (): Promise => { deps = initIocContainer({ ...Config, localCacheDuration: 0, - isTestTenantOperator: false + adminApiSecret: '123' //to force not being an operator. }) appContainer = await createTestApp(deps) knex = appContainer.knex walletAddressService = await deps.use('walletAddressService') + assetService = await deps.use('assetService') }) afterEach(async (): Promise => { @@ -310,16 +315,6 @@ describe('Wallet Address Resolvers', (): void => { }) test('bad input data when not allowed to perform cross tenant create', async (): Promise => { - /*await appContainer.apolloClient.stop() - await appContainer.shutdown() - appContainer = await createTestApp( - initIocContainer({ - ...Config, - localCacheDuration: 0, - isTestTenantOperator: false - }) - ) - knex = appContainer.knex*/ const badInputData = { tenantId: 'ae4950b6-3e1b-4e50-ad24-25c065bdd3a9', assetId: input.assetId, @@ -365,16 +360,6 @@ describe('Wallet Address Resolvers', (): void => { }) ) } - - // Recover with default: - /*await appContainer.apolloClient.stop() - await appContainer.shutdown() - deps = initIocContainer({ - ...Config, - localCacheDuration: 0 - }) - appContainer = await createTestApp(deps) - knex = appContainer.knex*/ }) }) @@ -708,6 +693,68 @@ describe('Wallet Address Resolvers', (): void => { ) } }) + + test('bad input data when not allowed to perform cross tenant update', async (): Promise => { + try { + const tenantOptions = { + apiSecret: 'test-api-secret-new', + publicName: 'test tenant new', + email: faker.internet.email(), + idpConsentUrl: faker.internet.url(), + idpSecret: 'test-idp-secret-new' + } + const newTenant = await Tenant.query(knex).insertAndFetch(tenantOptions) + + const newAsset = await assetService.create({ + code: 'USD', + scale: 2, + tenantId: newTenant!.id + }) + const newWalletAddress = await walletAddressService.create({ + assetId: (newAsset as Asset).id, + tenantId: newTenant!.id, + url: 'https://alice.me/.well-known/pay-2' + }) + const id = (newWalletAddress as WalletAddressModel).id + + await appContainer.apolloClient + .mutate({ + mutation: gql` + mutation UpdateWalletAddress($input: UpdateWalletAddressInput!) { + updateWalletAddress(input: $input) { + walletAddress { + id + status + } + } + } + `, + variables: { + input: { + id, + status: WalletAddressStatus.Inactive + } + } + }) + .then((query): UpdateWalletAddressMutationResponse => { + if (query.data) { + return query.data.updateWalletAddress + } else { + throw new Error('Data was empty') + } + }) + } catch (error) { + expect(error).toBeInstanceOf(ApolloError) + expect((error as ApolloError).graphQLErrors).toContainEqual( + expect.objectContaining({ + message: 'Unknown wallet address', + extensions: expect.objectContaining({ + code: GraphQLErrorCode.NotFound + }) + }) + ) + } + }) }) describe('Wallet Address Queries', (): void => { diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index 4818e13671..74f86b0446 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -23,7 +23,8 @@ import { CreateOptions, UpdateOptions } from '../../open_payments/wallet_address/service' -import { tenantIdToUseAndValidate } from '../../shared/utils' +import { tenantIdToProceed } from '../../shared/utils' +import { GraphQLErrorCode } from '../errors' export const getWalletAddresses: QueryResolvers['walletAddresses'] = async ( @@ -46,10 +47,16 @@ export const getWalletAddresses: QueryResolvers['walletAd }) return { pageInfo, - edges: walletAddresses.map((walletAddress: WalletAddress) => ({ - cursor: walletAddress.id, - node: walletAddressToGraphql(walletAddress, ctx) - })) + edges: walletAddresses + .filter( + (wa: WalletAddress) => + tenantIdToProceed(ctx.isOperator, ctx.tenant.id, wa.tenantId) != + undefined + ) + .map((walletAddress: WalletAddress) => ({ + cursor: walletAddress.id, + node: walletAddressToGraphql(walletAddress) + })) } } @@ -57,7 +64,10 @@ export const getWalletAddress: QueryResolvers['walletAddr async (parent, args, ctx): Promise => { const walletAddressService = await ctx.container.use('walletAddressService') const walletAddress = await walletAddressService.get(args.id) - if (!walletAddress) { + if ( + !walletAddress || + !tenantIdToProceed(ctx.isOperator, ctx.tenant.id, walletAddress.tenantId) + ) { throw new GraphQLError( errorToMessage[WalletAddressError.UnknownWalletAddress], { @@ -67,7 +77,7 @@ export const getWalletAddress: QueryResolvers['walletAddr } ) } - return walletAddressToGraphql(walletAddress, ctx) + return walletAddressToGraphql(walletAddress) } export const getWalletAddressByUrl: QueryResolvers['walletAddressByUrl'] = @@ -78,7 +88,10 @@ export const getWalletAddressByUrl: QueryResolvers['walle ): Promise => { const walletAddressService = await ctx.container.use('walletAddressService') const walletAddress = await walletAddressService.getByUrl(args.url) - return walletAddress ? walletAddressToGraphql(walletAddress, ctx) : null + return walletAddress && + tenantIdToProceed(ctx.isOperator, ctx.tenant.id, walletAddress.tenantId) + ? walletAddressToGraphql(walletAddress) + : null } export const createWalletAddress: MutationResolvers['createWalletAddress'] = @@ -132,6 +145,19 @@ export const updateWalletAddress: MutationResolvers['upda const updateOptions: UpdateOptions = { ...rest } + + const existing = await walletAddressService.get(updateOptions.id) + if ( + existing && + !tenantIdToProceed(ctx.isOperator, ctx.forTenantId, existing.tenantId) + ) { + throw new GraphQLError(`Unknown wallet address`, { + extensions: { + code: GraphQLErrorCode.NotFound + } + }) + } + if (additionalProperties) { updateOptions.additionalProperties = additionalProperties.map( (property) => { @@ -171,11 +197,8 @@ export const triggerWalletAddressEvents: MutationResolvers Promise @@ -39,26 +40,3 @@ export async function validateTenantMiddleware( tenantCtx.forTenantId = forTenantId return next() } - -/** - * The tenantId to use will be determined as follows: - * - When an operator and the {tenantId} is present, return {tenantId} - * - When an operator and {tenantId} is not present, return {signatureTenantId} - * - When NOT an operator and {tenantId} is present, but does not match {signatureTenantId}, return {undefined} - * - Otherwise return {signatureTenantId} - * - * @param isOperator is operator - * @param signatureTenantId the signature tenantId - * @param tenantId the intended tenantId - */ -function tenantIdToProceed( - isOperator: boolean, - signatureTenantId: string, - tenantId?: string -): string | undefined { - if (isOperator && tenantId) return tenantId - else if (isOperator) return signatureTenantId - return tenantId && tenantId !== signatureTenantId - ? undefined - : signatureTenantId -} diff --git a/packages/backend/src/shared/utils.test.ts b/packages/backend/src/shared/utils.test.ts index 409f194c4e..5fa16ac9ac 100644 --- a/packages/backend/src/shared/utils.test.ts +++ b/packages/backend/src/shared/utils.test.ts @@ -9,7 +9,9 @@ import { poll, requestWithTimeout, sleep, - getTenantFromApiSignature + getTenantFromApiSignature, + ensureTrailingSlash, + tenantIdToProceed } from './utils' import { AppServices, AppContext } from '../app' import { TestContainer, createTestApp } from '../tests/app' @@ -444,4 +446,21 @@ describe('utils', (): void => { expect(getSpy).toHaveBeenCalled() }) }) + + test('test ensuring trailing slash', async (): Promise => { + const path = '/utils' + + expect(ensureTrailingSlash(path)).toBe(`${path}/`) + expect(ensureTrailingSlash(`${path}/`)).toBe(`${path}/`) + }) + + test('test tenant id to proceed', async (): Promise => { + const sig = 'sig' + const tenantId = 'tenantId' + expect(tenantIdToProceed(false, sig)).toBe(sig) + expect(tenantIdToProceed(false, sig, tenantId)).toBeUndefined() + expect(tenantIdToProceed(false, sig, sig)).toBe(sig) + expect(tenantIdToProceed(true, sig)).toBe(sig) + expect(tenantIdToProceed(true, sig, tenantId)).toBe(tenantId) + }) }) diff --git a/packages/backend/src/shared/utils.ts b/packages/backend/src/shared/utils.ts index 832c945aad..29b0ce0a25 100644 --- a/packages/backend/src/shared/utils.ts +++ b/packages/backend/src/shared/utils.ts @@ -3,14 +3,8 @@ import { URL, type URL as URLType } from 'url' import { createHmac } from 'crypto' import { canonicalize } from 'json-canonicalize' import { IAppConfig } from '../config/app' -import { AppContext, TenantedApolloContext } from '../app' +import { AppContext } from '../app' import { Tenant } from '../tenants/model' -import { - errorToCode, - errorToMessage, - WalletAddressError -} from '../open_payments/wallet_address/errors' -import { GraphQLError } from 'graphql/index' export function validateId(id: string): boolean { return validate(id) && version(id) === 4 @@ -259,7 +253,7 @@ export function ensureTrailingSlash(str: string): string { * @param signatureTenantId the signature tenantId * @param tenantId the intended tenantId */ -function tenantIdToProceed( +export function tenantIdToProceed( isOperator: boolean, signatureTenantId: string, tenantId?: string @@ -270,19 +264,3 @@ function tenantIdToProceed( ? undefined : signatureTenantId } - -export function tenantIdToUseAndValidate( - ctx: TenantedApolloContext, - tenantId?: string -): string { - const returnVal = tenantIdToProceed(ctx.isOperator, ctx.tenant.id, tenantId) - if (!returnVal) { - const err = WalletAddressError.InvalidTenantIdNotAllowed - throw new GraphQLError(errorToMessage[err], { - extensions: { - code: errorToCode[err] - } - }) - } - return returnVal -} diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index 7a5fd40ea0..835e75a512 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -1461,8 +1461,6 @@ export type UpdateWalletAddressInput = { publicName?: InputMaybe; /** New status to set the wallet address to, either active or inactive. */ status?: InputMaybe; - /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature. */ - tenantId?: InputMaybe; }; export type UpdateWalletAddressMutationResponse = { diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index ad84539d43..8e67bbe7d7 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -1461,8 +1461,6 @@ export type UpdateWalletAddressInput = { publicName?: InputMaybe; /** New status to set the wallet address to, either active or inactive. */ status?: InputMaybe; - /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature. */ - tenantId?: InputMaybe; }; export type UpdateWalletAddressMutationResponse = { diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index ad84539d43..8e67bbe7d7 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -1461,8 +1461,6 @@ export type UpdateWalletAddressInput = { publicName?: InputMaybe; /** New status to set the wallet address to, either active or inactive. */ status?: InputMaybe; - /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature. */ - tenantId?: InputMaybe; }; export type UpdateWalletAddressMutationResponse = { From 60672bad2c4a9fe2a8a8fae0c60308d9d0352ef9 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Fri, 17 Jan 2025 16:08:23 +0100 Subject: [PATCH 43/66] feat(3114): force 'forTenantId'. --- ...1203112902_add_tenant_to_wallet_address.js | 25 --------------- ...0117112902_add_tenant_to_wallet_address.js | 32 +++++++++++++++++++ .../open_payments/wallet_address/service.ts | 14 ++++---- 3 files changed, 38 insertions(+), 33 deletions(-) delete mode 100644 packages/backend/migrations/20241203112902_add_tenant_to_wallet_address.js create mode 100644 packages/backend/migrations/20250117112902_add_tenant_to_wallet_address.js diff --git a/packages/backend/migrations/20241203112902_add_tenant_to_wallet_address.js b/packages/backend/migrations/20241203112902_add_tenant_to_wallet_address.js deleted file mode 100644 index fe069c35ce..0000000000 --- a/packages/backend/migrations/20241203112902_add_tenant_to_wallet_address.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @param { import("knex").Knex } knex - * @returns { Promise } - */ -exports.up = function (knex) { - return Promise.all([ - knex.schema.alterTable('walletAddresses', function (table) { - table.uuid('tenantId').notNullable() - table.foreign(['tenantId']).references('tenants.id') - }) - ]) -} - -/** - * @param { import("knex").Knex } knex - * @returns { Promise } - */ -exports.down = function (knex) { - return Promise.all([ - knex.schema.alterTable('walletAddresses', function (table) { - table.dropIndex('tenantId') - table.dropColumn('tenantId') - }) - ]) -} diff --git a/packages/backend/migrations/20250117112902_add_tenant_to_wallet_address.js b/packages/backend/migrations/20250117112902_add_tenant_to_wallet_address.js new file mode 100644 index 0000000000..a3f21e9904 --- /dev/null +++ b/packages/backend/migrations/20250117112902_add_tenant_to_wallet_address.js @@ -0,0 +1,32 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema + .alterTable('walletAddresses', (table) => { + table.uuid('tenantId').references('tenants.id').index() + }) + .then(() => { + return knex.raw( + `UPDATE "walletAddresses" SET "tenantId" = (SELECT id from "tenants" LIMIT 1)` + ) + }) + .then(() => { + return knex.schema.alterTable('walletAddresses', (table) => { + table.uuid('tenantId').notNullable().alter() + }) + }) +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return Promise.all([ + knex.schema.alterTable('walletAddresses', function (table) { + table.dropColumn('tenantId') + }) + ]) +} diff --git a/packages/backend/src/open_payments/wallet_address/service.ts b/packages/backend/src/open_payments/wallet_address/service.ts index bfd22f2352..ee187b60f9 100644 --- a/packages/backend/src/open_payments/wallet_address/service.ts +++ b/packages/backend/src/open_payments/wallet_address/service.ts @@ -169,26 +169,24 @@ async function createWalletAddress( } try { + const asset = await deps.assetService.get(options.assetId, options.tenantId) + if (!asset) return WalletAddressError.UnknownAsset + // Remove blank key/value pairs: const additionalProperties = options.additionalProperties ? cleanAdditionalProperties(options.additionalProperties) : undefined - const tenantId = options.tenantId - ? options.tenantId - : deps.config.operatorTenantId - const walletAddress = await WalletAddress.query( deps.knex ).insertGraphAndFetch({ - tenantId, + tenantId: options.tenantId, url: options.url.toLowerCase(), publicName: options.publicName, - assetId: options.assetId, + assetId: asset.id, additionalProperties: additionalProperties }) - const asset = await deps.assetService.get(walletAddress.assetId) - if (asset) walletAddress.asset = asset + walletAddress.asset = asset await deps.walletAddressCache.set(walletAddress.id, walletAddress) return walletAddress From 094271f1f10357f27e01f40dd8e00dee6458ee98 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Fri, 17 Jan 2025 16:51:00 +0100 Subject: [PATCH 44/66] feat(3114): enhancements for 'forTenantId'. --- packages/backend/src/app.ts | 9 ++++++--- .../backend/src/graphql/middleware/index.ts | 16 ++++++++++----- .../src/graphql/resolvers/wallet_address.ts | 16 +++++++-------- .../backend/src/middleware/tenant/index.ts | 20 ++++++++----------- 4 files changed, 33 insertions(+), 28 deletions(-) diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index ea468e4cf7..b77adcc4b7 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -71,7 +71,7 @@ import { Redis } from 'ioredis' import { idempotencyGraphQLMiddleware, lockGraphQLMutationMiddleware, - tenantValidateGraphQLMutationMiddleware + setForTenantIdGraphQLMutationMiddleware } from './graphql/middleware' import { createRedisDataStore } from './middleware/cache/data-stores/redis' import { createRedisLock } from './middleware/lock/redis' @@ -225,6 +225,9 @@ const WALLET_ADDRESS_PATH = '/:walletAddressPath+' export interface TenantedApolloContext extends ApolloContext { tenant: Tenant isOperator: boolean +} + +export interface ForTenantIdContext extends TenantedApolloContext { forTenantId: string } @@ -341,7 +344,7 @@ export class App { idempotencyGraphQLMiddleware( createRedisDataStore(redis, this.config.graphQLIdempotencyKeyTtlMs) ), - tenantValidateGraphQLMutationMiddleware() + setForTenantIdGraphQLMutationMiddleware() ) // Setup Armor @@ -449,7 +452,7 @@ export class App { koa.use( koaMiddleware(this.apolloServer, { - context: async (): Promise => { + context: async (): Promise => { return { ...tenantApiSignatureResult, container: this.container, diff --git a/packages/backend/src/graphql/middleware/index.ts b/packages/backend/src/graphql/middleware/index.ts index 714854692d..3adda300d1 100644 --- a/packages/backend/src/graphql/middleware/index.ts +++ b/packages/backend/src/graphql/middleware/index.ts @@ -1,6 +1,6 @@ import { GraphQLError } from 'graphql' import { IMiddleware } from 'graphql-middleware' -import { ApolloContext } from '../../app' +import { ApolloContext, ForTenantIdContext } from '../../app' import { CacheDataStore } from '../../middleware/cache/data-stores' import { lockMiddleware, Lock } from '../../middleware/lock' import { cacheMiddleware } from '../../middleware/cache' @@ -49,13 +49,19 @@ export function idempotencyGraphQLMiddleware( } } -export function tenantValidateGraphQLMutationMiddleware(): { - Mutation: IMiddleware +export function setForTenantIdGraphQLMutationMiddleware(): { + Mutation: IMiddleware } { return { - Mutation: async (resolve, root, args, context: ApolloContext, info) => { + Mutation: async ( + resolve, + root, + args, + context: ForTenantIdContext, + info + ) => { return validateTenantMiddleware({ - deps: { ctx: context }, + deps: { context }, next: () => resolve(root, args, context, info), tenantIdInput: args?.input?.tenantId, onFailValidation: () => { diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index 74f86b0446..e15884130e 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -8,7 +8,7 @@ import { MutationResolvers, WalletAddressStatus } from '../generated/graphql' -import { TenantedApolloContext } from '../../app' +import { ForTenantIdContext } from '../../app' import { WalletAddressError, isWalletAddressError, @@ -26,7 +26,7 @@ import { import { tenantIdToProceed } from '../../shared/utils' import { GraphQLErrorCode } from '../errors' -export const getWalletAddresses: QueryResolvers['walletAddresses'] = +export const getWalletAddresses: QueryResolvers['walletAddresses'] = async ( parent, args, @@ -60,7 +60,7 @@ export const getWalletAddresses: QueryResolvers['walletAd } } -export const getWalletAddress: QueryResolvers['walletAddress'] = +export const getWalletAddress: QueryResolvers['walletAddress'] = async (parent, args, ctx): Promise => { const walletAddressService = await ctx.container.use('walletAddressService') const walletAddress = await walletAddressService.get(args.id) @@ -80,7 +80,7 @@ export const getWalletAddress: QueryResolvers['walletAddr return walletAddressToGraphql(walletAddress) } -export const getWalletAddressByUrl: QueryResolvers['walletAddressByUrl'] = +export const getWalletAddressByUrl: QueryResolvers['walletAddressByUrl'] = async ( parent, args, @@ -94,7 +94,7 @@ export const getWalletAddressByUrl: QueryResolvers['walle : null } -export const createWalletAddress: MutationResolvers['createWalletAddress'] = +export const createWalletAddress: MutationResolvers['createWalletAddress'] = async ( parent, args, @@ -113,7 +113,7 @@ export const createWalletAddress: MutationResolvers['crea const options: CreateOptions = { assetId: args.input.assetId, - // We always have a tenant for [TenantedApolloContext]. + // We always have a tenant for [ForTenantIdContext]. tenantId: ctx.forTenantId, additionalProperties: addProps, publicName: args.input.publicName, @@ -133,7 +133,7 @@ export const createWalletAddress: MutationResolvers['crea } } -export const updateWalletAddress: MutationResolvers['updateWalletAddress'] = +export const updateWalletAddress: MutationResolvers['updateWalletAddress'] = async ( parent, args, @@ -183,7 +183,7 @@ export const updateWalletAddress: MutationResolvers['upda } } -export const triggerWalletAddressEvents: MutationResolvers['triggerWalletAddressEvents'] = +export const triggerWalletAddressEvents: MutationResolvers['triggerWalletAddressEvents'] = async ( parent, args, diff --git a/packages/backend/src/middleware/tenant/index.ts b/packages/backend/src/middleware/tenant/index.ts index 347f961051..cdb3fe130e 100644 --- a/packages/backend/src/middleware/tenant/index.ts +++ b/packages/backend/src/middleware/tenant/index.ts @@ -1,10 +1,10 @@ -import { ApolloContext, TenantedApolloContext } from '../../app' +import { ForTenantIdContext } from '../../app' import { tenantIdToProceed } from '../../shared/utils' type Request = () => Promise interface TenantValidateMiddlewareArgs { - deps: { ctx: ApolloContext } + deps: { context: ForTenantIdContext } tenantIdInput: string | undefined onFailValidation: Request next: Request @@ -14,29 +14,25 @@ export async function validateTenantMiddleware( args: TenantValidateMiddlewareArgs ): ReturnType { const { - deps: { ctx }, + deps: { context }, tenantIdInput, onFailValidation, next } = args - if (!('tenant' in ctx && 'isOperator' in ctx)) return next() - - const tenantCtx = ctx as TenantedApolloContext if (!tenantIdInput) { - tenantCtx.forTenantId = tenantCtx.tenant.id + context.forTenantId = context.tenant.id return next() } const forTenantId = tenantIdToProceed( - tenantCtx.isOperator, - tenantCtx.tenant.id, + context.isOperator, + context.tenant.id, tenantIdInput ) if (!forTenantId) { - ctx.logger.error('Tenant validation error') + context.logger.error('Tenant validation error') return onFailValidation() } - - tenantCtx.forTenantId = forTenantId + context.forTenantId = forTenantId return next() } From 6707b04adc20109a344e6e07c7e46f266e0c7ce2 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Fri, 17 Jan 2025 18:41:35 +0100 Subject: [PATCH 45/66] feat(3114): test case fixes. --- .../src/open_payments/payment/combined/service.test.ts | 3 ++- .../src/open_payments/wallet_address/service.test.ts | 2 +- .../backend/src/open_payments/wallet_address/service.ts | 5 ++++- packages/backend/src/tests/asset.ts | 5 +++-- packages/backend/src/tests/combinedPayment.ts | 8 ++++++-- packages/backend/src/tests/walletAddress.ts | 4 +++- 6 files changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/open_payments/payment/combined/service.test.ts b/packages/backend/src/open_payments/payment/combined/service.test.ts index 049f1b7661..d9907f6084 100644 --- a/packages/backend/src/open_payments/payment/combined/service.test.ts +++ b/packages/backend/src/open_payments/payment/combined/service.test.ts @@ -44,11 +44,12 @@ describe('Combined Payment Service', (): void => { receiveAsset = await createAsset(deps) sendWalletAddressId = ( await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId: sendAsset.tenantId, assetId: sendAsset.id }) ).id receiveWalletAddress = await createWalletAddress(deps, { + tenantId: sendAsset.tenantId, assetId: receiveAsset.id }) }) diff --git a/packages/backend/src/open_payments/wallet_address/service.test.ts b/packages/backend/src/open_payments/wallet_address/service.test.ts index 0477ecab48..cceeaba6bb 100644 --- a/packages/backend/src/open_payments/wallet_address/service.test.ts +++ b/packages/backend/src/open_payments/wallet_address/service.test.ts @@ -61,8 +61,8 @@ describe('Open Payments Wallet Address Service', (): void => { let options: CreateOptions beforeEach(async (): Promise => { - const { id: assetId } = await createAsset(deps) const { id: tenantId } = await createTenant(deps) + const { id: assetId } = await createAsset(deps, undefined, tenantId) options = { url: 'https://alice.me/.well-known/pay', assetId, diff --git a/packages/backend/src/open_payments/wallet_address/service.ts b/packages/backend/src/open_payments/wallet_address/service.ts index ee187b60f9..38bb777cdc 100644 --- a/packages/backend/src/open_payments/wallet_address/service.ts +++ b/packages/backend/src/open_payments/wallet_address/service.ts @@ -169,7 +169,10 @@ async function createWalletAddress( } try { - const asset = await deps.assetService.get(options.assetId, options.tenantId) + const tenantId = options.tenantId + ? options.tenantId + : deps.config.operatorTenantId + const asset = await deps.assetService.get(options.assetId, tenantId) if (!asset) return WalletAddressError.UnknownAsset // Remove blank key/value pairs: diff --git a/packages/backend/src/tests/asset.ts b/packages/backend/src/tests/asset.ts index d77fd1655b..63dd3994bc 100644 --- a/packages/backend/src/tests/asset.ts +++ b/packages/backend/src/tests/asset.ts @@ -25,14 +25,15 @@ export function randomLedger(): number { export async function createAsset( deps: IocContract, - options?: AssetOptions + options?: AssetOptions, + tenantId?: string ): Promise { const config = await deps.use('config') const assetService = await deps.use('assetService') const createOptions = options || randomAsset() const assetOrError = await assetService.create({ ...createOptions, - tenantId: config.operatorTenantId + tenantId: tenantId ? tenantId : config.operatorTenantId }) if (isAssetError(assetOrError)) { throw assetOrError diff --git a/packages/backend/src/tests/combinedPayment.ts b/packages/backend/src/tests/combinedPayment.ts index 504aceeb97..bb258e72a3 100644 --- a/packages/backend/src/tests/combinedPayment.ts +++ b/packages/backend/src/tests/combinedPayment.ts @@ -40,10 +40,14 @@ export async function createCombinedPayment( const sendAsset = await createAsset(deps) const receiveAsset = await createAsset(deps) const sendWalletAddressId = ( - await createWalletAddress(deps, { assetId: sendAsset.id }) + await createWalletAddress(deps, { + assetId: sendAsset.id, + tenantId: sendAsset.tenantId + }) ).id const receiveWalletAddress = await createWalletAddress(deps, { - assetId: receiveAsset.id + assetId: receiveAsset.id, + tenantId: sendAsset.tenantId }) const type = Math.random() < 0.5 ? PaymentType.Incoming : PaymentType.Outgoing diff --git a/packages/backend/src/tests/walletAddress.ts b/packages/backend/src/tests/walletAddress.ts index cf459ed6a8..3c319bff4d 100644 --- a/packages/backend/src/tests/walletAddress.ts +++ b/packages/backend/src/tests/walletAddress.ts @@ -32,7 +32,9 @@ export async function createWalletAddress( const walletAddressService = await deps.use('walletAddressService') const walletAddressOrError = (await walletAddressService.create({ ...options, - assetId: options.assetId || (await createAsset(deps)).id, + assetId: + options.assetId || + (await createAsset(deps, undefined, options.tenantId)).id, tenantId: options.tenantId || (await createTenant(deps)).id, url: options.url || `https://${faker.internet.domainName()}/.well-known/pay` })) as MockWalletAddress From f824b26aab92e72f56b22831662960f29a17e30f Mon Sep 17 00:00:00 2001 From: koekiebox Date: Fri, 17 Jan 2025 19:37:12 +0100 Subject: [PATCH 46/66] feat(3114): internal server error --- .../backend/src/graphql/resolvers/walletAddressKey.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts b/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts index 9c51f9b594..1573cb5027 100644 --- a/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts +++ b/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts @@ -20,6 +20,7 @@ import { createWalletAddress } from '../../tests/walletAddress' import { getPageTests } from './page.test' import { createWalletAddressKey } from '../../tests/walletAddressKey' import { GraphQLErrorCode } from '../errors' +import { knex } from 'knex' const TEST_KEY = generateJwk({ keyId: uuid() }) @@ -100,6 +101,7 @@ describe('Wallet Address Key Resolvers', (): void => { revoked: false }) }) + /* test('internal server error', async (): Promise => { jest @@ -163,7 +165,7 @@ describe('Wallet Address Key Resolvers', (): void => { }) ) } - }) + })*/ }) describe('Revoke key', (): void => { From a3af5ea29fb04a36bfc2274e36e272ae2cec80e3 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Mon, 20 Jan 2025 11:52:06 +0100 Subject: [PATCH 47/66] feat(3114): test case. --- .../backend/src/graphql/resolvers/walletAddressKey.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts b/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts index 046da45304..3736d63e5f 100644 --- a/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts +++ b/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts @@ -26,7 +26,6 @@ import { isWalletAddressKeyError, WalletAddressKeyError } from '../../open_payments/wallet_address/key/errors' -import { knex } from 'knex' const TEST_KEY = generateJwk({ keyId: uuid() }) @@ -107,8 +106,6 @@ describe('Wallet Address Key Resolvers', (): void => { revoked: false }) }) - /* - test('Cannot add duplicate key', async (): Promise => { const walletAddress = await createWalletAddress(deps) @@ -231,7 +228,7 @@ describe('Wallet Address Key Resolvers', (): void => { }) ) } - })*/ + }) }) describe('Revoke key', (): void => { From c799815b45db4da08e41197cf5ec892cf86c0db9 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Mon, 20 Jan 2025 12:41:06 +0100 Subject: [PATCH 48/66] feat(3114): test case. --- .../backend/src/graphql/resolvers/walletAddressKey.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts b/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts index 3736d63e5f..848def9e7a 100644 --- a/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts +++ b/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts @@ -107,7 +107,10 @@ describe('Wallet Address Key Resolvers', (): void => { }) }) test('Cannot add duplicate key', async (): Promise => { - const walletAddress = await createWalletAddress(deps) + const walletAddress = await createWalletAddress(deps, { + tenantId: Config.operatorTenantId + } + ) const input: CreateWalletAddressKeyInput = { walletAddressId: walletAddress.id, From aaf06b63486851eb28e641a152f103d1a4bf2fbb Mon Sep 17 00:00:00 2001 From: koekiebox Date: Mon, 20 Jan 2025 12:54:10 +0100 Subject: [PATCH 49/66] feat(3114): review feedback. --- packages/backend/src/graphql/resolvers/wallet_address.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend/src/graphql/resolvers/wallet_address.test.ts b/packages/backend/src/graphql/resolvers/wallet_address.test.ts index 7ac9dc17c6..2a469d3635 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.test.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.test.ts @@ -321,6 +321,7 @@ describe('Wallet Address Resolvers', (): void => { url: input.url } try { + expect.assertions(2) await appContainer.apolloClient .mutate({ mutation: gql` From 87f9ae68dbd0ab074283939b13f81ad93cdd4049 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Mon, 20 Jan 2025 13:18:37 +0100 Subject: [PATCH 50/66] feat(3114): review feedback. --- .../backend/src/graphql/resolvers/walletAddressKey.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts b/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts index 848def9e7a..fc4400f9af 100644 --- a/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts +++ b/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts @@ -108,9 +108,8 @@ describe('Wallet Address Key Resolvers', (): void => { }) test('Cannot add duplicate key', async (): Promise => { const walletAddress = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId - } - ) + tenantId: Config.operatorTenantId + }) const input: CreateWalletAddressKeyInput = { walletAddressId: walletAddress.id, From b028de73779f9f477e3dc94185c7222af9c9b173 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Mon, 20 Jan 2025 13:55:19 +0100 Subject: [PATCH 51/66] feat(3114): remove tenant on quote. --- packages/backend/src/tests/quote.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend/src/tests/quote.ts b/packages/backend/src/tests/quote.ts index 714a55e3c8..26c0928619 100644 --- a/packages/backend/src/tests/quote.ts +++ b/packages/backend/src/tests/quote.ts @@ -168,7 +168,6 @@ export async function createQuote( const withGraphFetchedArray = [ 'asset', 'walletAddress', - 'walletAddress.tenant', 'walletAddress.asset' ] if (withFee) { From 27c43524f1b7de81ef2f48a6deba43c4ecdb693b Mon Sep 17 00:00:00 2001 From: koekiebox Date: Mon, 20 Jan 2025 14:10:37 +0100 Subject: [PATCH 52/66] feat(3114): default operator tenant. --- .../app/lib/parse_config.server.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/localenv/mock-account-servicing-entity/app/lib/parse_config.server.ts b/localenv/mock-account-servicing-entity/app/lib/parse_config.server.ts index b2897e9a9f..e5db7b3798 100644 --- a/localenv/mock-account-servicing-entity/app/lib/parse_config.server.ts +++ b/localenv/mock-account-servicing-entity/app/lib/parse_config.server.ts @@ -27,6 +27,5 @@ export const CONFIG: Config = { authServerDomain: process.env.AUTH_SERVER_DOMAIN || 'http://localhost:3006', graphqlUrl: process.env.GRAPHQL_URL, idpSecret: process.env.IDP_SECRET, - operatorTenantId: - process.env.OPERATOR_TENANT_ID || '438fa74a-fa7d-4317-9ced-dde32ece1787' + operatorTenantId: process.env.OPERATOR_TENANT_ID || '' } From a3fda78ced466d6d3c8db5b7a13e0c06a92f054c Mon Sep 17 00:00:00 2001 From: koekiebox Date: Mon, 20 Jan 2025 16:49:01 +0100 Subject: [PATCH 53/66] feat(3114): review feedback. --- packages/backend/src/graphql/middleware/index.ts | 16 +++++++--------- packages/backend/src/middleware/tenant/index.ts | 11 +++++++---- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/backend/src/graphql/middleware/index.ts b/packages/backend/src/graphql/middleware/index.ts index 3adda300d1..03a15b7726 100644 --- a/packages/backend/src/graphql/middleware/index.ts +++ b/packages/backend/src/graphql/middleware/index.ts @@ -1,6 +1,10 @@ import { GraphQLError } from 'graphql' import { IMiddleware } from 'graphql-middleware' -import { ApolloContext, ForTenantIdContext } from '../../app' +import { + ApolloContext, + ForTenantIdContext, + TenantedApolloContext +} from '../../app' import { CacheDataStore } from '../../middleware/cache/data-stores' import { lockMiddleware, Lock } from '../../middleware/lock' import { cacheMiddleware } from '../../middleware/cache' @@ -50,16 +54,10 @@ export function idempotencyGraphQLMiddleware( } export function setForTenantIdGraphQLMutationMiddleware(): { - Mutation: IMiddleware + Mutation: IMiddleware } { return { - Mutation: async ( - resolve, - root, - args, - context: ForTenantIdContext, - info - ) => { + Mutation: async (resolve, root, args, context, info) => { return validateTenantMiddleware({ deps: { context }, next: () => resolve(root, args, context, info), diff --git a/packages/backend/src/middleware/tenant/index.ts b/packages/backend/src/middleware/tenant/index.ts index cdb3fe130e..db8c867316 100644 --- a/packages/backend/src/middleware/tenant/index.ts +++ b/packages/backend/src/middleware/tenant/index.ts @@ -1,10 +1,10 @@ -import { ForTenantIdContext } from '../../app' +import { ForTenantIdContext, TenantedApolloContext } from '../../app' import { tenantIdToProceed } from '../../shared/utils' type Request = () => Promise interface TenantValidateMiddlewareArgs { - deps: { context: ForTenantIdContext } + deps: { context: TenantedApolloContext } tenantIdInput: string | undefined onFailValidation: Request next: Request @@ -20,7 +20,8 @@ export async function validateTenantMiddleware( next } = args if (!tenantIdInput) { - context.forTenantId = context.tenant.id + ;(context as ForTenantIdContext).forTenantId = context.tenant.id + //TODO context.forTenantId = context.tenant.id return next() } @@ -33,6 +34,8 @@ export async function validateTenantMiddleware( context.logger.error('Tenant validation error') return onFailValidation() } - context.forTenantId = forTenantId + ;(context as ForTenantIdContext).forTenantId = forTenantId + //context.forTenantId = forTenantId + return next() } From 5e2d5ed5ef916e4c638d13dbfc0df212f5895d9d Mon Sep 17 00:00:00 2001 From: koekiebox Date: Tue, 21 Jan 2025 14:09:00 +0100 Subject: [PATCH 54/66] feat(3114): review feedback from Max. --- .../app/lib/parse_config.server.ts | 6 +- .../generated/graphql.ts | 4 ++ packages/backend/src/app.ts | 5 +- .../src/graphql/generated/graphql.schema.json | 30 +++++++++- .../backend/src/graphql/generated/graphql.ts | 4 ++ .../graphql/resolvers/wallet_address.test.ts | 59 +++++++++++++++++++ .../src/graphql/resolvers/wallet_address.ts | 35 ++++++----- packages/backend/src/graphql/schema.graphql | 7 ++- .../backend/src/middleware/tenant/index.ts | 4 +- .../open_payments/wallet_address/service.ts | 13 ++-- packages/backend/src/shared/baseModel.ts | 12 +++- packages/frontend/app/generated/graphql.ts | 4 ++ .../src/generated/graphql.ts | 4 ++ test/integration/lib/generated/graphql.ts | 4 ++ 14 files changed, 157 insertions(+), 34 deletions(-) diff --git a/localenv/mock-account-servicing-entity/app/lib/parse_config.server.ts b/localenv/mock-account-servicing-entity/app/lib/parse_config.server.ts index e5db7b3798..5ab802e182 100644 --- a/localenv/mock-account-servicing-entity/app/lib/parse_config.server.ts +++ b/localenv/mock-account-servicing-entity/app/lib/parse_config.server.ts @@ -15,6 +15,10 @@ if (!process.env.IDP_SECRET) { throw new Error('Environment variable IDP_SECRET is required') } +if (!process.env.OPERATOR_TENANT_ID) { + throw new Error('Environment variable OPERATOR_TENANT_ID is required') +} + export const CONFIG: Config = { seed: parse( readFileSync( @@ -27,5 +31,5 @@ export const CONFIG: Config = { authServerDomain: process.env.AUTH_SERVER_DOMAIN || 'http://localhost:3006', graphqlUrl: process.env.GRAPHQL_URL, idpSecret: process.env.IDP_SECRET, - operatorTenantId: process.env.OPERATOR_TENANT_ID || '' + operatorTenantId: process.env.OPERATOR_TENANT_ID } diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 8e67bbe7d7..078d9064de 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -1265,6 +1265,7 @@ export type QueryWalletAddressesArgs = { first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; + tenantId?: InputMaybe; }; @@ -1498,6 +1499,8 @@ export type WalletAddress = Model & { quotes?: Maybe; /** The current status of the wallet, either active or inactive. */ status: WalletAddressStatus; + /** Tenant ID of the wallet address. */ + tenantId: Scalars['String']['output']; /** Wallet Address URL. */ url: Scalars['String']['output']; /** List of keys associated with this wallet address */ @@ -2419,6 +2422,7 @@ export type WalletAddressResolvers, ParentType, ContextType>; quotes?: Resolver, ParentType, ContextType, Partial>; status?: Resolver; + tenantId?: Resolver; url?: Resolver; walletAddressKeys?: Resolver, ParentType, ContextType, Partial>; __isTypeOf?: IsTypeOfResolverFn; diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 76f36c75e9..9ac7c1bf59 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -454,12 +454,11 @@ export class App { koa.use( koaMiddleware(this.apolloServer, { - context: async (): Promise => { + context: async (): Promise => { return { ...tenantApiSignatureResult, container: this.container, - logger: await this.container.use('logger'), - forTenantId: this.config.operatorTenantId + logger: await this.container.use('logger') } } }) diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index 1e4bd6584a..8c33c0e337 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -6943,6 +6943,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "tenantId", + "description": "Unique identifier of the tenant associated with the wallet address. Optional, if not provided, the tenantId will be obtained from the signature.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "type": { @@ -8481,6 +8493,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "tenantId", + "description": "Tenant ID of the wallet address.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "url", "description": "Wallet Address URL.", @@ -8527,7 +8555,7 @@ }, { "name": "first", - "description": "Foward pagination: Limit the result to the first **n** keys after the `after` cursor.", + "description": "Forward pagination: Limit the result to the first **n** keys after the `after` cursor.", "type": { "kind": "SCALAR", "name": "Int", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 8e67bbe7d7..078d9064de 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -1265,6 +1265,7 @@ export type QueryWalletAddressesArgs = { first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; + tenantId?: InputMaybe; }; @@ -1498,6 +1499,8 @@ export type WalletAddress = Model & { quotes?: Maybe; /** The current status of the wallet, either active or inactive. */ status: WalletAddressStatus; + /** Tenant ID of the wallet address. */ + tenantId: Scalars['String']['output']; /** Wallet Address URL. */ url: Scalars['String']['output']; /** List of keys associated with this wallet address */ @@ -2419,6 +2422,7 @@ export type WalletAddressResolvers, ParentType, ContextType>; quotes?: Resolver, ParentType, ContextType, Partial>; status?: Resolver; + tenantId?: Resolver; url?: Resolver; walletAddressKeys?: Resolver, ParentType, ContextType, Partial>; __isTypeOf?: IsTypeOfResolverFn; diff --git a/packages/backend/src/graphql/resolvers/wallet_address.test.ts b/packages/backend/src/graphql/resolvers/wallet_address.test.ts index 2a469d3635..30a408ade3 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.test.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.test.ts @@ -696,6 +696,7 @@ describe('Wallet Address Resolvers', (): void => { }) test('bad input data when not allowed to perform cross tenant update', async (): Promise => { + expect.assertions(2) try { const tenantOptions = { apiSecret: 'test-api-secret-new', @@ -1001,6 +1002,64 @@ describe('Wallet Address Resolvers', (): void => { }) }) }) + + test('Can get page of wallet addresses with tenantId param', async (): Promise => { + const walletAddresses: WalletAddressModel[] = [] + for (let i = 0; i < 2; i++) { + walletAddresses.push( + await createWalletAddress(deps, { tenantId: Config.operatorTenantId }) + ) + } + walletAddresses.reverse() // Calling the default getPage will result in descending order + const query = await appContainer.apolloClient + .query({ + query: gql` + query WalletAddresses($tenantId: String) { + walletAddresses(tenantId: $tenantId) { + edges { + node { + id + asset { + code + scale + } + url + publicName + } + cursor + } + } + } + `, + variables: { + tenantId: Config.operatorTenantId + } + }) + .then((query): WalletAddressesConnection => { + if (query.data) { + return query.data.walletAddresses + } else { + throw new Error('Data was empty') + } + }) + + expect(query.edges).toHaveLength(2) + query.edges.forEach((edge, idx) => { + const walletAddress = walletAddresses[idx] + expect(edge.cursor).toEqual(walletAddress.id) + expect(edge.node).toEqual({ + __typename: 'WalletAddress', + id: walletAddress.id, + asset: { + __typename: 'Asset', + code: walletAddress.asset.code, + scale: walletAddress.asset.scale + }, + url: walletAddress.url, + publicName: walletAddress.publicName + }) + }) + }) }) describe('Trigger Wallet Address Events', (): void => { diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index e15884130e..dd0e4ed2fd 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -8,7 +8,7 @@ import { MutationResolvers, WalletAddressStatus } from '../generated/graphql' -import { ForTenantIdContext } from '../../app' +import { ForTenantIdContext, TenantedApolloContext } from '../../app' import { WalletAddressError, isWalletAddressError, @@ -26,18 +26,22 @@ import { import { tenantIdToProceed } from '../../shared/utils' import { GraphQLErrorCode } from '../errors' -export const getWalletAddresses: QueryResolvers['walletAddresses'] = +export const getWalletAddresses: QueryResolvers['walletAddresses'] = async ( parent, args, ctx ): Promise => { const walletAddressService = await ctx.container.use('walletAddressService') - const { sortOrder, ...pagination } = args + const { tenantId, sortOrder, ...pagination } = args const order = sortOrder === 'ASC' ? SortOrder.Asc : SortOrder.Desc + + const tenantForLookup = + tenantId && ctx.isOperator ? tenantId : ctx.tenant.id const walletAddresses = await walletAddressService.getPage( pagination, - order + order, + tenantForLookup ) const pageInfo = await getPageInfo({ getPage: (pagination: Pagination, sortOrder?: SortOrder) => @@ -47,20 +51,14 @@ export const getWalletAddresses: QueryResolvers['walletAddre }) return { pageInfo, - edges: walletAddresses - .filter( - (wa: WalletAddress) => - tenantIdToProceed(ctx.isOperator, ctx.tenant.id, wa.tenantId) != - undefined - ) - .map((walletAddress: WalletAddress) => ({ - cursor: walletAddress.id, - node: walletAddressToGraphql(walletAddress) - })) + edges: walletAddresses.map((walletAddress: WalletAddress) => ({ + cursor: walletAddress.id, + node: walletAddressToGraphql(walletAddress) + })) } } -export const getWalletAddress: QueryResolvers['walletAddress'] = +export const getWalletAddress: QueryResolvers['walletAddress'] = async (parent, args, ctx): Promise => { const walletAddressService = await ctx.container.use('walletAddressService') const walletAddress = await walletAddressService.get(args.id) @@ -80,7 +78,7 @@ export const getWalletAddress: QueryResolvers['walletAddress return walletAddressToGraphql(walletAddress) } -export const getWalletAddressByUrl: QueryResolvers['walletAddressByUrl'] = +export const getWalletAddressByUrl: QueryResolvers['walletAddressByUrl'] = async ( parent, args, @@ -183,7 +181,7 @@ export const updateWalletAddress: MutationResolvers['updateW } } -export const triggerWalletAddressEvents: MutationResolvers['triggerWalletAddressEvents'] = +export const triggerWalletAddressEvents: MutationResolvers['triggerWalletAddressEvents'] = async ( parent, args, @@ -207,6 +205,7 @@ export function walletAddressToGraphql( createdAt: new Date(+walletAddress.createdAt).toISOString(), status: walletAddress.isActive ? WalletAddressStatus.Active - : WalletAddressStatus.Inactive + : WalletAddressStatus.Inactive, + tenantId: walletAddress.tenantId } } diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 6ae8a6e255..0f4cc0d776 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -70,6 +70,8 @@ type Query { last: Int "Specify the sort order of wallet addresses based on their creation date, either ascending or descending." sortOrder: SortOrder + "Unique identifier of the tenant associated with the wallet address. Optional, if not provided, the tenantId will be obtained from the signature." + tenantId: String ): WalletAddressesConnection! "Fetch an Open Payments quote by its ID." @@ -793,7 +795,7 @@ type WalletAddress implements Model { after: String "Backward pagination: Cursor (wallet address key ID) to start retrieving keys before this point." before: String - "Foward pagination: Limit the result to the first **n** keys after the `after` cursor." + "Forward pagination: Limit the result to the first **n** keys after the `after` cursor." first: Int "Backward pagination: Limit the result to the last **n** keys before the `before` cursor." last: Int @@ -803,6 +805,9 @@ type WalletAddress implements Model { "Additional properties associated with the wallet address." additionalProperties: [AdditionalProperty] + + "Tenant ID of the wallet address." + tenantId: String! } type AdditionalProperty { diff --git a/packages/backend/src/middleware/tenant/index.ts b/packages/backend/src/middleware/tenant/index.ts index db8c867316..611cd1a3f5 100644 --- a/packages/backend/src/middleware/tenant/index.ts +++ b/packages/backend/src/middleware/tenant/index.ts @@ -21,7 +21,6 @@ export async function validateTenantMiddleware( } = args if (!tenantIdInput) { ;(context as ForTenantIdContext).forTenantId = context.tenant.id - //TODO context.forTenantId = context.tenant.id return next() } @@ -34,8 +33,7 @@ export async function validateTenantMiddleware( context.logger.error('Tenant validation error') return onFailValidation() } - ;(context as ForTenantIdContext).forTenantId = forTenantId - //context.forTenantId = forTenantId + ;(context as ForTenantIdContext).forTenantId = forTenantId return next() } diff --git a/packages/backend/src/open_payments/wallet_address/service.ts b/packages/backend/src/open_payments/wallet_address/service.ts index 38bb777cdc..d2cb2c0bad 100644 --- a/packages/backend/src/open_payments/wallet_address/service.ts +++ b/packages/backend/src/open_payments/wallet_address/service.ts @@ -70,7 +70,8 @@ export interface WalletAddressService { getOrPollByUrl(url: string): Promise getPage( pagination?: Pagination, - sortOrder?: SortOrder + sortOrder?: SortOrder, + tenantId?: string ): Promise processNext(): Promise triggerEvents(limit: number): Promise @@ -118,8 +119,8 @@ export async function createWalletAddressService({ get: (id) => getWalletAddress(deps, id), getByUrl: (url) => getWalletAddressByUrl(deps, url), getOrPollByUrl: (url) => getOrPollByUrl(deps, url), - getPage: (pagination?, sortOrder?) => - getWalletAddressPage(deps, pagination, sortOrder), + getPage: (pagination?, sortOrder?, tenantId?) => + getWalletAddressPage(deps, pagination, sortOrder, tenantId), processNext: () => processNextWalletAddress(deps), triggerEvents: (limit) => triggerWalletAddressEvents(deps, limit) } @@ -345,11 +346,13 @@ async function getWalletAddressByUrl( async function getWalletAddressPage( deps: ServiceDependencies, pagination?: Pagination, - sortOrder?: SortOrder + sortOrder?: SortOrder, + tenantId?: string ): Promise { const addresses = await WalletAddress.query(deps.knex).getPage( pagination, - sortOrder + sortOrder, + tenantId ) for (const address of addresses) { const asset = await deps.assetService.get(address.assetId) diff --git a/packages/backend/src/shared/baseModel.ts b/packages/backend/src/shared/baseModel.ts index 94e9c383b7..064cbf6bdb 100644 --- a/packages/backend/src/shared/baseModel.ts +++ b/packages/backend/src/shared/baseModel.ts @@ -49,11 +49,14 @@ class PaginationQueryBuilder extends QueryBuilder< * Please read the spec before changing things: * https://relay.dev/graphql/connections.htm * @param pagination Pagination - cursors and limits. + * @param sortOrder SortOrder - Asc/Desc sort order. + * @param tenantId string - When filtering for a specific tenant. * @returns Model[] An array of Models that form a page. */ getPage( pagination?: Pagination, - sortOrder: SortOrder = SortOrder.Desc + sortOrder: SortOrder = SortOrder.Desc, + tenantId?: string ): this { const tableName = this.modelClass().tableName if ( @@ -66,13 +69,18 @@ class PaginationQueryBuilder extends QueryBuilder< if (first < 0 || first > 100) throw new Error('Pagination index error') const last = pagination?.last || 20 if (last < 0 || last > 100) throw new Error('Pagination index error') + + const tenantFilterClause = tenantId + ? ` AND "${tableName}"."tenantId" = ? ${tenantId}` + : '' + /** * Forward pagination */ if (typeof pagination?.after === 'string') { const comparisonOperator = sortOrder === SortOrder.Asc ? '>' : '<' return this.whereRaw( - `("${tableName}"."createdAt", "${tableName}"."id") ${comparisonOperator} (select "${tableName}"."createdAt" :: TIMESTAMP, "${tableName}"."id" from ?? where "${tableName}"."id" = ?)`, + `("${tableName}"."createdAt", "${tableName}"."id") ${comparisonOperator} (select "${tableName}"."createdAt" :: TIMESTAMP, "${tableName}"."id" from ?? where "${tableName}"."id" = ?${tenantFilterClause})`, [this.modelClass().tableName, pagination.after] ) .orderBy([ diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index 835e75a512..6f46097e01 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -1265,6 +1265,7 @@ export type QueryWalletAddressesArgs = { first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; + tenantId?: InputMaybe; }; @@ -1498,6 +1499,8 @@ export type WalletAddress = Model & { quotes?: Maybe; /** The current status of the wallet, either active or inactive. */ status: WalletAddressStatus; + /** Tenant ID of the wallet address. */ + tenantId: Scalars['String']['output']; /** Wallet Address URL. */ url: Scalars['String']['output']; /** List of keys associated with this wallet address */ @@ -2419,6 +2422,7 @@ export type WalletAddressResolvers, ParentType, ContextType>; quotes?: Resolver, ParentType, ContextType, Partial>; status?: Resolver; + tenantId?: Resolver; url?: Resolver; walletAddressKeys?: Resolver, ParentType, ContextType, Partial>; __isTypeOf?: IsTypeOfResolverFn; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 8e67bbe7d7..078d9064de 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -1265,6 +1265,7 @@ export type QueryWalletAddressesArgs = { first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; + tenantId?: InputMaybe; }; @@ -1498,6 +1499,8 @@ export type WalletAddress = Model & { quotes?: Maybe; /** The current status of the wallet, either active or inactive. */ status: WalletAddressStatus; + /** Tenant ID of the wallet address. */ + tenantId: Scalars['String']['output']; /** Wallet Address URL. */ url: Scalars['String']['output']; /** List of keys associated with this wallet address */ @@ -2419,6 +2422,7 @@ export type WalletAddressResolvers, ParentType, ContextType>; quotes?: Resolver, ParentType, ContextType, Partial>; status?: Resolver; + tenantId?: Resolver; url?: Resolver; walletAddressKeys?: Resolver, ParentType, ContextType, Partial>; __isTypeOf?: IsTypeOfResolverFn; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 8e67bbe7d7..078d9064de 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -1265,6 +1265,7 @@ export type QueryWalletAddressesArgs = { first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; + tenantId?: InputMaybe; }; @@ -1498,6 +1499,8 @@ export type WalletAddress = Model & { quotes?: Maybe; /** The current status of the wallet, either active or inactive. */ status: WalletAddressStatus; + /** Tenant ID of the wallet address. */ + tenantId: Scalars['String']['output']; /** Wallet Address URL. */ url: Scalars['String']['output']; /** List of keys associated with this wallet address */ @@ -2419,6 +2422,7 @@ export type WalletAddressResolvers, ParentType, ContextType>; quotes?: Resolver, ParentType, ContextType, Partial>; status?: Resolver; + tenantId?: Resolver; url?: Resolver; walletAddressKeys?: Resolver, ParentType, ContextType, Partial>; __isTypeOf?: IsTypeOfResolverFn; From eb879548d917ea1e0f8c15f8707350cf2fc1942d Mon Sep 17 00:00:00 2001 From: koekiebox Date: Tue, 21 Jan 2025 16:31:17 +0100 Subject: [PATCH 55/66] feat(3114): review feedback from Max. --- .../generated/graphql.ts | 4 ++-- .../src/graphql/generated/graphql.schema.json | 16 ++++++---------- .../backend/src/graphql/generated/graphql.ts | 4 ++-- packages/backend/src/graphql/schema.graphql | 8 ++++---- packages/frontend/app/generated/graphql.ts | 4 ++-- .../src/generated/graphql.ts | 4 ++-- test/integration/lib/generated/graphql.ts | 4 ++-- 7 files changed, 20 insertions(+), 24 deletions(-) diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 078d9064de..a958f5c1b9 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -1500,7 +1500,7 @@ export type WalletAddress = Model & { /** The current status of the wallet, either active or inactive. */ status: WalletAddressStatus; /** Tenant ID of the wallet address. */ - tenantId: Scalars['String']['output']; + tenantId?: Maybe; /** Wallet Address URL. */ url: Scalars['String']['output']; /** List of keys associated with this wallet address */ @@ -2422,7 +2422,7 @@ export type WalletAddressResolvers, ParentType, ContextType>; quotes?: Resolver, ParentType, ContextType, Partial>; status?: Resolver; - tenantId?: Resolver; + tenantId?: Resolver, ParentType, ContextType>; url?: Resolver; walletAddressKeys?: Resolver, ParentType, ContextType, Partial>; __isTypeOf?: IsTypeOfResolverFn; diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index 8c33c0e337..028b55788d 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -594,7 +594,7 @@ }, { "name": "first", - "description": "Foward pagination: Limit the result to the first **n** fees after the `after` cursor.", + "description": "Forward pagination: Limit the result to the first **n** fees after the `after` cursor.", "type": { "kind": "SCALAR", "name": "Int", @@ -6330,7 +6330,7 @@ }, { "name": "first", - "description": "Foward pagination: Limit the result to the first **n** assets after the `after` cursor.", + "description": "Forward pagination: Limit the result to the first **n** assets after the `after` cursor.", "type": { "kind": "SCALAR", "name": "Int", @@ -8434,7 +8434,7 @@ }, { "name": "first", - "description": "Foward pagination: Limit the result to the first **n** quotes after the `after` cursor.", + "description": "Forward pagination: Limit the result to the first **n** quotes after the `after` cursor.", "type": { "kind": "SCALAR", "name": "Int", @@ -8498,13 +8498,9 @@ "description": "Tenant ID of the wallet address.", "args": [], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "isDeprecated": false, "deprecationReason": null diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 078d9064de..a958f5c1b9 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -1500,7 +1500,7 @@ export type WalletAddress = Model & { /** The current status of the wallet, either active or inactive. */ status: WalletAddressStatus; /** Tenant ID of the wallet address. */ - tenantId: Scalars['String']['output']; + tenantId?: Maybe; /** Wallet Address URL. */ url: Scalars['String']['output']; /** List of keys associated with this wallet address */ @@ -2422,7 +2422,7 @@ export type WalletAddressResolvers, ParentType, ContextType>; quotes?: Resolver, ParentType, ContextType, Partial>; status?: Resolver; - tenantId?: Resolver; + tenantId?: Resolver, ParentType, ContextType>; url?: Resolver; walletAddressKeys?: Resolver, ParentType, ContextType, Partial>; __isTypeOf?: IsTypeOfResolverFn; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 0f4cc0d776..1874dc4433 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -16,7 +16,7 @@ type Query { after: String "Backward pagination: Cursor (asset ID) to start retrieving assets before this point." before: String - "Foward pagination: Limit the result to the first **n** assets after the `after` cursor." + "Forward pagination: Limit the result to the first **n** assets after the `after` cursor." first: Int "Backward pagination: Limit the result to the last **n** assets before the `before` cursor." last: Int @@ -626,7 +626,7 @@ type Asset implements Model { after: String "Backward pagination: Cursor (fee ID) to start retrieving fees before this point." before: String - "Foward pagination: Limit the result to the first **n** fees after the `after` cursor." + "Forward pagination: Limit the result to the first **n** fees after the `after` cursor." first: Int "Backward pagination: Limit the result to the last **n** fees before the `before` cursor." last: Int @@ -761,7 +761,7 @@ type WalletAddress implements Model { after: String "Backward pagination: Cursor (quote ID) to start retrieving quotes before this point." before: String - "Foward pagination: Limit the result to the first **n** quotes after the `after` cursor." + "Forward pagination: Limit the result to the first **n** quotes after the `after` cursor." first: Int "Backward pagination: Limit the result to the last **n** quotes before the `before` cursor." last: Int @@ -807,7 +807,7 @@ type WalletAddress implements Model { additionalProperties: [AdditionalProperty] "Tenant ID of the wallet address." - tenantId: String! + tenantId: String } type AdditionalProperty { diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index 6f46097e01..eb63120530 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -1500,7 +1500,7 @@ export type WalletAddress = Model & { /** The current status of the wallet, either active or inactive. */ status: WalletAddressStatus; /** Tenant ID of the wallet address. */ - tenantId: Scalars['String']['output']; + tenantId?: Maybe; /** Wallet Address URL. */ url: Scalars['String']['output']; /** List of keys associated with this wallet address */ @@ -2422,7 +2422,7 @@ export type WalletAddressResolvers, ParentType, ContextType>; quotes?: Resolver, ParentType, ContextType, Partial>; status?: Resolver; - tenantId?: Resolver; + tenantId?: Resolver, ParentType, ContextType>; url?: Resolver; walletAddressKeys?: Resolver, ParentType, ContextType, Partial>; __isTypeOf?: IsTypeOfResolverFn; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 078d9064de..a958f5c1b9 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -1500,7 +1500,7 @@ export type WalletAddress = Model & { /** The current status of the wallet, either active or inactive. */ status: WalletAddressStatus; /** Tenant ID of the wallet address. */ - tenantId: Scalars['String']['output']; + tenantId?: Maybe; /** Wallet Address URL. */ url: Scalars['String']['output']; /** List of keys associated with this wallet address */ @@ -2422,7 +2422,7 @@ export type WalletAddressResolvers, ParentType, ContextType>; quotes?: Resolver, ParentType, ContextType, Partial>; status?: Resolver; - tenantId?: Resolver; + tenantId?: Resolver, ParentType, ContextType>; url?: Resolver; walletAddressKeys?: Resolver, ParentType, ContextType, Partial>; __isTypeOf?: IsTypeOfResolverFn; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 078d9064de..a958f5c1b9 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -1500,7 +1500,7 @@ export type WalletAddress = Model & { /** The current status of the wallet, either active or inactive. */ status: WalletAddressStatus; /** Tenant ID of the wallet address. */ - tenantId: Scalars['String']['output']; + tenantId?: Maybe; /** Wallet Address URL. */ url: Scalars['String']['output']; /** List of keys associated with this wallet address */ @@ -2422,7 +2422,7 @@ export type WalletAddressResolvers, ParentType, ContextType>; quotes?: Resolver, ParentType, ContextType, Partial>; status?: Resolver; - tenantId?: Resolver; + tenantId?: Resolver, ParentType, ContextType>; url?: Resolver; walletAddressKeys?: Resolver, ParentType, ContextType, Partial>; __isTypeOf?: IsTypeOfResolverFn; From 951a4c76daca99ca3f4604efb89de0406db26cb0 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Tue, 21 Jan 2025 16:51:04 +0100 Subject: [PATCH 56/66] feat(3114): fixed. --- packages/backend/src/shared/baseModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/shared/baseModel.ts b/packages/backend/src/shared/baseModel.ts index 064cbf6bdb..5515121794 100644 --- a/packages/backend/src/shared/baseModel.ts +++ b/packages/backend/src/shared/baseModel.ts @@ -71,7 +71,7 @@ class PaginationQueryBuilder extends QueryBuilder< if (last < 0 || last > 100) throw new Error('Pagination index error') const tenantFilterClause = tenantId - ? ` AND "${tableName}"."tenantId" = ? ${tenantId}` + ? ` AND "${tableName}"."tenantId" = '${tenantId}'` : '' /** From 4c60f7b4acd1523db9a1cd1aa0594d9e39d96891 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Wed, 22 Jan 2025 15:22:17 +0100 Subject: [PATCH 57/66] feat(3114): review feedback. --- .../generated/graphql.ts | 3 +- .../src/graphql/generated/graphql.schema.json | 14 +++++++- .../backend/src/graphql/generated/graphql.ts | 3 +- .../src/graphql/resolvers/wallet_address.ts | 24 ++++++++++---- packages/backend/src/graphql/schema.graphql | 4 ++- .../backend/src/middleware/tenant/index.ts | 5 --- .../open_payments/wallet_address/service.ts | 32 ++++++++++++------- packages/backend/src/shared/baseModel.ts | 11 ++----- packages/frontend/app/generated/graphql.ts | 3 +- .../src/generated/graphql.ts | 3 +- test/integration/lib/generated/graphql.ts | 3 +- 11 files changed, 67 insertions(+), 38 deletions(-) diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index a958f5c1b9..1ee5eb4488 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -374,7 +374,7 @@ export type CreateWalletAddressInput = { /** Public name associated with the wallet address. This is visible to anyone with the wallet address URL. */ publicName?: InputMaybe; /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature. */ - tenantId?: InputMaybe; + tenantId?: InputMaybe; /** Wallet address URL. This cannot be changed. */ url: Scalars['String']['input']; }; @@ -1251,6 +1251,7 @@ export type QueryReceiverArgs = { export type QueryWalletAddressArgs = { id: Scalars['String']['input']; + tenantId?: InputMaybe; }; diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index 028b55788d..6930bd5cd4 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -2176,7 +2176,7 @@ "description": "Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature.", "type": { "kind": "SCALAR", - "name": "String", + "name": "ID", "ofType": null }, "defaultValue": null, @@ -6841,6 +6841,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "tenantId", + "description": "Unique identifier of the tenant associated with the wallet address. Optional, if not provided, the tenantId will be obtained from the signature.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "type": { diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index a958f5c1b9..1ee5eb4488 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -374,7 +374,7 @@ export type CreateWalletAddressInput = { /** Public name associated with the wallet address. This is visible to anyone with the wallet address URL. */ publicName?: InputMaybe; /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature. */ - tenantId?: InputMaybe; + tenantId?: InputMaybe; /** Wallet address URL. This cannot be changed. */ url: Scalars['String']['input']; }; @@ -1251,6 +1251,7 @@ export type QueryReceiverArgs = { export type QueryWalletAddressArgs = { id: Scalars['String']['input']; + tenantId?: InputMaybe; }; diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index dd0e4ed2fd..0ba1b48840 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -60,12 +60,25 @@ export const getWalletAddresses: QueryResolvers['walletAd export const getWalletAddress: QueryResolvers['walletAddress'] = async (parent, args, ctx): Promise => { + const tenantId = tenantIdToProceed( + ctx.isOperator, + ctx.tenant.id, + args.tenantId + ) + if (!tenantId) { + throw new GraphQLError( + errorToMessage[WalletAddressError.UnknownWalletAddress], + { + extensions: { + code: errorToCode[WalletAddressError.UnknownWalletAddress] + } + } + ) + } + const walletAddressService = await ctx.container.use('walletAddressService') - const walletAddress = await walletAddressService.get(args.id) - if ( - !walletAddress || - !tenantIdToProceed(ctx.isOperator, ctx.tenant.id, walletAddress.tenantId) - ) { + const walletAddress = await walletAddressService.get(args.id, tenantId) + if (!walletAddress) { throw new GraphQLError( errorToMessage[WalletAddressError.UnknownWalletAddress], { @@ -111,7 +124,6 @@ export const createWalletAddress: MutationResolvers['createW const options: CreateOptions = { assetId: args.input.assetId, - // We always have a tenant for [ForTenantIdContext]. tenantId: ctx.forTenantId, additionalProperties: addProps, publicName: args.input.publicName, diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 1874dc4433..3138c5df9e 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -53,6 +53,8 @@ type Query { walletAddress( "Unique identifier of the wallet address." id: String! + "Unique identifier of the tenant associated with the wallet address. Optional, if not provided, the tenantId will be obtained from the signature." + tenantId: String ): WalletAddress "Get a wallet address by its url if it exists" @@ -1232,7 +1234,7 @@ type CreateReceiverResponse { input CreateWalletAddressInput { "Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature." - tenantId: String + tenantId: ID "Unique identifier of the asset associated with the wallet address. This cannot be changed." assetId: String! "Wallet address URL. This cannot be changed." diff --git a/packages/backend/src/middleware/tenant/index.ts b/packages/backend/src/middleware/tenant/index.ts index 611cd1a3f5..e0ef5b27e1 100644 --- a/packages/backend/src/middleware/tenant/index.ts +++ b/packages/backend/src/middleware/tenant/index.ts @@ -19,11 +19,6 @@ export async function validateTenantMiddleware( onFailValidation, next } = args - if (!tenantIdInput) { - ;(context as ForTenantIdContext).forTenantId = context.tenant.id - return next() - } - const forTenantId = tenantIdToProceed( context.isOperator, context.tenant.id, diff --git a/packages/backend/src/open_payments/wallet_address/service.ts b/packages/backend/src/open_payments/wallet_address/service.ts index d2cb2c0bad..c04a2720fa 100644 --- a/packages/backend/src/open_payments/wallet_address/service.ts +++ b/packages/backend/src/open_payments/wallet_address/service.ts @@ -65,7 +65,7 @@ export interface WalletAddressService { id: string, includeVisibleOnlyAddProps: boolean ): Promise - get(id: string): Promise + get(id: string, tenantId?: string): Promise getByUrl(url: string): Promise getOrPollByUrl(url: string): Promise getPage( @@ -116,7 +116,7 @@ export async function createWalletAddressService({ walletAddressId, includeVisibleOnlyAddProps ), - get: (id) => getWalletAddress(deps, id), + get: (id, tenantId) => getWalletAddress(deps, id, tenantId), getByUrl: (url) => getWalletAddressByUrl(deps, url), getOrPollByUrl: (url) => getOrPollByUrl(deps, url), getPage: (pagination?, sortOrder?, tenantId?) => @@ -268,12 +268,20 @@ async function updateWalletAddress( async function getWalletAddress( deps: ServiceDependencies, - id: string + id: string, + tenantId?: string ): Promise { - const walletAdd = await deps.walletAddressCache.get(id) - if (walletAdd) return walletAdd + const inMem = await deps.walletAddressCache.get(id) + if (inMem) { + return tenantId && inMem.tenantId !== tenantId ? undefined : inMem + } + + const query = WalletAddress.query(deps.knex) + if (tenantId) { + query.andWhere({ tenantId }) + } - const walletAddress = await WalletAddress.query(deps.knex).findById(id) + const walletAddress = await query.findById(id) if (walletAddress) { const asset = await deps.assetService.get(walletAddress.assetId) if (asset) walletAddress.asset = asset @@ -349,11 +357,13 @@ async function getWalletAddressPage( sortOrder?: SortOrder, tenantId?: string ): Promise { - const addresses = await WalletAddress.query(deps.knex).getPage( - pagination, - sortOrder, - tenantId - ) + const query = WalletAddress.query(deps.knex) + + if (tenantId && tenantId.length > 0) { + query.where({ tenantId }) + } + + const addresses = await query.getPage(pagination, sortOrder) for (const address of addresses) { const asset = await deps.assetService.get(address.assetId) if (asset) address.asset = asset diff --git a/packages/backend/src/shared/baseModel.ts b/packages/backend/src/shared/baseModel.ts index 5515121794..8a2c33a796 100644 --- a/packages/backend/src/shared/baseModel.ts +++ b/packages/backend/src/shared/baseModel.ts @@ -50,13 +50,11 @@ class PaginationQueryBuilder extends QueryBuilder< * https://relay.dev/graphql/connections.htm * @param pagination Pagination - cursors and limits. * @param sortOrder SortOrder - Asc/Desc sort order. - * @param tenantId string - When filtering for a specific tenant. * @returns Model[] An array of Models that form a page. */ getPage( pagination?: Pagination, - sortOrder: SortOrder = SortOrder.Desc, - tenantId?: string + sortOrder: SortOrder = SortOrder.Desc ): this { const tableName = this.modelClass().tableName if ( @@ -69,18 +67,13 @@ class PaginationQueryBuilder extends QueryBuilder< if (first < 0 || first > 100) throw new Error('Pagination index error') const last = pagination?.last || 20 if (last < 0 || last > 100) throw new Error('Pagination index error') - - const tenantFilterClause = tenantId - ? ` AND "${tableName}"."tenantId" = '${tenantId}'` - : '' - /** * Forward pagination */ if (typeof pagination?.after === 'string') { const comparisonOperator = sortOrder === SortOrder.Asc ? '>' : '<' return this.whereRaw( - `("${tableName}"."createdAt", "${tableName}"."id") ${comparisonOperator} (select "${tableName}"."createdAt" :: TIMESTAMP, "${tableName}"."id" from ?? where "${tableName}"."id" = ?${tenantFilterClause})`, + `("${tableName}"."createdAt", "${tableName}"."id") ${comparisonOperator} (select "${tableName}"."createdAt" :: TIMESTAMP, "${tableName}"."id" from ?? where "${tableName}"."id" = ?)`, [this.modelClass().tableName, pagination.after] ) .orderBy([ diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index eb63120530..78028ee916 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -374,7 +374,7 @@ export type CreateWalletAddressInput = { /** Public name associated with the wallet address. This is visible to anyone with the wallet address URL. */ publicName?: InputMaybe; /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature. */ - tenantId?: InputMaybe; + tenantId?: InputMaybe; /** Wallet address URL. This cannot be changed. */ url: Scalars['String']['input']; }; @@ -1251,6 +1251,7 @@ export type QueryReceiverArgs = { export type QueryWalletAddressArgs = { id: Scalars['String']['input']; + tenantId?: InputMaybe; }; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index a958f5c1b9..1ee5eb4488 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -374,7 +374,7 @@ export type CreateWalletAddressInput = { /** Public name associated with the wallet address. This is visible to anyone with the wallet address URL. */ publicName?: InputMaybe; /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature. */ - tenantId?: InputMaybe; + tenantId?: InputMaybe; /** Wallet address URL. This cannot be changed. */ url: Scalars['String']['input']; }; @@ -1251,6 +1251,7 @@ export type QueryReceiverArgs = { export type QueryWalletAddressArgs = { id: Scalars['String']['input']; + tenantId?: InputMaybe; }; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index a958f5c1b9..1ee5eb4488 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -374,7 +374,7 @@ export type CreateWalletAddressInput = { /** Public name associated with the wallet address. This is visible to anyone with the wallet address URL. */ publicName?: InputMaybe; /** Unique identifier of the tenant associated with the wallet address. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature. */ - tenantId?: InputMaybe; + tenantId?: InputMaybe; /** Wallet address URL. This cannot be changed. */ url: Scalars['String']['input']; }; @@ -1251,6 +1251,7 @@ export type QueryReceiverArgs = { export type QueryWalletAddressArgs = { id: Scalars['String']['input']; + tenantId?: InputMaybe; }; From 3278c879d8b7c8018131765947d0b9ed63ab16cb Mon Sep 17 00:00:00 2001 From: koekiebox Date: Wed, 22 Jan 2025 15:40:09 +0100 Subject: [PATCH 58/66] feat(3114): review feedback. --- .../graphql/resolvers/wallet_address.test.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/packages/backend/src/graphql/resolvers/wallet_address.test.ts b/packages/backend/src/graphql/resolvers/wallet_address.test.ts index 30a408ade3..3b0b905554 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.test.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.test.ts @@ -842,6 +842,53 @@ describe('Wallet Address Resolvers', (): void => { } ] }) + + // Attempt to switch tenant: + try { + const queryTenant = await appContainer.apolloClient + .query({ + query: gql` + query WalletAddress($walletAddressId: String!, $tenantId: String) { + walletAddress(id: $walletAddressId, tenantId: $tenantId) { + id + liquidity + asset { + code + scale + } + url + publicName + additionalProperties { + key + value + visibleInOpenPayments + } + } + } + `, + variables: { + walletAddressId: walletAddress.id, + tenantId: 'ae4950b6-3e1b-4e50-ad24-25c065bdd3a9', + } + }) + .then((query): WalletAddress => { + if (query.data) { + return query.data.walletAddress + } else { + throw new Error('Data was empty') + } + }) + } catch (error) { + expect(error).toBeInstanceOf(ApolloError) + expect((error as ApolloError).graphQLErrors).toContainEqual( + expect.objectContaining({ + message: 'unknown wallet address', + extensions: expect.objectContaining({ + code: GraphQLErrorCode.NotFound + }) + }) + ) + } } ) From 4d13e0fd651f44f1fd3a6da20cbcc82d962e0044 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Wed, 22 Jan 2025 19:22:07 +0100 Subject: [PATCH 59/66] feat(3114): review feedback. do not force error on graphql middleware. --- packages/backend/src/app.ts | 2 +- .../backend/src/graphql/middleware/index.ts | 13 +--- .../graphql/resolvers/wallet_address.test.ts | 66 ++++++++++--------- .../src/graphql/resolvers/wallet_address.ts | 23 +++++-- .../backend/src/middleware/tenant/index.ts | 10 +-- 5 files changed, 54 insertions(+), 60 deletions(-) diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 9ac7c1bf59..aa46528a58 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -228,7 +228,7 @@ export interface TenantedApolloContext extends ApolloContext { } export interface ForTenantIdContext extends TenantedApolloContext { - forTenantId: string + forTenantId?: string } export interface AppServices { diff --git a/packages/backend/src/graphql/middleware/index.ts b/packages/backend/src/graphql/middleware/index.ts index 03a15b7726..f2b7d0ff53 100644 --- a/packages/backend/src/graphql/middleware/index.ts +++ b/packages/backend/src/graphql/middleware/index.ts @@ -9,7 +9,6 @@ import { CacheDataStore } from '../../middleware/cache/data-stores' import { lockMiddleware, Lock } from '../../middleware/lock' import { cacheMiddleware } from '../../middleware/cache' import { validateTenantMiddleware } from '../../middleware/tenant' -import { GraphQLErrorCode } from '../errors' export function lockGraphQLMutationMiddleware(lock: Lock): { Mutation: IMiddleware @@ -61,17 +60,7 @@ export function setForTenantIdGraphQLMutationMiddleware(): { return validateTenantMiddleware({ deps: { context }, next: () => resolve(root, args, context, info), - tenantIdInput: args?.input?.tenantId, - onFailValidation: () => { - throw new GraphQLError( - `Assignment to the specified tenant is not permitted`, - { - extensions: { - code: GraphQLErrorCode.BadUserInput - } - } - ) - } + tenantIdInput: args?.input?.tenantId }) } } diff --git a/packages/backend/src/graphql/resolvers/wallet_address.test.ts b/packages/backend/src/graphql/resolvers/wallet_address.test.ts index 3b0b905554..63023094c8 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.test.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.test.ts @@ -706,7 +706,6 @@ describe('Wallet Address Resolvers', (): void => { idpSecret: 'test-idp-secret-new' } const newTenant = await Tenant.query(knex).insertAndFetch(tenantOptions) - const newAsset = await assetService.create({ code: 'USD', scale: 2, @@ -845,39 +844,42 @@ describe('Wallet Address Resolvers', (): void => { // Attempt to switch tenant: try { - const queryTenant = await appContainer.apolloClient - .query({ - query: gql` - query WalletAddress($walletAddressId: String!, $tenantId: String) { - walletAddress(id: $walletAddressId, tenantId: $tenantId) { - id - liquidity - asset { - code - scale - } - url - publicName - additionalProperties { - key - value - visibleInOpenPayments - } + await appContainer.apolloClient + .query({ + query: gql` + query WalletAddress( + $walletAddressId: String! + $tenantId: String + ) { + walletAddress(id: $walletAddressId, tenantId: $tenantId) { + id + liquidity + asset { + code + scale } + url + publicName + additionalProperties { + key + value + visibleInOpenPayments + } + } } - `, - variables: { - walletAddressId: walletAddress.id, - tenantId: 'ae4950b6-3e1b-4e50-ad24-25c065bdd3a9', - } - }) - .then((query): WalletAddress => { - if (query.data) { - return query.data.walletAddress - } else { - throw new Error('Data was empty') - } - }) + `, + variables: { + walletAddressId: walletAddress.id, + tenantId: 'ae4950b6-3e1b-4e50-ad24-25c065bdd3a9' + } + }) + .then((queryTenant): WalletAddress => { + if (queryTenant.data) { + return queryTenant.data.walletAddress + } else { + throw new Error('Data was empty') + } + }) } catch (error) { expect(error).toBeInstanceOf(ApolloError) expect((error as ApolloError).graphQLErrors).toContainEqual( diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index 0ba1b48840..48c8d32b64 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -122,9 +122,20 @@ export const createWalletAddress: MutationResolvers['createW addProps.push(toAdd) }) + const tenantId = ctx.forTenantId + if (!tenantId) + throw new GraphQLError( + `Assignment to the specified tenant is not permitted`, + { + extensions: { + code: GraphQLErrorCode.BadUserInput + } + } + ) + const options: CreateOptions = { assetId: args.input.assetId, - tenantId: ctx.forTenantId, + tenantId, additionalProperties: addProps, publicName: args.input.publicName, url: args.input.url @@ -156,11 +167,11 @@ export const updateWalletAddress: MutationResolvers['updateW ...rest } - const existing = await walletAddressService.get(updateOptions.id) - if ( - existing && - !tenantIdToProceed(ctx.isOperator, ctx.forTenantId, existing.tenantId) - ) { + const existing = await walletAddressService.get( + updateOptions.id, + ctx.forTenantId + ) + if (!existing) { throw new GraphQLError(`Unknown wallet address`, { extensions: { code: GraphQLErrorCode.NotFound diff --git a/packages/backend/src/middleware/tenant/index.ts b/packages/backend/src/middleware/tenant/index.ts index e0ef5b27e1..455442c396 100644 --- a/packages/backend/src/middleware/tenant/index.ts +++ b/packages/backend/src/middleware/tenant/index.ts @@ -6,7 +6,6 @@ type Request = () => Promise interface TenantValidateMiddlewareArgs { deps: { context: TenantedApolloContext } tenantIdInput: string | undefined - onFailValidation: Request next: Request } @@ -16,19 +15,12 @@ export async function validateTenantMiddleware( const { deps: { context }, tenantIdInput, - onFailValidation, next } = args - const forTenantId = tenantIdToProceed( + ;(context as ForTenantIdContext).forTenantId = tenantIdToProceed( context.isOperator, context.tenant.id, tenantIdInput ) - if (!forTenantId) { - context.logger.error('Tenant validation error') - return onFailValidation() - } - - ;(context as ForTenantIdContext).forTenantId = forTenantId return next() } From b29636ac905c32a61d9820e38cf067b2b19b9da3 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Thu, 23 Jan 2025 13:35:47 +0100 Subject: [PATCH 60/66] feat(3114): fix the integration test. --- .../payment/incoming_remote/service.ts | 7 ++++--- packages/backend/src/shared/utils.test.ts | 12 +++++++++++- packages/backend/src/shared/utils.ts | 8 ++++++++ test/integration/lib/test-actions/index.ts | 9 +++++---- test/integration/lib/test-actions/open-payments.ts | 13 +++++++------ 5 files changed, 35 insertions(+), 14 deletions(-) diff --git a/packages/backend/src/open_payments/payment/incoming_remote/service.ts b/packages/backend/src/open_payments/payment/incoming_remote/service.ts index 72c830e5c5..42e3771e47 100644 --- a/packages/backend/src/open_payments/payment/incoming_remote/service.ts +++ b/packages/backend/src/open_payments/payment/incoming_remote/service.ts @@ -13,6 +13,7 @@ import { BaseService } from '../../../shared/baseService' import { Amount, serializeAmount } from '../../amount' import { RemoteIncomingPaymentError } from './errors' import { isGrantError } from '../../grant/errors' +import { urlWithoutTenantId } from '../../../shared/utils' interface CreateRemoteIncomingPaymentArgs { walletAddressUrl: string @@ -102,7 +103,7 @@ async function createIncomingPayment( walletAddress.resourceServer ?? new URL(walletAddress.id).origin const grantOptions = { - authServer: walletAddress.authServer, + authServer: urlWithoutTenantId(walletAddress.authServer), accessType: AccessType.IncomingPayment, accessActions: [AccessAction.Create, AccessAction.ReadAll] } @@ -116,7 +117,7 @@ async function createIncomingPayment( try { return await deps.openPaymentsClient.incomingPayment.create( { - url: resourceServerUrl, + url: urlWithoutTenantId(resourceServerUrl), accessToken: grant.accessToken }, { @@ -216,7 +217,7 @@ async function getIncomingPayment( OpenPaymentsIncomingPaymentWithPaymentMethods | RemoteIncomingPaymentError > { const grantOptions = { - authServer: authServerUrl, + authServer: urlWithoutTenantId(authServerUrl), accessType: AccessType.IncomingPayment, accessActions: [AccessAction.ReadAll] } diff --git a/packages/backend/src/shared/utils.test.ts b/packages/backend/src/shared/utils.test.ts index 5fa16ac9ac..c3841cf281 100644 --- a/packages/backend/src/shared/utils.test.ts +++ b/packages/backend/src/shared/utils.test.ts @@ -11,7 +11,8 @@ import { sleep, getTenantFromApiSignature, ensureTrailingSlash, - tenantIdToProceed + tenantIdToProceed, + urlWithoutTenantId } from './utils' import { AppServices, AppContext } from '../app' import { TestContainer, createTestApp } from '../tests/app' @@ -463,4 +464,13 @@ describe('utils', (): void => { expect(tenantIdToProceed(true, sig)).toBe(sig) expect(tenantIdToProceed(true, sig, tenantId)).toBe(tenantId) }) + + test('test tenant id stripped from url', async (): Promise => { + expect( + urlWithoutTenantId( + 'http://happy-life-bank-test-auth:4106/cf5fd7d3-1eb1-4041-8e43-ba45747e9e5d' + ) + ).toBe('http://happy-life-bank-test-auth:4106') + expect(urlWithoutTenantId('http://happy-life')).toBe('http://happy-life') + }) }) diff --git a/packages/backend/src/shared/utils.ts b/packages/backend/src/shared/utils.ts index 29b0ce0a25..663456f7e3 100644 --- a/packages/backend/src/shared/utils.ts +++ b/packages/backend/src/shared/utils.ts @@ -264,3 +264,11 @@ export function tenantIdToProceed( ? undefined : signatureTenantId } + +/** + * @param url remove the tenant id from the {url} + */ +export function urlWithoutTenantId(url: string): string { + if (url.length > 36 && validateId(url.slice(-36))) return url.slice(0, -37) + return url +} diff --git a/test/integration/lib/test-actions/index.ts b/test/integration/lib/test-actions/index.ts index c47998ef2e..e829b52c08 100644 --- a/test/integration/lib/test-actions/index.ts +++ b/test/integration/lib/test-actions/index.ts @@ -4,6 +4,7 @@ import { parseCookies } from '../utils' import { WalletAddress, PendingGrant } from '@interledger/open-payments' import { AdminActions, createAdminActions } from './admin' import { OpenPaymentsActions, createOpenPaymentsActions } from './open-payments' +import { urlWithoutTenantId } from '../../../../packages/backend/src/shared/utils' export interface TestActionsDeps { sendingASE: MockASE @@ -54,9 +55,9 @@ async function consentInteraction( idpSecret ) - // Finish interacton + // Finish interaction const finishResponse = await fetch( - `${senderWalletAddress.authServer}/interact/${interactId}/${nonce}/finish`, + `${urlWithoutTenantId(senderWalletAddress.authServer)}/interact/${interactId}/${nonce}/finish`, { method: 'GET', headers: { @@ -81,9 +82,9 @@ async function consentInteractionWithInteractRef( idpSecret ) - // Finish interacton + // Finish interaction const finishResponse = await fetch( - `${senderWalletAddress.authServer}/interact/${interactId}/${nonce}/finish`, + `${urlWithoutTenantId(senderWalletAddress.authServer)}/interact/${interactId}/${nonce}/finish`, { method: 'GET', headers: { diff --git a/test/integration/lib/test-actions/open-payments.ts b/test/integration/lib/test-actions/open-payments.ts index 6f54f25b68..d39a026b89 100644 --- a/test/integration/lib/test-actions/open-payments.ts +++ b/test/integration/lib/test-actions/open-payments.ts @@ -18,6 +18,7 @@ import { CreateOutgoingPaymentArgs, CreateIncomingPaymentArgs } from '@interledger/open-payments/dist/types' +import { urlWithoutTenantId } from '../../../../packages/backend/src/shared/utils' export interface OpenPaymentsActionsDeps { sendingASE: MockASE @@ -98,7 +99,7 @@ async function grantRequestIncomingPayment( const grant = await sendingASE.opClient.grant.request( { - url: receiverWalletAddress.authServer + url: urlWithoutTenantId(receiverWalletAddress.authServer) }, { access_token: { @@ -152,7 +153,7 @@ async function createIncomingPayment( const incomingPayment = await sendingASE.opClient.incomingPayment.create( { - url: receiverWalletAddress.resourceServer, + url: urlWithoutTenantId(receiverWalletAddress.resourceServer), accessToken }, createInput @@ -185,7 +186,7 @@ async function grantRequestQuote( const { sendingASE } = deps const grant = await sendingASE.opClient.grant.request( { - url: senderWalletAddress.authServer + url: urlWithoutTenantId(senderWalletAddress.authServer) }, { access_token: { @@ -211,7 +212,7 @@ async function createQuote( const { sendingASE } = deps return await sendingASE.opClient.quote.create( { - url: senderWalletAddress.resourceServer, + url: urlWithoutTenantId(senderWalletAddress.resourceServer), accessToken }, { @@ -235,7 +236,7 @@ async function grantRequestOutgoingPayment( const { receivingASE } = deps const grant = await receivingASE.opClient.grant.request( { - url: senderWalletAddress.authServer + url: urlWithoutTenantId(senderWalletAddress.authServer) }, { access_token: { @@ -319,7 +320,7 @@ async function createOutgoingPayment( const outgoingPayment = await sendingASE.opClient.outgoingPayment.create( { - url: senderWalletAddress.resourceServer, + url: urlWithoutTenantId(senderWalletAddress.resourceServer), accessToken: grantContinue.access_token.value }, { From e16cb6563ccaac64c51837cd198c18a63b008219 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Thu, 23 Jan 2025 15:38:12 +0100 Subject: [PATCH 61/66] feat(3114): further review comments. --- .../generated/graphql.ts | 1 + .../src/graphql/generated/graphql.schema.json | 12 +++++ .../backend/src/graphql/generated/graphql.ts | 1 + .../graphql/resolvers/wallet_address.test.ts | 48 +++++++++++++++++++ .../src/graphql/resolvers/wallet_address.ts | 31 +++++++++--- packages/backend/src/graphql/schema.graphql | 7 ++- .../open_payments/wallet_address/service.ts | 13 +++-- packages/frontend/app/generated/graphql.ts | 1 + .../src/generated/graphql.ts | 1 + test/integration/lib/generated/graphql.ts | 1 + 10 files changed, 105 insertions(+), 11 deletions(-) diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 1ee5eb4488..b61b3bf836 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -1256,6 +1256,7 @@ export type QueryWalletAddressArgs = { export type QueryWalletAddressByUrlArgs = { + tenantId?: InputMaybe; url: Scalars['String']['input']; }; diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index 6930bd5cd4..1ca0c40f46 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -6867,6 +6867,18 @@ "name": "walletAddressByUrl", "description": "Get a wallet address by its url if it exists", "args": [ + { + "name": "tenantId", + "description": "Unique identifier of the tenant associated with the wallet address. Optional, if not provided, the tenantId will be obtained from the signature.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "url", "description": "Wallet Address URL.", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 1ee5eb4488..b61b3bf836 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -1256,6 +1256,7 @@ export type QueryWalletAddressArgs = { export type QueryWalletAddressByUrlArgs = { + tenantId?: InputMaybe; url: Scalars['String']['input']; }; diff --git a/packages/backend/src/graphql/resolvers/wallet_address.test.ts b/packages/backend/src/graphql/resolvers/wallet_address.test.ts index 63023094c8..fb4745dc11 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.test.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.test.ts @@ -844,6 +844,7 @@ describe('Wallet Address Resolvers', (): void => { // Attempt to switch tenant: try { + expect.assertions(3) await appContainer.apolloClient .query({ query: gql` @@ -951,6 +952,53 @@ describe('Wallet Address Resolvers', (): void => { publicName: publicName ?? null, additionalProperties: [] }) + + expect.assertions(3) + try { + await appContainer.apolloClient + .query({ + query: gql` + query getWalletAddressByUrl($url: String!, $tenantId: String) { + walletAddressByUrl(url: $url, tenantId: $tenantId) { + id + liquidity + asset { + code + scale + } + url + publicName + additionalProperties { + key + value + visibleInOpenPayments + } + } + } + `, + variables: { + url: walletAddress.url, + tenantId: 'ae4950b6-3e1b-4e50-ad24-25c065bdd3a9' + } + }) + .then((query): WalletAddress => { + if (query.data) { + return query.data.walletAddressByUrl + } else { + throw new Error('Data was empty') + } + }) + } catch (error) { + expect(error).toBeInstanceOf(ApolloError) + expect((error as ApolloError).graphQLErrors).toContainEqual( + expect.objectContaining({ + message: 'unknown wallet address', + extensions: expect.objectContaining({ + code: GraphQLErrorCode.NotFound + }) + }) + ) + } } ) diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index 48c8d32b64..cac9546ac4 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -77,7 +77,10 @@ export const getWalletAddress: QueryResolvers['walletAddr } const walletAddressService = await ctx.container.use('walletAddressService') - const walletAddress = await walletAddressService.get(args.id, tenantId) + const walletAddress = await walletAddressService.get( + args.id, + ctx.isOperator && !args.tenantId ? undefined : tenantId + ) if (!walletAddress) { throw new GraphQLError( errorToMessage[WalletAddressError.UnknownWalletAddress], @@ -97,12 +100,28 @@ export const getWalletAddressByUrl: QueryResolvers['walle args, ctx ): Promise => { + const tenantId = tenantIdToProceed( + ctx.isOperator, + ctx.tenant.id, + args.tenantId + ) + if (!tenantId) { + throw new GraphQLError( + errorToMessage[WalletAddressError.UnknownWalletAddress], + { + extensions: { + code: errorToCode[WalletAddressError.UnknownWalletAddress] + } + } + ) + } + const walletAddressService = await ctx.container.use('walletAddressService') - const walletAddress = await walletAddressService.getByUrl(args.url) - return walletAddress && - tenantIdToProceed(ctx.isOperator, ctx.tenant.id, walletAddress.tenantId) - ? walletAddressToGraphql(walletAddress) - : null + const walletAddress = await walletAddressService.getByUrl( + args.url, + ctx.isOperator && !args.tenantId ? undefined : tenantId + ) + return walletAddress ? walletAddressToGraphql(walletAddress) : null } export const createWalletAddress: MutationResolvers['createWalletAddress'] = diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 3138c5df9e..4182f3e131 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -58,7 +58,12 @@ type Query { ): WalletAddress "Get a wallet address by its url if it exists" - walletAddressByUrl("Wallet Address URL." url: String!): WalletAddress + walletAddressByUrl( + "Wallet Address URL." + url: String! + "Unique identifier of the tenant associated with the wallet address. Optional, if not provided, the tenantId will be obtained from the signature." + tenantId: String + ): WalletAddress "Fetch a paginated list of wallet addresses." walletAddresses( diff --git a/packages/backend/src/open_payments/wallet_address/service.ts b/packages/backend/src/open_payments/wallet_address/service.ts index c04a2720fa..44d358df7d 100644 --- a/packages/backend/src/open_payments/wallet_address/service.ts +++ b/packages/backend/src/open_payments/wallet_address/service.ts @@ -66,7 +66,7 @@ export interface WalletAddressService { includeVisibleOnlyAddProps: boolean ): Promise get(id: string, tenantId?: string): Promise - getByUrl(url: string): Promise + getByUrl(url: string, tenantId?: string): Promise getOrPollByUrl(url: string): Promise getPage( pagination?: Pagination, @@ -117,7 +117,7 @@ export async function createWalletAddressService({ includeVisibleOnlyAddProps ), get: (id, tenantId) => getWalletAddress(deps, id, tenantId), - getByUrl: (url) => getWalletAddressByUrl(deps, url), + getByUrl: (url, tenantId) => getWalletAddressByUrl(deps, url, tenantId), getOrPollByUrl: (url) => getOrPollByUrl(deps, url), getPage: (pagination?, sortOrder?, tenantId?) => getWalletAddressPage(deps, pagination, sortOrder, tenantId), @@ -339,9 +339,14 @@ async function getOrPollByUrl( async function getWalletAddressByUrl( deps: ServiceDependencies, - url: string + url: string, + tenantId?: string ): Promise { - const walletAddress = await WalletAddress.query(deps.knex).findOne({ + const query = WalletAddress.query(deps.knex) + if (tenantId) { + query.andWhere({ tenantId }) + } + const walletAddress = await query.findOne({ url: url.toLowerCase() }) if (walletAddress) { diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index 78028ee916..5140605c0b 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -1256,6 +1256,7 @@ export type QueryWalletAddressArgs = { export type QueryWalletAddressByUrlArgs = { + tenantId?: InputMaybe; url: Scalars['String']['input']; }; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 1ee5eb4488..b61b3bf836 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -1256,6 +1256,7 @@ export type QueryWalletAddressArgs = { export type QueryWalletAddressByUrlArgs = { + tenantId?: InputMaybe; url: Scalars['String']['input']; }; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 1ee5eb4488..b61b3bf836 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -1256,6 +1256,7 @@ export type QueryWalletAddressArgs = { export type QueryWalletAddressByUrlArgs = { + tenantId?: InputMaybe; url: Scalars['String']['input']; }; From e6b44d7a42271d9bb9e22451bb68f4c7f3e9090c Mon Sep 17 00:00:00 2001 From: koekiebox Date: Fri, 24 Jan 2025 12:08:25 +0100 Subject: [PATCH 62/66] feat(3114): further review comments. --- .../src/graphql/resolvers/wallet_address.ts | 4 +--- .../open_payments/wallet_address/service.ts | 19 +++++-------------- pnpm-lock.yaml | 3 +++ test/integration/lib/test-actions/index.ts | 3 +-- .../lib/test-actions/open-payments.ts | 9 +++++++-- test/integration/lib/utils.ts | 16 +++++++++++++++- test/integration/package.json | 3 ++- 7 files changed, 34 insertions(+), 23 deletions(-) diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index cac9546ac4..5506ae228b 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -36,12 +36,10 @@ export const getWalletAddresses: QueryResolvers['walletAd const { tenantId, sortOrder, ...pagination } = args const order = sortOrder === 'ASC' ? SortOrder.Asc : SortOrder.Desc - const tenantForLookup = - tenantId && ctx.isOperator ? tenantId : ctx.tenant.id const walletAddresses = await walletAddressService.getPage( pagination, order, - tenantForLookup + ctx.isOperator ? tenantId : ctx.tenant.id ) const pageInfo = await getPageInfo({ getPage: (pagination: Pagination, sortOrder?: SortOrder) => diff --git a/packages/backend/src/open_payments/wallet_address/service.ts b/packages/backend/src/open_payments/wallet_address/service.ts index 44d358df7d..c5f5d852d8 100644 --- a/packages/backend/src/open_payments/wallet_address/service.ts +++ b/packages/backend/src/open_payments/wallet_address/service.ts @@ -170,10 +170,7 @@ async function createWalletAddress( } try { - const tenantId = options.tenantId - ? options.tenantId - : deps.config.operatorTenantId - const asset = await deps.assetService.get(options.assetId, tenantId) + const asset = await deps.assetService.get(options.assetId, options.tenantId) if (!asset) return WalletAddressError.UnknownAsset // Remove blank key/value pairs: @@ -277,9 +274,7 @@ async function getWalletAddress( } const query = WalletAddress.query(deps.knex) - if (tenantId) { - query.andWhere({ tenantId }) - } + if (tenantId) query.andWhere({ tenantId }) const walletAddress = await query.findById(id) if (walletAddress) { @@ -343,9 +338,8 @@ async function getWalletAddressByUrl( tenantId?: string ): Promise { const query = WalletAddress.query(deps.knex) - if (tenantId) { - query.andWhere({ tenantId }) - } + if (tenantId) query.andWhere({ tenantId }) + const walletAddress = await query.findOne({ url: url.toLowerCase() }) @@ -363,10 +357,7 @@ async function getWalletAddressPage( tenantId?: string ): Promise { const query = WalletAddress.query(deps.knex) - - if (tenantId && tenantId.length > 0) { - query.where({ tenantId }) - } + if (tenantId) query.where({ tenantId }) const addresses = await query.getPage(pagination, sortOrder) for (const address of addresses) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4eb0b61cf..f0840fe900 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -794,6 +794,9 @@ importers: mock-account-service-lib: specifier: workspace:* version: link:../../packages/mock-account-service-lib + uuid: + specifier: ^9.0.1 + version: 9.0.1 yaml: specifier: ^2.7.0 version: 2.7.0 diff --git a/test/integration/lib/test-actions/index.ts b/test/integration/lib/test-actions/index.ts index e829b52c08..253bd04697 100644 --- a/test/integration/lib/test-actions/index.ts +++ b/test/integration/lib/test-actions/index.ts @@ -1,10 +1,9 @@ import assert from 'assert' import { MockASE } from '../mock-ase' -import { parseCookies } from '../utils' +import { parseCookies, urlWithoutTenantId } from '../utils' import { WalletAddress, PendingGrant } from '@interledger/open-payments' import { AdminActions, createAdminActions } from './admin' import { OpenPaymentsActions, createOpenPaymentsActions } from './open-payments' -import { urlWithoutTenantId } from '../../../../packages/backend/src/shared/utils' export interface TestActionsDeps { sendingASE: MockASE diff --git a/test/integration/lib/test-actions/open-payments.ts b/test/integration/lib/test-actions/open-payments.ts index d39a026b89..0f3dc23c61 100644 --- a/test/integration/lib/test-actions/open-payments.ts +++ b/test/integration/lib/test-actions/open-payments.ts @@ -12,13 +12,18 @@ import { isPendingGrant } from '@interledger/open-payments' import { MockASE } from '../mock-ase' -import { UnionOmit, poll, pollCondition, wait } from '../utils' +import { + UnionOmit, + poll, + pollCondition, + wait, + urlWithoutTenantId +} from '../utils' import { WebhookEventType } from 'mock-account-service-lib' import { CreateOutgoingPaymentArgs, CreateIncomingPaymentArgs } from '@interledger/open-payments/dist/types' -import { urlWithoutTenantId } from '../../../../packages/backend/src/shared/utils' export interface OpenPaymentsActionsDeps { sendingASE: MockASE diff --git a/test/integration/lib/utils.ts b/test/integration/lib/utils.ts index 7c1f47cbbc..4a3cb56482 100644 --- a/test/integration/lib/utils.ts +++ b/test/integration/lib/utils.ts @@ -1,3 +1,5 @@ +import { validate, version } from 'uuid' + export function wait(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) } @@ -62,7 +64,7 @@ export function parseCookies(response: Response) { } /** - * Omit distrubuted to all types in a union. + * Omit distributed to all types in a union. * @example * type WithoutA = UnionOmit<{ a: number; c: number } | { b: number }, 'a'> // { c: number } | { b: number } * const withoutAOK: WithoutA = { c: 1 } // OK @@ -73,3 +75,15 @@ export function parseCookies(response: Response) { export type UnionOmit = T extends any ? Omit : never + +/** + * @param url remove the tenant id from the {url} + */ +export function urlWithoutTenantId(url: string): string { + if (url.length > 36 && validateId(url.slice(-36))) return url.slice(0, -37) + return url +} + +function validateId(id: string): boolean { + return validate(id) && version(id) === 4 +} diff --git a/test/integration/package.json b/test/integration/package.json index 6f8d221915..efbbd22699 100644 --- a/test/integration/package.json +++ b/test/integration/package.json @@ -27,6 +27,7 @@ "json-canonicalize": "^1.0.6", "koa": "^2.15.3", "mock-account-service-lib": "workspace:*", - "yaml": "^2.7.0" + "yaml": "^2.7.0", + "uuid": "^9.0.1" } } From 961c249635dc667adee770fec7e596cc1b23b466 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Fri, 24 Jan 2025 12:46:38 +0100 Subject: [PATCH 63/66] feat(3114): final round with Max. --- .../generated/graphql.ts | 2 - .../src/graphql/generated/graphql.schema.json | 24 ----- .../backend/src/graphql/generated/graphql.ts | 2 - .../graphql/resolvers/wallet_address.test.ts | 98 ------------------- .../src/graphql/resolvers/wallet_address.ts | 37 ++----- packages/backend/src/graphql/schema.graphql | 9 +- .../src/middleware/tenant/index.test.ts | 13 +++ .../backend/src/middleware/tenant/index.ts | 24 ++++- packages/backend/src/shared/utils.test.ts | 11 --- packages/backend/src/shared/utils.ts | 23 ----- packages/backend/src/tests/walletAddress.ts | 6 +- packages/frontend/app/generated/graphql.ts | 2 - .../src/generated/graphql.ts | 2 - test/integration/lib/generated/graphql.ts | 2 - 14 files changed, 47 insertions(+), 208 deletions(-) create mode 100644 packages/backend/src/middleware/tenant/index.test.ts diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index b61b3bf836..4bc9d24de2 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -1251,12 +1251,10 @@ export type QueryReceiverArgs = { export type QueryWalletAddressArgs = { id: Scalars['String']['input']; - tenantId?: InputMaybe; }; export type QueryWalletAddressByUrlArgs = { - tenantId?: InputMaybe; url: Scalars['String']['input']; }; diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index 1ca0c40f46..db39137300 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -6841,18 +6841,6 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null - }, - { - "name": "tenantId", - "description": "Unique identifier of the tenant associated with the wallet address. Optional, if not provided, the tenantId will be obtained from the signature.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null } ], "type": { @@ -6867,18 +6855,6 @@ "name": "walletAddressByUrl", "description": "Get a wallet address by its url if it exists", "args": [ - { - "name": "tenantId", - "description": "Unique identifier of the tenant associated with the wallet address. Optional, if not provided, the tenantId will be obtained from the signature.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "url", "description": "Wallet Address URL.", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index b61b3bf836..4bc9d24de2 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -1251,12 +1251,10 @@ export type QueryReceiverArgs = { export type QueryWalletAddressArgs = { id: Scalars['String']['input']; - tenantId?: InputMaybe; }; export type QueryWalletAddressByUrlArgs = { - tenantId?: InputMaybe; url: Scalars['String']['input']; }; diff --git a/packages/backend/src/graphql/resolvers/wallet_address.test.ts b/packages/backend/src/graphql/resolvers/wallet_address.test.ts index fb4745dc11..88ae1d78d8 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.test.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.test.ts @@ -841,57 +841,6 @@ describe('Wallet Address Resolvers', (): void => { } ] }) - - // Attempt to switch tenant: - try { - expect.assertions(3) - await appContainer.apolloClient - .query({ - query: gql` - query WalletAddress( - $walletAddressId: String! - $tenantId: String - ) { - walletAddress(id: $walletAddressId, tenantId: $tenantId) { - id - liquidity - asset { - code - scale - } - url - publicName - additionalProperties { - key - value - visibleInOpenPayments - } - } - } - `, - variables: { - walletAddressId: walletAddress.id, - tenantId: 'ae4950b6-3e1b-4e50-ad24-25c065bdd3a9' - } - }) - .then((queryTenant): WalletAddress => { - if (queryTenant.data) { - return queryTenant.data.walletAddress - } else { - throw new Error('Data was empty') - } - }) - } catch (error) { - expect(error).toBeInstanceOf(ApolloError) - expect((error as ApolloError).graphQLErrors).toContainEqual( - expect.objectContaining({ - message: 'unknown wallet address', - extensions: expect.objectContaining({ - code: GraphQLErrorCode.NotFound - }) - }) - ) - } } ) @@ -952,53 +901,6 @@ describe('Wallet Address Resolvers', (): void => { publicName: publicName ?? null, additionalProperties: [] }) - - expect.assertions(3) - try { - await appContainer.apolloClient - .query({ - query: gql` - query getWalletAddressByUrl($url: String!, $tenantId: String) { - walletAddressByUrl(url: $url, tenantId: $tenantId) { - id - liquidity - asset { - code - scale - } - url - publicName - additionalProperties { - key - value - visibleInOpenPayments - } - } - } - `, - variables: { - url: walletAddress.url, - tenantId: 'ae4950b6-3e1b-4e50-ad24-25c065bdd3a9' - } - }) - .then((query): WalletAddress => { - if (query.data) { - return query.data.walletAddressByUrl - } else { - throw new Error('Data was empty') - } - }) - } catch (error) { - expect(error).toBeInstanceOf(ApolloError) - expect((error as ApolloError).graphQLErrors).toContainEqual( - expect.objectContaining({ - message: 'unknown wallet address', - extensions: expect.objectContaining({ - code: GraphQLErrorCode.NotFound - }) - }) - ) - } } ) diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index 5506ae228b..ce5bbb19fe 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -23,7 +23,6 @@ import { CreateOptions, UpdateOptions } from '../../open_payments/wallet_address/service' -import { tenantIdToProceed } from '../../shared/utils' import { GraphQLErrorCode } from '../errors' export const getWalletAddresses: QueryResolvers['walletAddresses'] = @@ -58,26 +57,10 @@ export const getWalletAddresses: QueryResolvers['walletAd export const getWalletAddress: QueryResolvers['walletAddress'] = async (parent, args, ctx): Promise => { - const tenantId = tenantIdToProceed( - ctx.isOperator, - ctx.tenant.id, - args.tenantId - ) - if (!tenantId) { - throw new GraphQLError( - errorToMessage[WalletAddressError.UnknownWalletAddress], - { - extensions: { - code: errorToCode[WalletAddressError.UnknownWalletAddress] - } - } - ) - } - const walletAddressService = await ctx.container.use('walletAddressService') const walletAddress = await walletAddressService.get( args.id, - ctx.isOperator && !args.tenantId ? undefined : tenantId + ctx.isOperator ? undefined : ctx.tenant.id ) if (!walletAddress) { throw new GraphQLError( @@ -98,12 +81,12 @@ export const getWalletAddressByUrl: QueryResolvers['walle args, ctx ): Promise => { - const tenantId = tenantIdToProceed( - ctx.isOperator, - ctx.tenant.id, - args.tenantId + const walletAddressService = await ctx.container.use('walletAddressService') + const walletAddress = await walletAddressService.getByUrl( + args.url, + ctx.isOperator ? undefined : ctx.tenant.id ) - if (!tenantId) { + if (!walletAddress) { throw new GraphQLError( errorToMessage[WalletAddressError.UnknownWalletAddress], { @@ -113,13 +96,7 @@ export const getWalletAddressByUrl: QueryResolvers['walle } ) } - - const walletAddressService = await ctx.container.use('walletAddressService') - const walletAddress = await walletAddressService.getByUrl( - args.url, - ctx.isOperator && !args.tenantId ? undefined : tenantId - ) - return walletAddress ? walletAddressToGraphql(walletAddress) : null + return walletAddressToGraphql(walletAddress) } export const createWalletAddress: MutationResolvers['createWalletAddress'] = diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 4182f3e131..b80f5c0df9 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -53,17 +53,10 @@ type Query { walletAddress( "Unique identifier of the wallet address." id: String! - "Unique identifier of the tenant associated with the wallet address. Optional, if not provided, the tenantId will be obtained from the signature." - tenantId: String ): WalletAddress "Get a wallet address by its url if it exists" - walletAddressByUrl( - "Wallet Address URL." - url: String! - "Unique identifier of the tenant associated with the wallet address. Optional, if not provided, the tenantId will be obtained from the signature." - tenantId: String - ): WalletAddress + walletAddressByUrl("Wallet Address URL." url: String!): WalletAddress "Fetch a paginated list of wallet addresses." walletAddresses( diff --git a/packages/backend/src/middleware/tenant/index.test.ts b/packages/backend/src/middleware/tenant/index.test.ts new file mode 100644 index 0000000000..38d1288313 --- /dev/null +++ b/packages/backend/src/middleware/tenant/index.test.ts @@ -0,0 +1,13 @@ +import { tenantIdToProceed } from './index' + +describe('Set For Tenant', (): void => { + test('test tenant id to proceed', async (): Promise => { + const sig = 'sig' + const tenantId = 'tenantId' + expect(tenantIdToProceed(false, sig)).toBe(sig) + expect(tenantIdToProceed(false, sig, tenantId)).toBeUndefined() + expect(tenantIdToProceed(false, sig, sig)).toBe(sig) + expect(tenantIdToProceed(true, sig)).toBe(sig) + expect(tenantIdToProceed(true, sig, tenantId)).toBe(tenantId) + }) +}) diff --git a/packages/backend/src/middleware/tenant/index.ts b/packages/backend/src/middleware/tenant/index.ts index 455442c396..a90f75670b 100644 --- a/packages/backend/src/middleware/tenant/index.ts +++ b/packages/backend/src/middleware/tenant/index.ts @@ -1,5 +1,4 @@ import { ForTenantIdContext, TenantedApolloContext } from '../../app' -import { tenantIdToProceed } from '../../shared/utils' type Request = () => Promise @@ -24,3 +23,26 @@ export async function validateTenantMiddleware( ) return next() } + +/** + * The tenantId to use will be determined as follows: + * - When an operator and the {tenantId} is present, return {tenantId} + * - When an operator and {tenantId} is not present, return {signatureTenantId} + * - When NOT an operator and {tenantId} is present, but does not match {signatureTenantId}, return {undefined} + * - Otherwise return {signatureTenantId} + * + * @param isOperator is operator + * @param signatureTenantId the signature tenantId + * @param tenantId the intended tenantId + */ +export function tenantIdToProceed( + isOperator: boolean, + signatureTenantId: string, + tenantId?: string +): string | undefined { + if (isOperator && tenantId) return tenantId + else if (isOperator) return signatureTenantId + return tenantId && tenantId !== signatureTenantId + ? undefined + : signatureTenantId +} diff --git a/packages/backend/src/shared/utils.test.ts b/packages/backend/src/shared/utils.test.ts index c3841cf281..fac329100b 100644 --- a/packages/backend/src/shared/utils.test.ts +++ b/packages/backend/src/shared/utils.test.ts @@ -11,7 +11,6 @@ import { sleep, getTenantFromApiSignature, ensureTrailingSlash, - tenantIdToProceed, urlWithoutTenantId } from './utils' import { AppServices, AppContext } from '../app' @@ -455,16 +454,6 @@ describe('utils', (): void => { expect(ensureTrailingSlash(`${path}/`)).toBe(`${path}/`) }) - test('test tenant id to proceed', async (): Promise => { - const sig = 'sig' - const tenantId = 'tenantId' - expect(tenantIdToProceed(false, sig)).toBe(sig) - expect(tenantIdToProceed(false, sig, tenantId)).toBeUndefined() - expect(tenantIdToProceed(false, sig, sig)).toBe(sig) - expect(tenantIdToProceed(true, sig)).toBe(sig) - expect(tenantIdToProceed(true, sig, tenantId)).toBe(tenantId) - }) - test('test tenant id stripped from url', async (): Promise => { expect( urlWithoutTenantId( diff --git a/packages/backend/src/shared/utils.ts b/packages/backend/src/shared/utils.ts index 663456f7e3..8cd9b6d63e 100644 --- a/packages/backend/src/shared/utils.ts +++ b/packages/backend/src/shared/utils.ts @@ -242,29 +242,6 @@ export function ensureTrailingSlash(str: string): string { return str } -/** - * The tenantId to use will be determined as follows: - * - When an operator and the {tenantId} is present, return {tenantId} - * - When an operator and {tenantId} is not present, return {signatureTenantId} - * - When NOT an operator and {tenantId} is present, but does not match {signatureTenantId}, return {undefined} - * - Otherwise return {signatureTenantId} - * - * @param isOperator is operator - * @param signatureTenantId the signature tenantId - * @param tenantId the intended tenantId - */ -export function tenantIdToProceed( - isOperator: boolean, - signatureTenantId: string, - tenantId?: string -): string | undefined { - if (isOperator && tenantId) return tenantId - else if (isOperator) return signatureTenantId - return tenantId && tenantId !== signatureTenantId - ? undefined - : signatureTenantId -} - /** * @param url remove the tenant id from the {url} */ diff --git a/packages/backend/src/tests/walletAddress.ts b/packages/backend/src/tests/walletAddress.ts index 3c319bff4d..cc3c55092d 100644 --- a/packages/backend/src/tests/walletAddress.ts +++ b/packages/backend/src/tests/walletAddress.ts @@ -30,12 +30,12 @@ export async function createWalletAddress( options: Partial = {} ): Promise { const walletAddressService = await deps.use('walletAddressService') + const tenantIdToUse = options.tenantId || (await createTenant(deps)).id const walletAddressOrError = (await walletAddressService.create({ ...options, assetId: - options.assetId || - (await createAsset(deps, undefined, options.tenantId)).id, - tenantId: options.tenantId || (await createTenant(deps)).id, + options.assetId || (await createAsset(deps, undefined, tenantIdToUse)).id, + tenantId: tenantIdToUse, url: options.url || `https://${faker.internet.domainName()}/.well-known/pay` })) as MockWalletAddress if (isWalletAddressError(walletAddressOrError)) { diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index 5140605c0b..117ed190d9 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -1251,12 +1251,10 @@ export type QueryReceiverArgs = { export type QueryWalletAddressArgs = { id: Scalars['String']['input']; - tenantId?: InputMaybe; }; export type QueryWalletAddressByUrlArgs = { - tenantId?: InputMaybe; url: Scalars['String']['input']; }; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index b61b3bf836..4bc9d24de2 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -1251,12 +1251,10 @@ export type QueryReceiverArgs = { export type QueryWalletAddressArgs = { id: Scalars['String']['input']; - tenantId?: InputMaybe; }; export type QueryWalletAddressByUrlArgs = { - tenantId?: InputMaybe; url: Scalars['String']['input']; }; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index b61b3bf836..4bc9d24de2 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -1251,12 +1251,10 @@ export type QueryReceiverArgs = { export type QueryWalletAddressArgs = { id: Scalars['String']['input']; - tenantId?: InputMaybe; }; export type QueryWalletAddressByUrlArgs = { - tenantId?: InputMaybe; url: Scalars['String']['input']; }; From fa1919859a535553fd9f1feb52e5dc9285a67318 Mon Sep 17 00:00:00 2001 From: koekiebox Date: Fri, 24 Jan 2025 13:13:32 +0100 Subject: [PATCH 64/66] feat(3114): revert. --- .../backend/src/graphql/resolvers/wallet_address.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index ce5bbb19fe..d4766b521d 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -86,17 +86,7 @@ export const getWalletAddressByUrl: QueryResolvers['walle args.url, ctx.isOperator ? undefined : ctx.tenant.id ) - if (!walletAddress) { - throw new GraphQLError( - errorToMessage[WalletAddressError.UnknownWalletAddress], - { - extensions: { - code: errorToCode[WalletAddressError.UnknownWalletAddress] - } - } - ) - } - return walletAddressToGraphql(walletAddress) + return walletAddress ? walletAddressToGraphql(walletAddress) : null } export const createWalletAddress: MutationResolvers['createWalletAddress'] = From ae157bd27e569a716a9ba2d003c34363cfce6a1e Mon Sep 17 00:00:00 2001 From: koekiebox Date: Fri, 24 Jan 2025 14:36:10 +0100 Subject: [PATCH 65/66] feat(3114): remove unused. --- .../backend/src/open_payments/wallet_address/errors.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/open_payments/wallet_address/errors.ts b/packages/backend/src/open_payments/wallet_address/errors.ts index 0c0acfe2db..03d672762c 100644 --- a/packages/backend/src/open_payments/wallet_address/errors.ts +++ b/packages/backend/src/open_payments/wallet_address/errors.ts @@ -4,8 +4,7 @@ export enum WalletAddressError { InvalidUrl = 'InvalidUrl', UnknownAsset = 'UnknownAsset', UnknownWalletAddress = 'UnknownWalletAddress', - DuplicateWalletAddress = 'DuplicateWalletAddress', - InvalidTenantIdNotAllowed = 'InvalidTenantIdNotAllowed' + DuplicateWalletAddress = 'DuplicateWalletAddress' } // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types @@ -18,8 +17,7 @@ export const errorToCode: { [WalletAddressError.InvalidUrl]: GraphQLErrorCode.BadUserInput, [WalletAddressError.UnknownAsset]: GraphQLErrorCode.BadUserInput, [WalletAddressError.UnknownWalletAddress]: GraphQLErrorCode.NotFound, - [WalletAddressError.DuplicateWalletAddress]: GraphQLErrorCode.Duplicate, - [WalletAddressError.InvalidTenantIdNotAllowed]: GraphQLErrorCode.BadUserInput + [WalletAddressError.DuplicateWalletAddress]: GraphQLErrorCode.Duplicate } export const errorToMessage: { @@ -29,7 +27,5 @@ export const errorToMessage: { [WalletAddressError.UnknownAsset]: 'unknown asset', [WalletAddressError.UnknownWalletAddress]: 'unknown wallet address', [WalletAddressError.DuplicateWalletAddress]: - 'Duplicate wallet address found with the same url', - [WalletAddressError.InvalidTenantIdNotAllowed]: - 'Assignment to the specified tenant is not permitted.' + 'Duplicate wallet address found with the same url' } From e8bc63c472169ad411043b451da203555eb3ce2c Mon Sep 17 00:00:00 2001 From: koekiebox Date: Sun, 26 Jan 2025 10:50:40 +0100 Subject: [PATCH 66/66] feat(3114): set the correct operator id. --- localenv/cloud-nine-wallet/docker-compose.yml | 1 + localenv/happy-life-bank/docker-compose.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/localenv/cloud-nine-wallet/docker-compose.yml b/localenv/cloud-nine-wallet/docker-compose.yml index d1883d5aa7..21973eae52 100644 --- a/localenv/cloud-nine-wallet/docker-compose.yml +++ b/localenv/cloud-nine-wallet/docker-compose.yml @@ -25,6 +25,7 @@ services: IDP_SECRET: 2pEcn2kkCclbOHQiGNEwhJ0rucATZhrA807HTm2rNXE= DISPLAY_NAME: Cloud Nine Wallet DISPLAY_ICON: wallet-icon.svg + OPERATOR_TENANT_ID: 438fa74a-fa7d-4317-9ced-dde32ece1787 volumes: - ../cloud-nine-wallet/seed.yml:/workspace/seed.yml - ../cloud-nine-wallet/private-key.pem:/workspace/private-key.pem diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index 467a1dec4c..54f8511b87 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -21,6 +21,7 @@ services: IDP_SECRET: 2pEcn2kkCclbOHQiGNEwhJ0rucATZhrA807HTm2rNXE= DISPLAY_NAME: Happy Life Bank DISPLAY_ICON: bank-icon.svg + OPERATOR_TENANT_ID: cf5fd7d3-1eb1-4041-8e43-ba45747e9e5d volumes: - ../happy-life-bank/seed.yml:/workspace/seed.yml - ../happy-life-bank/private-key.pem:/workspace/private-key.pem