From a5c944bba86d8b71abe2bd2e9853a706d13acf14 Mon Sep 17 00:00:00 2001 From: Kallyn Gowdy Date: Fri, 7 Feb 2025 13:46:58 -0500 Subject: [PATCH] feat: Add the POST /api/v2/records/entitlements/revoke endpoint --- src/aux-records/RecordsServer.spec.ts | 214 ++++++++++++++++++ src/aux-records/RecordsServer.ts | 52 +++-- .../__snapshots__/RecordsServer.spec.ts.snap | 122 ++++++++++ 3 files changed, 371 insertions(+), 17 deletions(-) diff --git a/src/aux-records/RecordsServer.spec.ts b/src/aux-records/RecordsServer.spec.ts index f882832c4..e55ea31bf 100644 --- a/src/aux-records/RecordsServer.spec.ts +++ b/src/aux-records/RecordsServer.spec.ts @@ -16329,6 +16329,220 @@ describe('RecordsServer', () => { ); }); + describe('POST /api/v2/records/entitlement/revoke', () => { + beforeEach(async () => { + await packageStore.createItem(recordName, { + id: 'packageId', + address: 'address', + markers: [PUBLIC_READ_MARKER], + }); + + await packageVersionsStore.createItem(recordName, { + id: `packageVersionId`, + address: 'address', + key: { + major: 1, + minor: 0, + patch: 0, + tag: '', + }, + auxFileName: 'test.aux', + auxSha256: 'auxSha256', + sizeInBytes: 123, + createdAtMs: 999, + createdFile: true, + entitlements: [], + readme: '', + requiresReview: false, + sha256: 'sha256', + }); + + await store.saveGrantedPackageEntitlement({ + id: 'grantId', + userId: userId, + recordName, + packageId: 'packageId', + feature: 'data', + scope: 'designated', + expireTimeMs: Date.now() + 1000 * 60, + revokeTimeMs: null, + createdAtMs: Date.now(), + }); + + await store.saveGrantedPackageEntitlement({ + id: 'grantId2', + userId: userId, + recordName, + packageId: 'packageId', + feature: 'file', + scope: 'designated', + expireTimeMs: Date.now() + 1000 * 60, + revokeTimeMs: null, + createdAtMs: Date.now(), + }); + }); + + it('should revoke the given entitlement grant', async () => { + const result = await server.handleHttpRequest( + httpPost( + `/api/v2/records/entitlement/revoke`, + JSON.stringify({ + grantId: 'grantId', + }), + apiHeaders + ) + ); + + await expectResponseBodyToEqual(result, { + statusCode: 200, + body: { + success: true, + }, + headers: apiCorsHeaders, + }); + + expect(store.grantedPackageEntitlements).toEqual([ + { + id: 'grantId', + userId: userId, + recordName, + packageId: 'packageId', + feature: 'data', + scope: 'designated', + expireTimeMs: expect.any(Number), + revokeTimeMs: expect.any(Number), + createdAtMs: expect.any(Number), + }, + { + id: 'grantId2', + userId: userId, + recordName, + packageId: 'packageId', + feature: 'file', + scope: 'designated', + expireTimeMs: expect.any(Number), + revokeTimeMs: null, + createdAtMs: expect.any(Number), + }, + ]); + }); + + it('should revoke the grant for other users if the current user is a super user', async () => { + const owner = await store.findUser(ownerId); + await store.saveUser({ + ...owner, + role: 'superUser', + }); + + const result = await server.handleHttpRequest( + httpPost( + `/api/v2/records/entitlement/revoke`, + JSON.stringify({ + grantId: 'grantId', + }), + { + ...apiHeaders, + authorization: `Bearer ${ownerSessionKey}`, + } + ) + ); + + await expectResponseBodyToEqual(result, { + statusCode: 200, + body: { + success: true, + }, + headers: apiCorsHeaders, + }); + + expect(store.grantedPackageEntitlements).toEqual([ + { + id: 'grantId', + userId: userId, + recordName, + packageId: 'packageId', + feature: 'data', + scope: 'designated', + expireTimeMs: expect.any(Number), + revokeTimeMs: expect.any(Number), + createdAtMs: expect.any(Number), + }, + { + id: 'grantId2', + userId: userId, + recordName, + packageId: 'packageId', + feature: 'file', + scope: 'designated', + expireTimeMs: expect.any(Number), + revokeTimeMs: null, + createdAtMs: expect.any(Number), + }, + ]); + }); + + it('should return not_authorized if the user is trying to revoke a grant for someone else', async () => { + const result = await server.handleHttpRequest( + httpPost( + `/api/v2/records/entitlement/revoke`, + JSON.stringify({ + grantId: 'grantId', + }), + { + ...apiHeaders, + authorization: `Bearer ${ownerSessionKey}`, + } + ) + ); + + await expectResponseBodyToEqual(result, { + statusCode: 403, + body: { + success: false, + errorCode: 'not_authorized', + errorMessage: + 'You are not authorized to perform this action.', + }, + headers: apiCorsHeaders, + }); + + expect(store.grantedPackageEntitlements).toEqual([ + { + id: 'grantId', + userId: userId, + recordName, + packageId: 'packageId', + feature: 'data', + scope: 'designated', + expireTimeMs: expect.any(Number), + revokeTimeMs: null, + createdAtMs: expect.any(Number), + }, + { + id: 'grantId2', + userId: userId, + recordName, + packageId: 'packageId', + feature: 'file', + scope: 'designated', + expireTimeMs: expect.any(Number), + revokeTimeMs: null, + createdAtMs: expect.any(Number), + }, + ]); + }); + + testUrl( + 'POST', + '/api/v2/records/entitlement/revoke', + () => + JSON.stringify({ + grantId: 'grantId', + }), + () => apiHeaders + ); + }); + describe('GET /api/v2/records/insts/list', () => { const inst1 = 'myInst'; const inst2 = 'myInst2'; diff --git a/src/aux-records/RecordsServer.ts b/src/aux-records/RecordsServer.ts index bed0654d6..984e26704 100644 --- a/src/aux-records/RecordsServer.ts +++ b/src/aux-records/RecordsServer.ts @@ -3024,26 +3024,13 @@ export class RecordsServer { return sessionKeyValidation; } - if (!userId) { - userId = sessionKeyValidation.userId; - } - - if ( - userId !== sessionKeyValidation.userId && - sessionKeyValidation.role !== 'superUser' - ) { - return { - success: false, - errorCode: 'not_authorized', - errorMessage: - 'You are not authorized to perform this action.', - }; - } - const result = await this._policyController.grantEntitlement({ packageId, - userId, + userId: sessionKeyValidation.userId, + userRole: sessionKeyValidation.role, + grantingUserId: + userId ?? sessionKeyValidation.userId, recordName, feature, scope, @@ -3054,6 +3041,37 @@ export class RecordsServer { } ), + revokeEntitlement: procedure() + .origins('api') + .http('POST', '/api/v2/records/entitlement/revoke') + .inputs( + z.object({ + grantId: z.string(), + }) + ) + .handler(async ({ grantId }, context) => { + const sessionKeyValidation = await this._validateSessionKey( + context.sessionKey + ); + if (sessionKeyValidation.success === false) { + if ( + sessionKeyValidation.errorCode === 'no_session_key' + ) { + return NOT_LOGGED_IN_RESULT; + } + return sessionKeyValidation; + } + + const result = + await this._policyController.revokeEntitlement({ + userId: sessionKeyValidation.userId, + userRole: sessionKeyValidation.role, + grantId, + }); + + return result; + }), + listGrantedEntitlements: procedure() .origins('api') .http('GET', '/api/v2/records/entitlement/grants/list') diff --git a/src/aux-records/__snapshots__/RecordsServer.spec.ts.snap b/src/aux-records/__snapshots__/RecordsServer.spec.ts.snap index 90b3ec25a..8e4fadb14 100644 --- a/src/aux-records/__snapshots__/RecordsServer.spec.ts.snap +++ b/src/aux-records/__snapshots__/RecordsServer.spec.ts.snap @@ -3288,6 +3288,67 @@ Object { "name": "revokeRole", "origins": "api", }, + Object { + "http": Object { + "method": "POST", + "path": "/api/v2/records/entitlement/grants", + }, + "inputs": Object { + "schema": Object { + "expireTimeMs": Object { + "type": "number", + }, + "feature": Object { + "type": "enum", + "values": Array [ + "data", + "file", + "event", + "inst", + "notification", + "package", + "permissions", + "webhook", + "ai", + ], + }, + "packageId": Object { + "type": "string", + }, + "recordName": Object { + "type": "string", + }, + "scope": Object { + "type": "literal", + "value": "designated", + }, + "userId": Object { + "nullable": true, + "optional": true, + "type": "string", + }, + }, + "type": "object", + }, + "name": "grantEntitlement", + "origins": "api", + }, + Object { + "http": Object { + "method": "POST", + "path": "/api/v2/records/entitlement/revoke", + }, + "inputs": Object { + "schema": Object { + "grantId": Object { + "type": "string", + }, + }, + "type": "object", + }, + "name": "revokeEntitlement", + "origins": "api", + }, Object { "http": Object { "method": "GET", @@ -7685,6 +7746,67 @@ Object { "name": "revokeRole", "origins": "api", }, + Object { + "http": Object { + "method": "POST", + "path": "/api/v2/records/entitlement/grants", + }, + "inputs": Object { + "schema": Object { + "expireTimeMs": Object { + "type": "number", + }, + "feature": Object { + "type": "enum", + "values": Array [ + "data", + "file", + "event", + "inst", + "notification", + "package", + "permissions", + "webhook", + "ai", + ], + }, + "packageId": Object { + "type": "string", + }, + "recordName": Object { + "type": "string", + }, + "scope": Object { + "type": "literal", + "value": "designated", + }, + "userId": Object { + "nullable": true, + "optional": true, + "type": "string", + }, + }, + "type": "object", + }, + "name": "grantEntitlement", + "origins": "api", + }, + Object { + "http": Object { + "method": "POST", + "path": "/api/v2/records/entitlement/revoke", + }, + "inputs": Object { + "schema": Object { + "grantId": Object { + "type": "string", + }, + }, + "type": "object", + }, + "name": "revokeEntitlement", + "origins": "api", + }, Object { "http": Object { "method": "GET",