From 5a69214561018a89bdfb7c96c6923f7cbc83e2ce Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Fri, 23 Aug 2024 15:24:24 +0200 Subject: [PATCH] feat(backend): add subset actions if present --- .../src/open_payments/grant/service.test.ts | 55 +++++++++++++++++++ .../src/open_payments/grant/service.ts | 28 +++++++++- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/open_payments/grant/service.test.ts b/packages/backend/src/open_payments/grant/service.test.ts index 3f408073b8..15b48ac9c1 100644 --- a/packages/backend/src/open_payments/grant/service.test.ts +++ b/packages/backend/src/open_payments/grant/service.test.ts @@ -362,6 +362,61 @@ describe('Grant Service', (): void => { expect(authServerServiceGetOrCreateSoy).toHaveBeenCalled() }) + test('creates new grant with additional subset actions', async () => { + const newOpenPaymentsGrant = mockGrant() + const openPaymentsGrantRequestSpy = jest + .spyOn(openPaymentsClient.grant, 'request') + .mockResolvedValueOnce({ + ...newOpenPaymentsGrant + }) + + const options = { + authServer: authServer.url, + accessType: AccessType.IncomingPayment, + accessActions: [ + AccessAction.Create, + AccessAction.ReadAll, + AccessAction.ListAll + ] + } + + const authServerServiceGetOrCreateSoy = jest.spyOn( + authServerService, + 'getOrCreate' + ) + + const grant = await grantService.getOrCreate(options) + + assert(!isGrantError(grant)) + expect(grant.accessActions.sort()).toEqual( + [ + AccessAction.Create, + AccessAction.ReadAll, + AccessAction.ListAll, + AccessAction.List, + AccessAction.Read + ].sort() + ) + expect(openPaymentsGrantRequestSpy).toHaveBeenCalledWith( + { url: options.authServer }, + { + access_token: { + access: [ + { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + type: options.accessType as any, + actions: options.accessActions + } + ] + }, + interact: { + start: ['redirect'] + } + } + ) + expect(authServerServiceGetOrCreateSoy).toHaveBeenCalled() + }) + test('creates new grant and deletes old one after being unable to rotate existing token', async () => { const existingGrant = await Grant.query(knex).insertAndFetch({ authServerId: authServer.id, diff --git a/packages/backend/src/open_payments/grant/service.ts b/packages/backend/src/open_payments/grant/service.ts index f5d94503d6..b6ef161e80 100644 --- a/packages/backend/src/open_payments/grant/service.ts +++ b/packages/backend/src/open_payments/grant/service.ts @@ -131,7 +131,9 @@ export async function getExistingGrant( }) .whereNull('deletedAt') .andWhere('authServer.url', options.authServer) - .andWhere('accessActions', '@>', options.accessActions) // all options.accessActions are a subset of saved accessActions + // all options.accessActions are a subset of saved accessActions + // e.g. if [ReadAll, Create] is saved, requesting just [Create] would still match + .andWhere('accessActions', '@>', options.accessActions) .withGraphJoined('authServer') } @@ -178,7 +180,7 @@ async function requestNewGrant( return Grant.query(deps.knex) .insertAndFetch({ accessType: options.accessType, - accessActions: options.accessActions, + accessActions: addSubsetActions(options.accessActions), accessToken: openPaymentsGrant.access_token.value, managementId: retrieveManagementId(openPaymentsGrant.access_token.manage), authServerId, @@ -235,6 +237,28 @@ async function deleteGrant( }) } +function addSubsetActions(accessActions: AccessAction[]): AccessAction[] { + const newAccessActions = [...accessActions] + + // Read is a subset action of ReadAll + if ( + accessActions.includes(AccessAction.ReadAll) && + !accessActions.includes(AccessAction.Read) + ) { + newAccessActions.push(AccessAction.Read) + } + + // List is a subset action of ListAll + if ( + accessActions.includes(AccessAction.ListAll) && + !accessActions.includes(AccessAction.List) + ) { + newAccessActions.push(AccessAction.List) + } + + return newAccessActions +} + function retrieveManagementId(managementUrl: string): string { const managementUrlParts = managementUrl.split('/') const managementId = managementUrlParts.pop() || managementUrlParts.pop() // handle trailing slash