Skip to content

Commit

Permalink
chore(backend): match subresources by payment pointer (#647)
Browse files Browse the repository at this point in the history
* chore(backend): store grantId on quote

Add abstract PaymentPointerSubresource model.

* chore(backend): match subresources by payment pointer

Add SubresourceQueryBuilder custom query builder.
Add shared subresource tests.

* chore(backend): distinguish read/read-all in auth middleware

Replace grant.includesAccess with findAccess.
Store filtering clientId on request context.
Add shared subresource GET route tests

* chore(backend): add list query to SubresourceQueryBuilder

Add PaymentPointerSubresourceService interface.

* chore(backend): add list to shared subresource tests

* chore(backend): run pagination tests via shared subresource tests

Create fewer instances in shared pagination tests.

* chore(backend): abstract list routes
  • Loading branch information
wilsonianb authored and omertoast committed Oct 28, 2022
1 parent c2364e1 commit 512244e
Show file tree
Hide file tree
Showing 36 changed files with 1,141 additions and 1,175 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ exports.up = function (knex) {
table.uuid('assetId').notNullable()
table.foreign('assetId').references('assets.id')

table.string('grantId').nullable()
table.foreign('grantId').references('grantReferences.id')

table.timestamp('createdAt').defaultTo(knex.fn.now())
table.timestamp('updatedAt').defaultTo(knex.fn.now())

Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export type AppRequest<ParamsT extends string = string> = Omit<
export interface PaymentPointerContext extends AppContext {
paymentPointer: PaymentPointer
grant?: Grant
clientId?: string
}

// Payment pointer subresources
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export const RafikiServicesFactory = Factory.define<MockRafikiServices>(
'incomingPayments',
['accounting'],
(accounting: MockAccountingService) => ({
get: async (id: string) => await accounting._getIncomingPayment(id),
get: async ({ id }: { id: string }) =>
await accounting._getIncomingPayment(id),
handlePayment: async (_id: string) => {
return undefined
}
Expand Down
6 changes: 3 additions & 3 deletions packages/backend/src/connector/core/middleware/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ export function createAccountMiddleware(serverAddress: string): ILPMiddleware {
OutgoingAccount | undefined
> => {
if (ctx.state.streamDestination) {
const incomingPayment = await incomingPayments.get(
ctx.state.streamDestination
)
const incomingPayment = await incomingPayments.get({
id: ctx.state.streamDestination
})
if (incomingPayment) {
if (
incomingPayment.state === IncomingPaymentState.Completed ||
Expand Down
12 changes: 7 additions & 5 deletions packages/backend/src/graphql/resolvers/incoming_payment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,17 @@ export const getPaymentPointerIncomingPayments: PaymentPointerResolvers<ApolloCo
'incomingPaymentService'
)
const incomingPayments = await incomingPaymentService.getPaymentPointerPage(
parent.id,
args
{
paymentPointerId: parent.id,
pagination: args
}
)
const pageInfo = await getPageInfo(
(pagination: Pagination) =>
incomingPaymentService.getPaymentPointerPage(
parent.id as string,
incomingPaymentService.getPaymentPointerPage({
paymentPointerId: parent.id as string,
pagination
),
}),
incomingPayments
)

Expand Down
16 changes: 10 additions & 6 deletions packages/backend/src/graphql/resolvers/outgoing_payment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ export const getOutgoingPayment: QueryResolvers<ApolloContext>['outgoingPayment'
const outgoingPaymentService = await ctx.container.use(
'outgoingPaymentService'
)
const payment = await outgoingPaymentService.get(args.id)
const payment = await outgoingPaymentService.get({
id: args.id
})
if (!payment) throw new Error('payment does not exist')
return paymentToGraphql(payment)
}
Expand Down Expand Up @@ -69,15 +71,17 @@ export const getPaymentPointerOutgoingPayments: PaymentPointerResolvers<ApolloCo
'outgoingPaymentService'
)
const outgoingPayments = await outgoingPaymentService.getPaymentPointerPage(
parent.id,
args
{
paymentPointerId: parent.id,
pagination: args
}
)
const pageInfo = await getPageInfo(
(pagination: Pagination) =>
outgoingPaymentService.getPaymentPointerPage(
parent.id as string,
outgoingPaymentService.getPaymentPointerPage({
paymentPointerId: parent.id as string,
pagination
),
}),
outgoingPayments
)
return {
Expand Down
14 changes: 11 additions & 3 deletions packages/backend/src/graphql/resolvers/quote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ export const getQuote: QueryResolvers<ApolloContext>['quote'] = async (
ctx
): Promise<ResolversTypes['Quote']> => {
const quoteService = await ctx.container.use('quoteService')
const quote = await quoteService.get(args.id)
const quote = await quoteService.get({
id: args.id
})
if (!quote) throw new Error('quote does not exist')
return quoteToGraphql(quote)
}
Expand Down Expand Up @@ -56,10 +58,16 @@ export const getPaymentPointerQuotes: PaymentPointerResolvers<ApolloContext>['qu
async (parent, args, ctx): Promise<ResolversTypes['QuoteConnection']> => {
if (!parent.id) throw new Error('missing payment pointer id')
const quoteService = await ctx.container.use('quoteService')
const quotes = await quoteService.getPaymentPointerPage(parent.id, args)
const quotes = await quoteService.getPaymentPointerPage({
paymentPointerId: parent.id,
pagination: args
})
const pageInfo = await getPageInfo(
(pagination: Pagination) =>
quoteService.getPaymentPointerPage(parent.id as string, pagination),
quoteService.getPaymentPointerPage({
paymentPointerId: parent.id as string,
pagination
}),
quotes
)
return {
Expand Down
22 changes: 11 additions & 11 deletions packages/backend/src/open_payments/auth/grant.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Interval } from 'luxon'
import { v4 as uuid } from 'uuid'

describe('Grant', (): void => {
describe('includesAccess', (): void => {
describe('findAccess', (): void => {
let grant: Grant
const type = AccessType.IncomingPayment
const action = AccessAction.Create
Expand Down Expand Up @@ -36,12 +36,12 @@ describe('Grant', (): void => {

test('Returns true for included access', async (): Promise<void> => {
expect(
grant.includesAccess({
grant.findAccess({
type,
action,
identifier
})
).toBe(true)
).toEqual(grant.access[1])
})
test.each`
superAction | subAction | description
Expand All @@ -63,12 +63,12 @@ describe('Grant', (): void => {
]
})
expect(
grant.includesAccess({
grant.findAccess({
type,
action: subAction,
identifier
})
).toBe(true)
).toEqual(grant.access[0])
}
)

Expand All @@ -80,34 +80,34 @@ describe('Grant', (): void => {
'Returns false for missing $description',
async ({ type, action, identifier }): Promise<void> => {
expect(
grant.includesAccess({
grant.findAccess({
type,
action,
identifier
})
).toBe(false)
).toBeUndefined()
}
)

if (identifier) {
test('Returns false for missing identifier', async (): Promise<void> => {
expect(
grant.includesAccess({
grant.findAccess({
type,
action,
identifier: 'https://wallet.example/bob'
})
).toBe(false)
).toBeUndefined()
})
} else {
test('Returns true for unrestricted identifier', async (): Promise<void> => {
expect(
grant.includesAccess({
grant.findAccess({
type,
action,
identifier: 'https://wallet.example/bob'
})
).toBe(true)
).toEqual(grant.access[1])
})
}
})
Expand Down
6 changes: 3 additions & 3 deletions packages/backend/src/open_payments/auth/grant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,16 @@ export class Grant {
public readonly access: GrantAccess[]
public readonly clientId: string

public includesAccess({
public findAccess({
type,
action,
identifier
}: {
type: AccessType
action: AccessAction
identifier: string
}): boolean {
return !!this.access?.find(
}): GrantAccess | undefined {
return this.access?.find(
(access) =>
access.type === type &&
(!access.identifier || access.identifier === identifier) &&
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/open_payments/auth/middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import { IocContract } from '@adonisjs/fold'
import { initIocContainer } from '../../'
import { AppServices, PaymentPointerContext } from '../../app'
import { HttpMethod, ValidateFunction } from 'openapi'
import { setup } from '../../shared/routes.test'
import { createTestApp, TestContainer } from '../../tests/app'
import { createPaymentPointer } from '../../tests/paymentPointer'
import { truncateTables } from '../../tests/tableManager'
import { GrantReference } from '../grantReference/model'
import { GrantReferenceService } from '../grantReference/service'
import { setup } from '../payment_pointer/model.test'

type AppMiddleware = (
ctx: PaymentPointerContext,
Expand Down
20 changes: 13 additions & 7 deletions packages/backend/src/open_payments/auth/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,12 @@ export function createAuthMiddleware({
if (!grant || !grant.active) {
ctx.throw(401, 'Invalid Token')
}
if (
!grant.includesAccess({
type,
action,
identifier: ctx.paymentPointer.url
})
) {
const access = grant.findAccess({
type,
action,
identifier: ctx.paymentPointer.url
})
if (!access) {
ctx.throw(403, 'Insufficient Grant')
}
await GrantReference.transaction(async (trx: Transaction) => {
Expand All @@ -66,6 +65,13 @@ export function createAuthMiddleware({
}
})
ctx.grant = grant

// Unless the relevant grant action is ReadAll/ListAll add the
// clientId to ctx for Read/List filtering
if (access.actions.includes(action)) {
ctx.clientId = grant.clientId
}

await next()
} catch (err) {
if (err.status === 401) {
Expand Down
50 changes: 17 additions & 33 deletions packages/backend/src/open_payments/payment/incoming/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { v4 as uuid } from 'uuid'

import { Amount, AmountJSON } from '../../amount'
import { ConnectionJSON } from '../../connection/service'
import { PaymentPointer } from '../../payment_pointer/model'
import {
PaymentPointer,
PaymentPointerSubresource
} from '../../payment_pointer/model'
import { Asset } from '../../../asset/model'
import { LiquidityAccount, OnCreditOptions } from '../../../accounting/service'
import { ConnectorAccount } from '../../../connector/core/rafiki'
import { BaseModel } from '../../../shared/baseModel'
import { WebhookEvent } from '../../../webhook/model'
import { GrantReference } from '../../grantReference/model'

export enum IncomingPaymentEventType {
IncomingPaymentExpired = 'incoming_payment.expired',
Expand Down Expand Up @@ -52,46 +53,32 @@ export class IncomingPaymentEvent extends WebhookEvent {
}

export class IncomingPayment
extends BaseModel
extends PaymentPointerSubresource
implements ConnectorAccount, LiquidityAccount
{
public static get tableName(): string {
return 'incomingPayments'
}
public static readonly urlPath = '/incoming-payments'

static get virtualAttributes(): string[] {
return ['completed', 'incomingAmount', 'receivedAmount', 'url']
}

static relationMappings = {
asset: {
relation: Model.HasOneRelation,
modelClass: Asset,
join: {
from: 'incomingPayments.assetId',
to: 'assets.id'
}
},
paymentPointer: {
relation: Model.BelongsToOneRelation,
modelClass: PaymentPointer,
join: {
from: 'incomingPayments.paymentPointerId',
to: 'paymentPointers.id'
}
},
grantRef: {
relation: Model.HasOneRelation,
modelClass: GrantReference,
join: {
from: 'incomingPayments.grantId',
to: 'grantReferences.id'
static get relationMappings() {
return {
...super.relationMappings,
asset: {
relation: Model.HasOneRelation,
modelClass: Asset,
join: {
from: 'incomingPayments.assetId',
to: 'assets.id'
}
}
}
}

// Open payments paymentPointer id this incoming payment is for
public paymentPointerId!: string
public paymentPointer!: PaymentPointer
public description?: string
public expiresAt!: Date
Expand All @@ -100,9 +87,6 @@ export class IncomingPayment
// The "| null" is necessary so that `$beforeUpdate` can modify a patch to remove the connectionId. If `$beforeUpdate` set `error = undefined`, the patch would ignore the modification.
public connectionId?: string | null

public grantId?: string
public grantRef?: GrantReference

public processAt!: Date | null

public readonly assetId!: string
Expand Down Expand Up @@ -143,7 +127,7 @@ export class IncomingPayment
}

public get url(): string {
return `${this.paymentPointer.url}/incoming-payments/${this.id}`
return `${this.paymentPointer.url}${IncomingPayment.urlPath}/${this.id}`
}

public async onCredit({
Expand Down
Loading

0 comments on commit 512244e

Please sign in to comment.