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(auth): tenanted grants #3187

Draft
wants to merge 2 commits into
base: 2893/multi-tenancy-v1
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions localenv/cloud-nine-wallet/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions localenv/happy-life-bank/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ const authLink = setContext((request, { headers }) => {
return {
headers: {
...headers,
signature: `t=${timestamp}, v${version}=${digest}`
signature: `t=${timestamp}, v${version}=${digest}`,
'tenant-id': process.env.OPERATOR_TENANT_ID
}
}
})
Expand Down
20 changes: 20 additions & 0 deletions packages/auth/migrations/20241206232423_add_tenant_to_grant.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.alterTable('grants', function (table) {
table.uuid('tenantId').notNullable()
table.foreign('tenantId').references('tenants.id')
})
}

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.alterTable('grants', function (table) {
table.dropColumn('tenantId')
})
}
24 changes: 8 additions & 16 deletions packages/auth/src/access/service.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { faker } from '@faker-js/faker'
import nock from 'nock'
import { Knex } from 'knex'
import { v4 } from 'uuid'
import { createTestApp, TestContainer } from '../tests/app'
import { truncateTables } from '../tests/tableManager'
import { Config } from '../config/app'
import { IocContract } from '@adonisjs/fold'
import { initIocContainer } from '../'
import { AppServices } from '../app'
import { AccessService } from './service'
import { Grant, GrantState, StartMethod, FinishMethod } from '../grant/model'
import { Grant } from '../grant/model'
import { IncomingPaymentRequest, OutgoingPaymentRequest } from './types'
import { generateNonce, generateToken } from '../shared/utils'
import { generateBaseGrant } from '../tests/grant'
import { AccessType, AccessAction } from '@interledger/open-payments'
import { Access } from './model'
import { Tenant } from '../tenant/model'
import { generateTenant } from '../tests/tenant'

describe('Access Service', (): void => {
let deps: IocContract<AppServices>
Expand All @@ -22,19 +22,11 @@ describe('Access Service', (): void => {
let trx: Knex.Transaction
let grant: Grant

const generateBaseGrant = () => ({
state: GrantState.Pending,
startMethod: [StartMethod.Redirect],
continueToken: generateToken(),
continueId: v4(),
finishMethod: FinishMethod.Redirect,
finishUri: 'https://example.com/finish',
clientNonce: generateNonce(),
client: faker.internet.url({ appendSlash: false })
})

beforeEach(async (): Promise<void> => {
grant = await Grant.query(trx).insertAndFetch(generateBaseGrant())
const tenant = await Tenant.query(trx).insertAndFetch(generateTenant())
grant = await Grant.query(trx).insertAndFetch(
generateBaseGrant({ tenantId: tenant.id })
)
})

beforeAll(async (): Promise<void> => {
Expand Down
10 changes: 8 additions & 2 deletions packages/auth/src/access/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { createTestApp, TestContainer } from '../tests/app'
import { truncateTables } from '../tests/tableManager'
import { generateToken, generateNonce } from '../shared/utils'
import { compareRequestAndGrantAccessItems } from './utils'
import { Tenant } from '../tenant/model'
import { generateTenant } from '../tests/tenant'

describe('Access utilities', (): void => {
let deps: IocContract<AppServices>
Expand All @@ -25,6 +27,7 @@ describe('Access utilities', (): void => {
let identifier: string
let grant: Grant
let grantAccessItem: Access
let tenant: Tenant

const receiver: string =
'https://wallet.com/alice/incoming-payments/12341234-1234-1234-1234-123412341234'
Expand All @@ -36,6 +39,7 @@ describe('Access utilities', (): void => {

beforeEach(async (): Promise<void> => {
identifier = `https://example.com/${v4()}`
tenant = await Tenant.query(trx).insertAndFetch(generateTenant())
grant = await Grant.query(trx).insertAndFetch({
state: GrantState.Processing,
startMethod: [StartMethod.Redirect],
Expand All @@ -44,7 +48,8 @@ describe('Access utilities', (): void => {
finishMethod: FinishMethod.Redirect,
finishUri: 'https://example.com/finish',
clientNonce: generateNonce(),
client: faker.internet.url({ appendSlash: false })
client: faker.internet.url({ appendSlash: false }),
tenantId: tenant.id
})

grantAccessItem = await Access.query(trx).insertAndFetch({
Expand Down Expand Up @@ -241,7 +246,8 @@ describe('Access utilities', (): void => {
finishMethod: FinishMethod.Redirect,
finishUri: 'https://example.com/finish',
clientNonce: generateNonce(),
client: faker.internet.url({ appendSlash: false })
client: faker.internet.url({ appendSlash: false }),
tenantId: tenant.id
})

const grantAccessItem = await Access.query(trx).insertAndFetch({
Expand Down
20 changes: 17 additions & 3 deletions packages/auth/src/accessToken/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
import { GrantService } from '../grant/service'
import { AccessTokenService } from './service'
import { GNAPErrorCode } from '../shared/gnapErrors'
import { generateTenant } from '../tests/tenant'
import { Tenant } from '../tenant/model'

describe('Access Token Routes', (): void => {
let deps: IocContract<AppServices>
Expand Down Expand Up @@ -96,7 +98,11 @@ describe('Access Token Routes', (): void => {
const method = 'POST'

beforeEach(async (): Promise<void> => {
grant = await Grant.query(trx).insertAndFetch(BASE_GRANT)
const tenant = await Tenant.query().insertAndFetch(generateTenant())
grant = await Grant.query(trx).insertAndFetch({
...BASE_GRANT,
tenantId: tenant.id
})
access = await Access.query(trx).insertAndFetch({
grantId: grant.id,
...BASE_ACCESS
Expand Down Expand Up @@ -367,7 +373,11 @@ describe('Access Token Routes', (): void => {
let token: AccessToken

beforeEach(async (): Promise<void> => {
grant = await Grant.query(trx).insertAndFetch(BASE_GRANT)
const tenant = await Tenant.query().insertAndFetch(generateTenant())
grant = await Grant.query(trx).insertAndFetch({
...BASE_GRANT,
tenantId: tenant.id
})
token = await AccessToken.query(trx).insertAndFetch({
grantId: grant.id,
...BASE_TOKEN
Expand Down Expand Up @@ -406,7 +416,11 @@ describe('Access Token Routes', (): void => {
let token: AccessToken

beforeEach(async (): Promise<void> => {
grant = await Grant.query(trx).insertAndFetch(BASE_GRANT)
const tenant = await Tenant.query(trx).insertAndFetch(generateTenant())
grant = await Grant.query(trx).insertAndFetch({
...BASE_GRANT,
tenantId: tenant.id
})
access = await Access.query(trx).insertAndFetch({
grantId: grant.id,
...BASE_ACCESS
Expand Down
13 changes: 11 additions & 2 deletions packages/auth/src/accessToken/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
AccessItem
} from '@interledger/open-payments'
import { generateBaseGrant } from '../tests/grant'
import { Tenant } from '../tenant/model'
import { generateTenant } from '../tests/tenant'

describe('Access Token Service', (): void => {
let deps: IocContract<AppServices>
Expand Down Expand Up @@ -63,8 +65,9 @@ describe('Access Token Service', (): void => {

let grant: Grant
beforeEach(async (): Promise<void> => {
const tenant = await Tenant.query(trx).insertAndFetch(generateTenant())
grant = await Grant.query(trx).insertAndFetch(
generateBaseGrant({ state: GrantState.Approved })
generateBaseGrant({ state: GrantState.Approved, tenantId: tenant.id })
)
grant.access = [
await Access.query(trx).insertAndFetch({
Expand Down Expand Up @@ -186,8 +189,9 @@ describe('Access Token Service', (): void => {
})

test('Introspection only returns requested access', async (): Promise<void> => {
const tenant = await Tenant.query(trx).insertAndFetch(generateTenant())
const grantWithTwoAccesses = await Grant.query(trx).insertAndFetch(
generateBaseGrant({ state: GrantState.Approved })
generateBaseGrant({ state: GrantState.Approved, tenantId: tenant.id })
)
grantWithTwoAccesses.access = [
await Access.query(trx).insertAndFetch({
Expand Down Expand Up @@ -247,11 +251,14 @@ describe('Access Token Service', (): void => {
})

describe('Revoke', (): void => {
let tenant: Tenant
let grant: Grant
let token: AccessToken
beforeEach(async (): Promise<void> => {
tenant = await Tenant.query(trx).insertAndFetch(generateTenant())
grant = await Grant.query(trx).insertAndFetch(
generateBaseGrant({
tenantId: tenant.id,
state: GrantState.Finalized,
finalizationReason: GrantFinalization.Issued
})
Expand Down Expand Up @@ -352,8 +359,10 @@ describe('Access Token Service', (): void => {
let token: AccessToken
let originalTokenValue: string
beforeEach(async (): Promise<void> => {
const tenant = await Tenant.query(trx).insertAndFetch(generateTenant())
grant = await Grant.query(trx).insertAndFetch(
generateBaseGrant({
tenantId: tenant.id,
state: GrantState.Finalized,
finalizationReason: GrantFinalization.Issued
})
Expand Down
6 changes: 3 additions & 3 deletions packages/auth/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ export class App {
/* Back-channel GNAP Routes */
// Grant Initiation
router.post<DefaultState, CreateContext>(
'/',
'/:tenantId',
createValidatorMiddleware<CreateContext>(openApi.authServerSpec, {
path: '/',
method: HttpMethod.POST
Expand All @@ -278,7 +278,7 @@ export class App {

// Grant Continue
router.post<DefaultState, ContinueContext>(
'/continue/:id',
'/:tenantId/continue/:id',
createValidatorMiddleware<ContinueContext>(openApi.authServerSpec, {
path: '/continue/{id}',
method: HttpMethod.POST
Expand All @@ -289,7 +289,7 @@ export class App {

// Grant Cancel
router.delete<DefaultState, GrantRevokeContext>(
'/continue/:id',
'/:tenantId/continue/:id',
createValidatorMiddleware<GrantRevokeContext>(openApi.authServerSpec, {
path: '/continue/{id}',
method: HttpMethod.DELETE
Expand Down
27 changes: 24 additions & 3 deletions packages/auth/src/grant/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '@interledger/open-payments'
import { AccessToken, toOpenPaymentsAccessToken } from '../accessToken/model'
import { Interaction } from '../interaction/model'
import { Tenant } from '../tenant/model'

export enum StartMethod {
Redirect = 'redirect'
Expand Down Expand Up @@ -61,6 +62,14 @@ export class Grant extends BaseModel {
from: 'grants.id',
to: 'interactions.grantId'
}
},
tenant: {
relation: Model.HasOneRelation,
modelClass: join(__dirname, '../tenant/model'),
join: {
from: 'grants.tenantId',
to: 'tenants.id'
}
}
})
public access!: Access[]
Expand All @@ -79,6 +88,10 @@ export class Grant extends BaseModel {

public lastContinuedAt!: Date

public tenantId!: string

public tenant?: Tenant

public $beforeInsert(context: QueryContext): void {
super.$beforeInsert(context)
this.lastContinuedAt = new Date()
Expand Down Expand Up @@ -125,7 +138,7 @@ export function toOpenPaymentPendingGrant(
access_token: {
value: grant.continueToken
},
uri: `${authServerUrl}/continue/${grant.continueId}`,
uri: `${authServerUrl}/${grant.tenantId}/continue/${grant.continueId}`,
wait: waitTimeSeconds
}
}
Expand All @@ -145,7 +158,7 @@ export function toOpenPaymentsGrantContinuation(
access_token: {
value: grant.continueToken
},
uri: `${args.authServerUrl}/continue/${grant.continueId}`,
uri: `${args.authServerUrl}/${grant.tenantId}/continue/${grant.continueId}`,
wait: args.waitTimeSeconds
}
}
Expand All @@ -165,7 +178,7 @@ export function toOpenPaymentsGrant(
access_token: {
value: grant.continueToken
},
uri: `${args.authServerUrl}/continue/${grant.continueId}`
uri: `${args.authServerUrl}/${grant.tenantId}/continue/${grant.continueId}`
}
}
}
Expand All @@ -192,3 +205,11 @@ export function isRevokedGrant(grant: Grant): boolean {
grant.finalizationReason === GrantFinalization.Revoked
)
}

export interface GrantWithTenant extends Grant {
tenant: NonNullable<Grant['tenant']>
}

export function isGrantWithTenant(grant: Grant): grant is GrantWithTenant {
return !!grant.tenant
}
Loading
Loading