Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(backend): add clientId to OP resources #535

Merged
merged 35 commits into from
Sep 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
04f9d1b
feat(backend): add clientId to incoming payments
sabineschaller Aug 24, 2022
f8d7db4
fix(backend): route tests
sabineschaller Aug 25, 2022
46db826
fix(backend): build
sabineschaller Aug 25, 2022
7ceda6e
Merge branch 'main' into s2-add-clientId-to-resources
sabineschaller Aug 25, 2022
c70f4dd
fix(backend): quote and outgoing payment tests
sabineschaller Aug 25, 2022
0902c53
Merge branch 'main' into s2-add-clientId-to-resources
sabineschaller Sep 7, 2022
036798f
fix(backend): incoming payment list route tests
sabineschaller Sep 7, 2022
b9e1bad
feat(backend): add grant model that holds clientId
sabineschaller Sep 9, 2022
d0e3fd7
fix(backend): outgoing payment resolver
sabineschaller Sep 9, 2022
fe913c5
Merge branch 'main' into s2-add-clientId-to-resources
sabineschaller Sep 9, 2022
fdc73a0
style(backend): remove unnecessary styling
sabineschaller Sep 9, 2022
04f436c
feat(backend): make grant optional on resource creation
sabineschaller Sep 12, 2022
eb75faf
feat(backend): use withgraphjoined on pagination
sabineschaller Sep 12, 2022
6f2bda9
fix(backend): lock grant
sabineschaller Sep 12, 2022
612115a
fix(backend): grant access can hold multiple items again
sabineschaller Sep 12, 2022
1d4e5a2
fix(backend): incoming payment glitches
sabineschaller Sep 13, 2022
3776626
feat(backend): add clientId to outgoing payments
sabineschaller Sep 13, 2022
06bb2ae
test(backend): add additional payments before running pagination tests
sabineschaller Sep 14, 2022
3a5bc3f
fix(backend): remove unnecessary query parameter from setup function
sabineschaller Sep 14, 2022
5661171
refactor(backend): setup function uses options instead of arg list
sabineschaller Sep 14, 2022
abd1360
feat(backend): `xAll` actions include `x` action
sabineschaller Sep 14, 2022
2cc0a05
refactor(backend): outgoing payment creation only takes Grant, not gr…
sabineschaller Sep 15, 2022
9fc9a71
feat(backend): return error if client id doesn't match known grant
sabineschaller Sep 15, 2022
72ae24d
refactor(backend): `referenceGrant` --> `grantRef`
sabineschaller Sep 15, 2022
50defc3
refactor(backend): rename model `Grant` --> `GrantReference`, move to…
sabineschaller Sep 15, 2022
3173839
feat(backend): add GrantReferenceService
sabineschaller Sep 15, 2022
1c55cc7
refactor(backend): add Brandon's suggestions
sabineschaller Sep 16, 2022
12a4c15
feat(backend): allow to pass transaction to GrantReferenceService met…
sabineschaller Sep 16, 2022
ed8dcca
test(backend): add GrantReference tests
sabineschaller Sep 16, 2022
10551e9
Merge branch 'main' into s2-add-clientId-to-resources
sabineschaller Sep 16, 2022
ee46354
fix(backend): throw 500 if clientId mismatch
sabineschaller Sep 19, 2022
15a315e
fix(backend): make transaction required in lock
sabineschaller Sep 19, 2022
0e34b6c
test(backend): fix create and get GrantReference tests
sabineschaller Sep 19, 2022
010a7e3
feat(backend): get and create GrantReference within the same transaction
sabineschaller Sep 19, 2022
8de25af
feat(backend): remove GrantReferenceService dependencies
sabineschaller Sep 20, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
exports.up = function (knex) {
return knex.schema.createTable('grantReferences', function (table) {
table.string('id').notNullable().primary()
table.string('clientId').notNullable()
})
}

exports.down = function (knex) {
return knex.schema.dropTableIfExists('grantReferences')
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ exports.up = function (knex) {
table.string('externalRef').nullable()
table.uuid('connectionId').nullable()

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

table.uuid('assetId').notNullable()
table.foreign('assetId').references('assets.id')
table.timestamp('processAt').nullable()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ exports.up = function (knex) {
table.string('description').nullable()
table.string('externalRef').nullable()

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

// Open payments payment pointer corresponding to wallet account
// from which to request funds for payment
Expand Down
6 changes: 5 additions & 1 deletion packages/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { Session } from './session/util'
import { createValidatorMiddleware, HttpMethod, isHttpMethod } from 'openapi'
import { ClientKeysService } from './clientKeys/service'
import { ClientService } from './clients/service'
import { GrantReferenceService } from './open_payments/grantReference/service'

export interface AppContextData {
logger: Logger
Expand Down Expand Up @@ -141,6 +142,7 @@ export interface AppServices {
sessionService: Promise<SessionService>
clientService: Promise<ClientService>
clientKeysService: Promise<ClientKeysService>
grantReferenceService: Promise<GrantReferenceService>
}

export type AppContainer = IocContract<AppServices>
Expand Down Expand Up @@ -277,8 +279,10 @@ export class App {
} = {
[AccessAction.Create]: 'create',
[AccessAction.Read]: 'get',
[AccessAction.ReadAll]: 'get',
[AccessAction.Complete]: 'complete',
[AccessAction.List]: 'list'
[AccessAction.List]: 'list',
[AccessAction.ListAll]: 'list'
}

for (const path in openApi.paths) {
Expand Down
11 changes: 11 additions & 0 deletions packages/backend/src/graphql/resolvers/incoming_payment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,25 @@ import { randomAsset } from '../../tests/asset'
import { createIncomingPayment } from '../../tests/incomingPayment'
import { createPaymentPointer } from '../../tests/paymentPointer'
import { truncateTables } from '../../tests/tableManager'
import { v4 as uuid } from 'uuid'
import { GrantReference } from '../../open_payments/grantReference/model'
import { GrantReferenceService } from '../../open_payments/grantReference/service'

describe('Incoming Payment Resolver', (): void => {
let deps: IocContract<AppServices>
let appContainer: TestContainer
let knex: Knex
let paymentPointerId: string
let grantReferenceService: GrantReferenceService
let grantRef: GrantReference

const asset = randomAsset()

beforeAll(async (): Promise<void> => {
deps = await initIocContainer(Config)
appContainer = await createTestApp(deps)
knex = await deps.use('knex')
grantReferenceService = await deps.use('grantReferenceService')
})

afterAll(async (): Promise<void> => {
Expand All @@ -34,13 +40,18 @@ describe('Incoming Payment Resolver', (): void => {
describe('Payment pointer incoming payments', (): void => {
beforeEach(async (): Promise<void> => {
paymentPointerId = (await createPaymentPointer(deps, { asset })).id
grantRef = await grantReferenceService.create({
id: uuid(),
clientId: uuid()
})
})

getPageTests({
getClient: () => appContainer.apolloClient,
createModel: () =>
createIncomingPayment(deps, {
paymentPointerId,
grantId: grantRef.id,
incomingAmount: {
value: BigInt(123),
assetCode: asset.code,
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { createConnectionService } from './open_payments/connection/service'
import { createConnectionRoutes } from './open_payments/connection/routes'
import { createClientKeysService } from './clientKeys/service'
import { createClientService } from './clients/service'
import { createGrantReferenceService } from './open_payments/grantReference/service'

BigInt.prototype.toJSON = function () {
return this.toString()
Expand Down Expand Up @@ -295,12 +296,16 @@ export function initIocContainer(
quoteService: await deps.use('quoteService')
})
})
container.singleton('grantReferenceService', async () => {
return createGrantReferenceService()
})
container.singleton('outgoingPaymentService', async (deps) => {
return await createOutgoingPaymentService({
logger: await deps.use('logger'),
knex: await deps.use('knex'),
accountingService: await deps.use('accountingService'),
clientService: await deps.use('openPaymentsClientService'),
grantReferenceService: await deps.use('grantReferenceService'),
makeIlpPlugin: await deps.use('makeIlpPlugin'),
peerService: await deps.use('peerService')
})
Expand Down
31 changes: 31 additions & 0 deletions packages/backend/src/open_payments/auth/grant.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Grant, AccessType, AccessAction, getInterval } from './grant'
import { Interval } from 'luxon'
import { v4 as uuid } from 'uuid'

describe('Grant', (): void => {
describe('includesAccess', (): void => {
let grant: Grant
const type = AccessType.IncomingPayment
const action = AccessAction.Create
const clientId = uuid()

describe.each`
identifier | description
Expand All @@ -16,6 +18,7 @@ describe('Grant', (): void => {
grant = new Grant({
active: true,
grant: 'PRY5NM33OM4TB8N6BW7',
clientId,
access: [
{
type: AccessType.OutgoingPayment,
Expand All @@ -40,6 +43,34 @@ describe('Grant', (): void => {
})
).toBe(true)
})
test.each`
superAction | subAction | description
${AccessAction.ReadAll} | ${AccessAction.Read} | ${'read'}
${AccessAction.ListAll} | ${AccessAction.List} | ${'list'}
`(
'Returns true for $description super access',
async ({ superAction, subAction }): Promise<void> => {
const grant = new Grant({
active: true,
grant: 'PRY5NM33OM4TB8N6BW7',
clientId,
access: [
{
type,
actions: [superAction],
identifier
}
]
})
expect(
grant.includesAccess({
type,
action: subAction,
identifier
})
).toBe(true)
}
)

test.each`
type | action | identifier | description
Expand Down
14 changes: 12 additions & 2 deletions packages/backend/src/open_payments/auth/grant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ export enum AccessType {
export enum AccessAction {
Create = 'create',
Read = 'read',
ReadAll = 'read-all',
Complete = 'complete',
List = 'list'
List = 'list',
ListAll = 'list-all'
}

export interface AccessLimits {
Expand Down Expand Up @@ -53,6 +55,7 @@ export type GrantAccessJSON = Omit<GrantAccess, 'limits'> & {
export interface GrantOptions {
active: boolean
grant: string
clientId: string
access?: GrantAccess[]
}

Expand All @@ -66,11 +69,13 @@ export class Grant {
this.active = options.active
this.grant = options.grant
this.access = options.access || []
this.clientId = options.clientId
}

public readonly active: boolean
public readonly grant: string
public readonly access: GrantAccess[]
public readonly clientId: string

public includesAccess({
sabineschaller marked this conversation as resolved.
Show resolved Hide resolved
type,
Expand All @@ -85,14 +90,19 @@ export class Grant {
(access) =>
access.type === type &&
(!access.identifier || access.identifier === identifier) &&
access.actions.includes(action)
(access.actions.includes(action) ||
(action === AccessAction.Read &&
access.actions.includes(AccessAction.ReadAll)) ||
(action === AccessAction.List &&
access.actions.includes(AccessAction.ListAll)))
)
}

public toJSON(): GrantJSON {
return {
active: this.active,
grant: this.grant,
clientId: this.clientId,
access: this.access?.map((access) => {
return {
...access,
Expand Down
67 changes: 64 additions & 3 deletions packages/backend/src/open_payments/auth/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from 'assert'
import nock, { Definition } from 'nock'
import { URL } from 'url'
import { v4 as uuid } from 'uuid'

import { createAuthMiddleware } from './middleware'
import { Grant, GrantJSON, AccessType, AccessAction } from './grant'
Expand All @@ -13,6 +14,8 @@ import { createTestApp, TestContainer } from '../../tests/app'
import { createContext } from '../../tests/context'
import { createPaymentPointer } from '../../tests/paymentPointer'
import { truncateTables } from '../../tests/tableManager'
import { GrantReference } from '../grantReference/model'
import { GrantReferenceService } from '../grantReference/service'

type AppMiddleware = (
ctx: AppContext,
Expand All @@ -32,6 +35,7 @@ describe('Auth Middleware', (): void => {
let ctx: AppContext
let next: jest.MockedFunction<() => Promise<void>>
let validateRequest: ValidateFunction<IntrospectionBody>
let grantReferenceService: GrantReferenceService
const token = 'OS9M2PMHKUR64TB8N6BW7OZB8CDFONP219RP1LT0'

beforeAll(async (): Promise<void> => {
Expand All @@ -47,6 +51,7 @@ describe('Auth Middleware', (): void => {
path: '/introspect',
method: HttpMethod.POST
})
grantReferenceService = await deps.use('grantReferenceService')
})

beforeEach(async (): Promise<void> => {
Expand Down Expand Up @@ -108,7 +113,7 @@ describe('Auth Middleware', (): void => {

const inactiveGrant = {
active: false,
grant: 'PRY5NM33OM4TB8N6BW7'
grant: uuid()
}

test.each`
Expand All @@ -131,7 +136,8 @@ describe('Auth Middleware', (): void => {
test('returns 403 for unauthorized request', async (): Promise<void> => {
const scope = mockAuthServer({
active: true,
grant: 'PRY5NM33OM4TB8N6BW7',
clientId: uuid(),
grant: uuid(),
access: [
{
type: AccessType.OutgoingPayment,
Expand All @@ -146,6 +152,30 @@ describe('Auth Middleware', (): void => {
expect(next).not.toHaveBeenCalled()
scope.isDone()
})
test('returns 500 for not matching clientId', async (): Promise<void> => {
const grant = new Grant({
active: true,
clientId: uuid(),
grant: uuid(),
access: [
{
type: AccessType.IncomingPayment,
actions: [AccessAction.Read],
identifier: ctx.params.paymentPointerId
}
]
})
await grantReferenceService.create({
id: grant.grant,
clientId: uuid()
})
const scope = mockAuthServer(grant.toJSON())
await expect(middleware(ctx, next)).rejects.toMatchObject({
status: 500
})
expect(next).not.toHaveBeenCalled()
scope.isDone()
})

test.each`
limitAccount
Expand All @@ -156,7 +186,8 @@ describe('Auth Middleware', (): void => {
async ({ limitAccount }): Promise<void> => {
const grant = new Grant({
active: true,
grant: 'PRY5NM33OM4TB8N6BW7',
clientId: uuid(),
sabineschaller marked this conversation as resolved.
Show resolved Hide resolved
grant: uuid(),
access: [
{
type: AccessType.IncomingPayment,
Expand Down Expand Up @@ -197,6 +228,12 @@ describe('Auth Middleware', (): void => {
await expect(middleware(ctx, next)).resolves.toBeUndefined()
expect(next).toHaveBeenCalled()
expect(ctx.grant).toEqual(grant)
await expect(
GrantReference.query().findById(grant.grant)
).resolves.toEqual({
id: grant.grant,
clientId: grant.clientId
})
scope.isDone()
}
)
Expand All @@ -208,4 +245,28 @@ describe('Auth Middleware', (): void => {
expect(introspectSpy).not.toHaveBeenCalled()
expect(next).toHaveBeenCalled()
})

test('sets the context and calls next if grant has been seen before', async (): Promise<void> => {
const grant = new Grant({
active: true,
clientId: uuid(),
grant: uuid(),
access: [
{
type: AccessType.IncomingPayment,
actions: [AccessAction.Read],
identifier: ctx.params.paymentPointerId
}
]
})
await grantReferenceService.create({
id: grant.grant,
clientId: grant.clientId
})
const scope = mockAuthServer(grant.toJSON())
await expect(middleware(ctx, next)).resolves.toBeUndefined()
expect(next).toHaveBeenCalled()
expect(ctx.grant).toEqual(grant)
scope.isDone()
})
})
Loading