Skip to content

Commit a3ba1dd

Browse files
committed
fix: use tenantId properly during asset creation
1 parent ee1bcbe commit a3ba1dd

File tree

13 files changed

+149
-55
lines changed

13 files changed

+149
-55
lines changed

localenv/mock-account-servicing-entity/generated/graphql.ts

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend/src/graphql/generated/graphql.schema.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend/src/graphql/generated/graphql.ts

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend/src/graphql/resolvers/asset.test.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import assert from 'assert'
33
import { v4 as uuid } from 'uuid'
44

55
import { getPageTests } from './page.test'
6-
import { createTestApp, TestContainer } from '../../tests/app'
6+
import {
7+
createApolloClient,
8+
createTestApp,
9+
TestContainer
10+
} from '../../tests/app'
711
import { IocContract } from '@adonisjs/fold'
812
import { AppServices } from '../../app'
913
import { initIocContainer } from '../..'
@@ -32,6 +36,7 @@ import { isFeeError } from '../../fee/errors'
3236
import { createFee } from '../../tests/fee'
3337
import { createAsset } from '../../tests/asset'
3438
import { GraphQLErrorCode } from '../errors'
39+
import { createTenant } from '../../tests/tenant'
3540

3641
describe('Asset Resolvers', (): void => {
3742
let deps: IocContract<AppServices>
@@ -212,6 +217,55 @@ describe('Asset Resolvers', (): void => {
212217
)
213218
}
214219
})
220+
221+
test('bad input data when not allowed to perform cross tenant create', async (): Promise<void> => {
222+
const otherTenant = await createTenant(deps)
223+
const badInputData = {
224+
...randomAsset(),
225+
tenantId: uuid()
226+
}
227+
228+
const tenantedApolloClient = await createApolloClient(
229+
appContainer.container,
230+
appContainer.app,
231+
otherTenant.id
232+
)
233+
try {
234+
expect.assertions(2)
235+
await tenantedApolloClient
236+
.mutate({
237+
mutation: gql`
238+
mutation CreateAsset($input: CreateAssetInput!) {
239+
createAsset(input: $input) {
240+
asset {
241+
id
242+
}
243+
}
244+
}
245+
`,
246+
variables: {
247+
input: badInputData
248+
}
249+
})
250+
.then((query): AssetMutationResponse => {
251+
if (query.data) {
252+
return query.data.createAsset
253+
} else {
254+
throw new Error('Data was empty')
255+
}
256+
})
257+
} catch (error) {
258+
expect(error).toBeInstanceOf(ApolloError)
259+
expect((error as ApolloError).graphQLErrors).toContainEqual(
260+
expect.objectContaining({
261+
message: 'Assignment to the specified tenant is not permitted',
262+
extensions: expect.objectContaining({
263+
code: GraphQLErrorCode.BadUserInput
264+
})
265+
})
266+
)
267+
}
268+
})
215269
})
216270

217271
describe('Asset Queries', (): void => {

packages/backend/src/graphql/resolvers/asset.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
} from '../generated/graphql'
88
import { Asset } from '../../asset/model'
99
import { errorToCode, errorToMessage, isAssetError } from '../../asset/errors'
10-
import { TenantedApolloContext } from '../../app'
10+
import { ForTenantIdContext, TenantedApolloContext } from '../../app'
1111
import { getPageInfo } from '../../shared/pagination'
1212
import { Pagination, SortOrder } from '../../shared/baseModel'
1313
import { feeToGraphql } from './fee'
@@ -50,7 +50,10 @@ export const getAsset: QueryResolvers<TenantedApolloContext>['asset'] = async (
5050
ctx
5151
): Promise<ResolversTypes['Asset']> => {
5252
const assetService = await ctx.container.use('assetService')
53-
const asset = await assetService.get(args.id, ctx.tenant.id)
53+
const asset = await assetService.get(
54+
args.id,
55+
ctx.isOperator ? undefined : ctx.tenant.id
56+
)
5457
if (!asset) {
5558
throw new GraphQLError('Asset not found', {
5659
extensions: {
@@ -72,16 +75,27 @@ export const getAssetByCodeAndScale: QueryResolvers<TenantedApolloContext>['asse
7275
return asset ? assetToGraphql(asset) : null
7376
}
7477

75-
export const createAsset: MutationResolvers<TenantedApolloContext>['createAsset'] =
78+
export const createAsset: MutationResolvers<ForTenantIdContext>['createAsset'] =
7679
async (
7780
parent,
7881
args,
7982
ctx
8083
): Promise<ResolversTypes['AssetMutationResponse']> => {
84+
const tenantId = ctx.forTenantId
85+
if (!tenantId)
86+
throw new GraphQLError(
87+
`Assignment to the specified tenant is not permitted`,
88+
{
89+
extensions: {
90+
code: GraphQLErrorCode.BadUserInput
91+
}
92+
}
93+
)
94+
ctx.logger.info({ tenantId }, 'tenantId for create asset')
8195
const assetService = await ctx.container.use('assetService')
8296
const assetOrError = await assetService.create({
8397
...args.input,
84-
tenantId: ctx.tenant.id
98+
tenantId
8599
})
86100
if (isAssetError(assetOrError)) {
87101
throw new GraphQLError(errorToMessage[assetOrError], {

packages/backend/src/graphql/schema.graphql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,8 @@ input CreateAssetInput {
390390
liquidityThreshold: UInt64
391391
"Unique key to ensure duplicate or retried requests are processed only once. For more information, refer to [idempotency](https://rafiki.dev/apis/graphql/admin-api-overview/#idempotency)."
392392
idempotencyKey: String
393+
"Unique identifier of the tenant associated with the asset. This cannot be changed. Optional, if not provided, the tenantId will be obtained from the signature."
394+
tenantId: ID
393395
}
394396

395397
input UpdateAssetInput {

packages/backend/src/tests/app.ts

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,36 +28,13 @@ export interface TestContainer {
2828
container: IocContract<AppServices>
2929
}
3030

31-
export const createTestApp = async (
32-
container: IocContract<AppServices>
33-
): Promise<TestContainer> => {
34-
const config = await container.use('config')
35-
config.adminPort = 0
36-
config.openPaymentsPort = 0
37-
config.connectorPort = 0
38-
config.autoPeeringServerPort = 0
39-
config.openPaymentsUrl = 'https://op.example'
40-
config.walletAddressUrl = 'https://wallet.example/.well-known/pay'
31+
export const createApolloClient = async (
32+
container: IocContract<AppServices>,
33+
app: App,
34+
tenantId?: string
35+
): Promise<ApolloClient<NormalizedCacheObject>> => {
4136
const logger = await container.use('logger')
42-
43-
const app = new App(container)
44-
await start(container, app)
45-
46-
const nock = (global as unknown as { nock: typeof import('nock') }).nock
47-
48-
// Since wallet addresses MUST use HTTPS, manually mock an HTTPS proxy to the Open Payments / SPSP server
49-
nock(config.openPaymentsUrl)
50-
.get(/.*/)
51-
.matchHeader('Accept', /application\/((ilp-stream|spsp4)\+)?json*./)
52-
.reply(200, function (path) {
53-
return Axios.get(`http://localhost:${app.getOpenPaymentsPort()}${path}`, {
54-
headers: this.req.headers
55-
}).then((res) => res.data)
56-
})
57-
.persist()
58-
59-
const knex = await container.use('knex')
60-
37+
const config = await container.use('config')
6138
const httpLink = createHttpLink({
6239
uri: `http://localhost:${app.getAdminPort()}/graphql`,
6340
fetch
@@ -80,14 +57,14 @@ export const createTestApp = async (
8057
return {
8158
headers: {
8259
...headers,
83-
'tenant-id': config.operatorTenantId
60+
'tenant-id': tenantId || config.operatorTenantId
8461
}
8562
}
8663
})
8764

8865
const link = ApolloLink.from([errorLink, authLink, httpLink])
8966

90-
const client = new ApolloClient({
67+
return new ApolloClient({
9168
cache: new InMemoryCache({}),
9269
link: link,
9370
defaultOptions: {
@@ -102,6 +79,38 @@ export const createTestApp = async (
10279
}
10380
}
10481
})
82+
}
83+
84+
export const createTestApp = async (
85+
container: IocContract<AppServices>
86+
): Promise<TestContainer> => {
87+
const config = await container.use('config')
88+
config.adminPort = 0
89+
config.openPaymentsPort = 0
90+
config.connectorPort = 0
91+
config.autoPeeringServerPort = 0
92+
config.openPaymentsUrl = 'https://op.example'
93+
config.walletAddressUrl = 'https://wallet.example/.well-known/pay'
94+
95+
const app = new App(container)
96+
await start(container, app)
97+
98+
const nock = (global as unknown as { nock: typeof import('nock') }).nock
99+
100+
// Since wallet addresses MUST use HTTPS, manually mock an HTTPS proxy to the Open Payments / SPSP server
101+
nock(config.openPaymentsUrl)
102+
.get(/.*/)
103+
.matchHeader('Accept', /application\/((ilp-stream|spsp4)\+)?json*./)
104+
.reply(200, function (path) {
105+
return Axios.get(`http://localhost:${app.getOpenPaymentsPort()}${path}`, {
106+
headers: this.req.headers
107+
}).then((res) => res.data)
108+
})
109+
.persist()
110+
111+
const knex = await container.use('knex')
112+
113+
const client = await createApolloClient(container, app)
105114

106115
return {
107116
app,

packages/frontend/app/generated/graphql.ts

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/frontend/app/lib/api/asset.server.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,8 @@ export const listAssets = async (request: Request, args: QueryAssetsArgs) => {
140140
return response.data.assets
141141
}
142142

143-
export const createAsset = async (
144-
request: Request,
145-
args: CreateAssetInput,
146-
tenantId?: string
147-
) => {
148-
const apolloClient = await getApolloClient(request, tenantId)
143+
export const createAsset = async (request: Request, args: CreateAssetInput) => {
144+
const apolloClient = await getApolloClient(request)
149145
const response = await apolloClient.mutate<
150146
CreateAssetMutation,
151147
CreateAssetMutationVariables

packages/frontend/app/lib/apollo.server.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ BigInt.prototype.toJSON = function (this: bigint) {
2323
return this.toString()
2424
}
2525

26-
async function createAuthLink(request: Request, tenantId?: string) {
26+
async function createAuthLink(request: Request) {
2727
return setContext(async (gqlRequest, { headers }) => {
2828
const timestamp = Date.now()
2929
const version = process.env.SIGNATURE_VERSION
@@ -53,10 +53,9 @@ async function createAuthLink(request: Request, tenantId?: string) {
5353
}
5454
}
5555

56-
const sessionTenantId = session.get('tenantId')
57-
if (sessionTenantId || tenantId) {
58-
// Use session tenant id if operator does not specify other tenant
59-
link.headers['tenant-id'] = tenantId ?? sessionTenantId
56+
const tenantId = session.get('tenantId')
57+
if (tenantId) {
58+
link.headers['tenant-id'] = tenantId
6059
}
6160

6261
return link
@@ -67,9 +66,9 @@ const httpLink = createHttpLink({
6766
uri: process.env.GRAPHQL_URL
6867
})
6968

70-
export async function getApolloClient(request: Request, tenantId?: string) {
69+
export async function getApolloClient(request: Request) {
7170
return new ApolloClient({
7271
cache: new InMemoryCache({}),
73-
link: ApolloLink.from([await createAuthLink(request, tenantId), httpLink])
72+
link: ApolloLink.from([await createAuthLink(request), httpLink])
7473
})
7574
}

packages/frontend/app/routes/assets.create.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
useNavigation
77
} from '@remix-run/react'
88
import { PageHeader } from '~/components'
9-
import { Button, Dropdown, ErrorPanel, Input } from '~/components/ui'
9+
import { Button, Select, ErrorPanel, Input } from '~/components/ui'
1010
import { createAsset } from '~/lib/api/asset.server'
1111
import { messageStorage, setMessageAndRedirect } from '~/lib/message.server'
1212
import { createAssetSchema } from '~/lib/validate.server'
@@ -81,10 +81,10 @@ export default function CreateAssetPage() {
8181
error={response?.errors.fieldErrors.withdrawalThreshold}
8282
/>
8383
{tenants && (
84-
<Dropdown
84+
<Select
8585
options={tenants.map((tenant) => ({
86-
label: tenant.node.id,
87-
value: `${tenant.node.id} ${tenant.node.publicName ? `(${tenant.node.publicName})` : ''}`
86+
label: `${tenant.node.id}${tenant.node.publicName ? ` (${tenant.node.publicName})` : ''}`,
87+
value: tenant.node.id
8888
}))}
8989
name='tenantId'
9090
placeholder='Select tenant...'
@@ -130,8 +130,6 @@ export async function action({ request }: ActionFunctionArgs) {
130130
return json({ errors }, { status: 400 })
131131
}
132132

133-
// Make input fields match GraphQL schema
134-
delete result.data.tenantId
135133
const response = await createAsset(request, {
136134
...result.data,
137135
...(result.data.withdrawalThreshold

packages/mock-account-service-lib/src/generated/graphql.ts

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/integration/lib/generated/graphql.ts

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)