Skip to content

Commit

Permalink
feat(backend): request grant to query incoming payment receiver (#779)
Browse files Browse the repository at this point in the history
* feat(backend): request grant to query incoming payment receiver

Add services for managing grants.

* fix(localenv): connect Docker network

Update environment variables.

* fix(auth): add grant access actions

Remove unused locations and interval fields and account access.

* fix(backend): properly export public key jwk

* fix(open-payments): properly construct grant request body

* chore(backend): import open-payments generateJwk

* chore(backend): store and check grant expiresAt

* chore(auth): add interval to OutgoingPaymentLimit

* chore(localenv): re-format networks

* chore(backend): don't cast GrantRequest
  • Loading branch information
wilsonianb authored Dec 6, 2022
1 parent 18d2537 commit a0302b3
Show file tree
Hide file tree
Showing 25 changed files with 727 additions and 122 deletions.
11 changes: 6 additions & 5 deletions infrastructure/local/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ services:
dockerfile: ./packages/auth/Dockerfile
restart: always
networks:
rafiki:
- rafiki
ports:
- '3006:3006'
environment:
Expand All @@ -24,7 +24,7 @@ services:
dockerfile: ./packages/mock-account-provider/Dockerfile
restart: always
networks:
rafiki:
- rafiki
ports:
- '3030:80'
environment:
Expand All @@ -47,7 +47,7 @@ services:
- '3000:80'
- '3001:3001'
networks:
rafiki:
- rafiki
environment:
NODE_ENV: development
LOG_LEVEL: debug
Expand All @@ -71,6 +71,7 @@ services:
REDIS_URL: redis://redis:6379/0
QUOTE_URL: http://fynbos/quotes
BYPASS_SIGNATURE_VALIDATION: "true"
PAYMENT_POINTER_URL: https://backend/.well-known/pay
depends_on:
- tigerbeetle
- database
Expand All @@ -79,7 +80,7 @@ services:
image: 'postgres:15' # use latest official postgres version
restart: unless-stopped
networks:
rafiki:
- rafiki
volumes:
- database-data:/var/lib/postgresql/data/ # persist data even if container shuts down
- ./dbinit.sql:/docker-entrypoint-initdb.d/init.sql
Expand Down Expand Up @@ -119,7 +120,7 @@ services:
image: 'redis:7'
restart: unless-stopped
networks:
rafiki:
- rafiki
volumes:
database-data: # named volumes can be managed easier using docker-compose
tigerbeetle-data: # named volumes can be managed easier using docker-compose
Expand Down
10 changes: 7 additions & 3 deletions infrastructure/local/peer-docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ services:
dockerfile: ./packages/auth/Dockerfile
restart: always
networks:
rafiki:
- local_rafiki
ports:
- "4006:3006"
environment:
Expand All @@ -27,7 +27,7 @@ services:
- "4000:80"
- "4001:3001"
networks:
rafiki:
- local_rafiki
environment:
NODE_ENV: development
LOG_LEVEL: debug
Expand All @@ -51,13 +51,14 @@ services:
REDIS_URL: redis://redis:6379/1
QUOTE_URL: http://local-bank/quote
BYPASS_SIGNATURE_VALIDATION: "true"
PAYMENT_POINTER_URL: https://peer-backend/.well-known/pay
local-bank:
build:
context: ../..
dockerfile: ./packages/mock-account-provider/Dockerfile
restart: always
networks:
rafiki:
- local_rafiki
ports:
- '3031:80'
environment:
Expand All @@ -69,3 +70,6 @@ services:
- ./seed.peer.yml:/workspace/seed.peer.yml
depends_on:
- peer-backend
networks:
local_rafiki:
external: true
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ exports.up = function (knex) {
table.string('type').notNullable()
table.specificType('actions', 'text[]').notNullable()
table.string('identifier')
table.specificType('locations', 'text[]')
table.integer('interval')
table.jsonb('limits')
table.uuid('grantId').notNullable()
table.foreign('grantId').references('grants.id').onDelete('CASCADE')
Expand Down
2 changes: 0 additions & 2 deletions packages/auth/src/access/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,5 @@ export class Access extends BaseModel {
public type!: AccessType
public actions!: Action[]
public identifier?: string
public locations?: string[]
public interval?: string
public limits?: LimitData
}
21 changes: 1 addition & 20 deletions packages/auth/src/access/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export enum AccessType {
Account = 'account',
IncomingPayment = 'incoming-payment',
OutgoingPayment = 'outgoing-payment',
Quote = 'quote'
Expand All @@ -16,9 +15,7 @@ export enum Action {

interface BaseAccessRequest {
actions: Action[]
locations?: string[]
identifier?: string
interval?: string
}

export interface IncomingPaymentRequest extends BaseAccessRequest {
Expand All @@ -31,11 +28,6 @@ interface OutgoingPaymentRequest extends BaseAccessRequest {
limits?: OutgoingPaymentLimit
}

interface AccountRequest extends BaseAccessRequest {
type: AccessType.Account
limits?: never
}

interface QuoteRequest extends BaseAccessRequest {
type: AccessType.Quote
limits?: never
Expand All @@ -44,7 +36,6 @@ interface QuoteRequest extends BaseAccessRequest {
export type AccessRequest =
| IncomingPaymentRequest
| OutgoingPaymentRequest
| AccountRequest
| QuoteRequest

export function isAccessType(accessType: AccessType): accessType is AccessType {
Expand Down Expand Up @@ -80,16 +71,6 @@ function isOutgoingPaymentAccessRequest(
)
}

function isAccountAccessRequest(
accessRequest: AccountRequest
): accessRequest is AccountRequest {
return (
accessRequest.type === AccessType.Account &&
isAction(accessRequest.actions) &&
!accessRequest.limits
)
}

function isQuoteAccessRequest(
accessRequest: QuoteRequest
): accessRequest is QuoteRequest {
Expand All @@ -106,7 +87,6 @@ export function isAccessRequest(
return (
isIncomingPaymentAccessRequest(accessRequest as IncomingPaymentRequest) ||
isOutgoingPaymentAccessRequest(accessRequest as OutgoingPaymentRequest) ||
isAccountAccessRequest(accessRequest as AccountRequest) ||
isQuoteAccessRequest(accessRequest as QuoteRequest)
)
}
Expand All @@ -123,6 +103,7 @@ export type OutgoingPaymentLimit = {
receiver: string
sendAmount?: PaymentAmount
receiveAmount?: PaymentAmount
interval?: string
}

export type LimitData = OutgoingPaymentLimit
Expand Down
1 change: 0 additions & 1 deletion packages/auth/src/grant/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ describe('Grant Service', (): void => {

const BASE_GRANT_ACCESS = {
actions: [Action.Create, Action.Read, Action.List],
locations: ['https://example.com'],
identifier: `https://example.com/${v4()}`
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
exports.up = function (knex) {
return knex.schema.createTable('authServers', function (table) {
table.uuid('id').notNullable().primary()
table.string('url').notNullable().unique()
table.timestamp('createdAt').defaultTo(knex.fn.now())
table.timestamp('updatedAt').defaultTo(knex.fn.now())
})
}

exports.down = function (knex) {
return knex.schema.dropTableIfExists('authServers')
}
23 changes: 23 additions & 0 deletions packages/backend/migrations/20221012013413_create_grants_table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
exports.up = function (knex) {
return knex.schema.createTable('grants', function (table) {
table.uuid('id').notNullable().primary()
table.uuid('authServerId').notNullable()
table.foreign('authServerId').references('authServers.id')
table.string('continueId').nullable()
table.string('continueToken').nullable()
table.string('accessToken').nullable().unique()
table.string('accessType').notNullable()
table.specificType('accessActions', 'text[]')

table.timestamp('expiresAt').nullable()

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

table.unique(['authServerId', 'accessType', 'accessActions'])
})
}

exports.down = function (knex) {
return knex.schema.dropTableIfExists('grants')
}
22 changes: 18 additions & 4 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import { createAssetService } from './asset/service'
import { createAccountingService } from './accounting/service'
import { createPeerService } from './peer/service'
import { createAuthService } from './open_payments/auth/service'

import { createAuthServerService } from './open_payments/authServer/service'
import { createGrantService } from './open_payments/grant/service'
import { createPaymentPointerService } from './open_payments/payment_pointer/service'
import { createSPSPRoutes } from './spsp/routes'
import { createPaymentPointerKeyRoutes } from './paymentPointerKey/routes'
Expand Down Expand Up @@ -175,6 +176,19 @@ export function initIocContainer(
authOpenApi: await deps.use('authOpenApi')
})
})
container.singleton('authServerService', async (deps) => {
return await createAuthServerService({
logger: await deps.use('logger'),
knex: await deps.use('knex')
})
})
container.singleton('grantService', async (deps) => {
return await createGrantService({
authServerService: await deps.use('authServerService'),
logger: await deps.use('logger'),
knex: await deps.use('knex')
})
})
container.singleton('paymentPointerService', async (deps) => {
const logger = await deps.use('logger')
const assetService = await deps.use('assetService')
Expand Down Expand Up @@ -217,8 +231,9 @@ export function initIocContainer(
})
})
container.singleton('paymentPointerRoutes', async (deps) => {
const config = await deps.use('config')
return createPaymentPointerRoutes({
config: await deps.use('config')
authServer: config.authServerGrantUrl
})
})
container.singleton('paymentPointerKeyRoutes', async (deps) => {
Expand Down Expand Up @@ -247,9 +262,8 @@ export function initIocContainer(
const config = await deps.use('config')
return await createReceiverService({
logger: await deps.use('logger'),
// TODO: https://github.com/interledger/rafiki/issues/583
accessToken: config.devAccessToken,
connectionService: await deps.use('connectionService'),
grantService: await deps.use('grantService'),
incomingPaymentService: await deps.use('incomingPaymentService'),
openPaymentsUrl: config.openPaymentsUrl,
paymentPointerService: await deps.use('paymentPointerService'),
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/open_payments/auth/grant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface AmountJSON {
assetScale: number
}

// TODO: replace with open-payments generated types
export enum AccessType {
IncomingPayment = 'incoming-payment',
OutgoingPayment = 'outgoing-payment',
Expand Down
9 changes: 9 additions & 0 deletions packages/backend/src/open_payments/authServer/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { BaseModel } from '../../shared/baseModel'

export class AuthServer extends BaseModel {
public static get tableName(): string {
return 'authServers'
}

public url!: string
}
50 changes: 50 additions & 0 deletions packages/backend/src/open_payments/authServer/service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { IocContract } from '@adonisjs/fold'
import { faker } from '@faker-js/faker'
import { Knex } from 'knex'

import { AuthServer } from './model'
import { AuthServerService } from './service'
import { initIocContainer } from '../../'
import { AppServices } from '../../app'
import { Config } from '../../config/app'
import { createTestApp, TestContainer } from '../../tests/app'
import { truncateTables } from '../../tests/tableManager'

describe('Auth Server Service', (): void => {
let deps: IocContract<AppServices>
let appContainer: TestContainer
let authServerService: AuthServerService
let knex: Knex

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

afterEach(async (): Promise<void> => {
await truncateTables(knex)
})

afterAll(async (): Promise<void> => {
await appContainer.shutdown()
})

describe('getOrCreate', (): void => {
test('Auth server can be created or fetched', async (): Promise<void> => {
const url = faker.internet.url()
await expect(
AuthServer.query(knex).findOne({ url })
).resolves.toBeUndefined()
const authServer = await authServerService.getOrCreate(url)
await expect(authServer).toMatchObject({ url })
await expect(AuthServer.query(knex).findOne({ url })).resolves.toEqual(
authServer
)
await expect(authServerService.getOrCreate(url)).resolves.toEqual(
authServer
)
})
})
})
40 changes: 40 additions & 0 deletions packages/backend/src/open_payments/authServer/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { UniqueViolationError } from 'objection'

import { AuthServer } from './model'
import { BaseService } from '../../shared/baseService'

export interface AuthServerService {
getOrCreate(url: string): Promise<AuthServer>
}

type ServiceDependencies = BaseService

export async function createAuthServerService(
deps_: ServiceDependencies
): Promise<AuthServerService> {
const deps: ServiceDependencies = {
...deps_,
logger: deps_.logger.child({
service: 'AuthServerService'
})
}
return {
getOrCreate: (url) => getOrCreateAuthServer(deps, url)
}
}

async function getOrCreateAuthServer(
deps: ServiceDependencies,
url: string
): Promise<AuthServer> {
try {
return await AuthServer.query(deps.knex).insertAndFetch({
url
})
} catch (err) {
if (err instanceof UniqueViolationError) {
return await AuthServer.query(deps.knex).findOne({ url })
}
throw err
}
}
Loading

0 comments on commit a0302b3

Please sign in to comment.