From a49e7bb35f27c61b1c466d4ca27406df21c4efd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Fri, 24 May 2024 15:48:47 +0100 Subject: [PATCH] [AC-2303] Implement approveAllRequests method (#9031) * [AC-2302] Move organization-auth-request.service to bit-common folder * [AC-2302] Rename organization-auth-request.service to organization-auth-request-api.service * [AC-2302] Move logic from component to organization-auth-request.service * [AC-2302] Fix import path in OrganizationAuthRequestService * [AC-2302] Move imports to OrganizationsModule and delete unused CoreOrganizationModule * [AC-2302] Move the call to get userResetPasswordDetails into OrganizationAuthRequestService * [AC-2302] Remove @Injectable() and manually configure dependencies * [AC-2302] Add OrganizationAuthRequestService unit tests first draft * [AC-2302] Refactor device-approvals.component.ts to remove unused imports * [AC-2302] Set up jest on bit-common and add unit tests for OrganizationAuthRequestService * [AC-2302] Add bit-common to jest.config.js * [AC-2302] Update organizations.module.ts to include safeProviders declared in variable * [AC-2302] Remove services and views folders from bit-common * [AC-2302] Define path mapping * Adjust an import path The import path of `PendingAuthRequestView` in `OrganizationAuthRequestApiService` was pointing to the wrong place. I think this file was just recently moved, and the import didn't get updated. * Get paths working * Fix import * Update jest config to use ts-jest adn jsdom * Copy-paste path mappings from bit-web * Remove unnecessary test setup file * Undo unnecessary change * Fix remaining path mappings * Remove Bitwarden License mapping from OSS code * Fix bit-web so it uses its own tsconfig * Fix import path * Remove web-bit entrypoint from OSS tsconfig * Make DeviceApprovalsComponent standalone * Remove organization-auth-request-api.service export * Add BulkApproveAuthRequestsRequest class for bulk approval of authentication requests * Add api call for device bulk approvals * Add bulk device approval to OrganizationAuthRequestService * Add unit tests for bulk device approval method * Remove OrganizationsRoutingModule from DeviceApprovalsComponent imports * Remove CoreOrganizationModule from OrganizationsModule imports * Remove NoItemsModule from OrganizationsModule imports * Get keys for each item to approve * Update approvePendingRequests unit test * Use ApiService from JslibServicesModule * Update providers in device-approvals.component.ts * Add method to retrieve reset password details for multiple organization users * Add organizationUserId property to OrganizationUserResetPasswordDetailsResponse class * Use method to retrieve reset password details for multiple organization users * Rename ResetPasswordDetails to AccountRecoveryDetails * Update OrganizationAuthRequestService to use getManyOrganizationUserAccountRecoveryDetails * Update AdminAuthRequestUpdateWithIdRequest property names and imports * Refactor bulk approval functionality in organization auth requests * Rename update request AdminAuthRequestUpdateWithIdRequest to OrganizationAuthRequestUpdateRequest * Update organization-auth-request.service.spec.ts to use bulkUpdatePendingRequests method --------- Co-authored-by: Addison Beck Co-authored-by: Thomas Rittson --- .../organization-auth-request-api.service.ts | 14 +++++ ...rganization-auth-request-update.request.ts | 7 +++ .../organization-auth-request.service.spec.ts | 51 +++++++++++++++++++ .../organization-auth-request.service.ts | 37 ++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request-update.request.ts diff --git a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request-api.service.ts b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request-api.service.ts index 9b8e4c246d..3387b7d54e 100644 --- a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request-api.service.ts +++ b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request-api.service.ts @@ -4,6 +4,7 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { AdminAuthRequestUpdateRequest } from "./admin-auth-request-update.request"; import { BulkDenyAuthRequestsRequest } from "./bulk-deny-auth-requests.request"; +import { OrganizationAuthRequestUpdateRequest } from "./organization-auth-request-update.request"; import { PendingAuthRequestView } from "./pending-auth-request.view"; import { PendingOrganizationAuthRequestResponse } from "./pending-organization-auth-request.response"; @@ -34,6 +35,19 @@ export class OrganizationAuthRequestApiService { ); } + async bulkUpdatePendingRequests( + organizationId: string, + items: OrganizationAuthRequestUpdateRequest[], + ): Promise { + await this.apiService.send( + "POST", + `/organizations/${organizationId}/auth-requests`, + items, + true, + false, + ); + } + async approvePendingRequest( organizationId: string, requestId: string, diff --git a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request-update.request.ts b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request-update.request.ts new file mode 100644 index 0000000000..5596ce853f --- /dev/null +++ b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request-update.request.ts @@ -0,0 +1,7 @@ +export class OrganizationAuthRequestUpdateRequest { + constructor( + public id: string, + public approved: boolean, + public key?: string, + ) {} +} diff --git a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts index 24c6f06be3..ee3e0d73f4 100644 --- a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts +++ b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts @@ -2,10 +2,12 @@ import { MockProxy, mock } from "jest-mock-extended"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { OrganizationUserResetPasswordDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { OrganizationAuthRequestApiService } from "./organization-auth-request-api.service"; +import { OrganizationAuthRequestUpdateRequest } from "./organization-auth-request-update.request"; import { OrganizationAuthRequestService } from "./organization-auth-request.service"; import { PendingAuthRequestView } from "./pending-auth-request.view"; @@ -92,6 +94,55 @@ describe("OrganizationAuthRequestService", () => { }); }); + describe("approvePendingRequests", () => { + it("should approve the specified pending auth requests", async () => { + jest.spyOn(organizationAuthRequestApiService, "bulkUpdatePendingRequests"); + + const organizationId = "organizationId"; + + const organizationUserResetPasswordDetailsResponse = new ListResponse( + { + Data: [ + { + organizationUserId: "organizationUserId1", + resetPasswordKey: "resetPasswordKey", + encryptedPrivateKey: "encryptedPrivateKey", + }, + ], + }, + OrganizationUserResetPasswordDetailsResponse, + ); + + organizationUserService.getManyOrganizationUserAccountRecoveryDetails.mockResolvedValueOnce( + organizationUserResetPasswordDetailsResponse, + ); + + const encryptedUserKey = new EncString("encryptedUserKey"); + cryptoService.rsaDecrypt.mockResolvedValue(new Uint8Array(32)); + cryptoService.rsaEncrypt.mockResolvedValue(encryptedUserKey); + + const mockPendingAuthRequest = new PendingAuthRequestView(); + mockPendingAuthRequest.id = "requestId1"; + mockPendingAuthRequest.organizationUserId = "organizationUserId1"; + mockPendingAuthRequest.publicKey = "publicKey1"; + + await organizationAuthRequestService.approvePendingRequests(organizationId, [ + mockPendingAuthRequest, + ]); + + expect(organizationAuthRequestApiService.bulkUpdatePendingRequests).toHaveBeenCalledWith( + organizationId, + [ + new OrganizationAuthRequestUpdateRequest( + "requestId1", + true, + encryptedUserKey.encryptedString, + ), + ], + ); + }); + }); + describe("approvePendingRequest", () => { it("should approve the specified pending auth request", async () => { jest.spyOn(organizationAuthRequestApiService, "approvePendingRequest"); diff --git a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts index efc2cdcedb..c35b91396d 100644 --- a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts +++ b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts @@ -6,6 +6,7 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { OrganizationAuthRequestApiService } from "./organization-auth-request-api.service"; +import { OrganizationAuthRequestUpdateRequest } from "./organization-auth-request-update.request"; import { PendingAuthRequestView } from "./pending-auth-request.view"; export class OrganizationAuthRequestService { @@ -23,6 +24,42 @@ export class OrganizationAuthRequestService { await this.organizationAuthRequestApiService.denyPendingRequests(organizationId, ...requestIds); } + async approvePendingRequests( + organizationId: string, + authRequests: PendingAuthRequestView[], + ): Promise { + const organizationUserIds = authRequests.map((r) => r.organizationUserId); + const details = + await this.organizationUserService.getManyOrganizationUserAccountRecoveryDetails( + organizationId, + organizationUserIds, + ); + + if ( + details == null || + details.data.length == 0 || + details.data.some((d) => d.resetPasswordKey == null) + ) { + throw new Error( + "All users must be enrolled in account recovery (password reset) in order for the requests to be approved.", + ); + } + + const requestsToApprove = await Promise.all( + authRequests.map(async (r) => { + const detail = details.data.find((d) => d.organizationUserId === r.organizationUserId); + const encryptedKey = await this.getEncryptedUserKey(organizationId, r.publicKey, detail); + + return new OrganizationAuthRequestUpdateRequest(r.id, true, encryptedKey.encryptedString); + }), + ); + + await this.organizationAuthRequestApiService.bulkUpdatePendingRequests( + organizationId, + requestsToApprove, + ); + } + async approvePendingRequest(organizationId: string, authRequest: PendingAuthRequestView) { const details = await this.organizationUserService.getOrganizationUserResetPasswordDetails( organizationId,