diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts index 4ec50799ae0..01b0d7bc380 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts @@ -11,10 +11,13 @@ import { OrganizationUserBulkResponse, } from "@bitwarden/admin-console/common"; import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response"; import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { StateProvider } from "@bitwarden/common/platform/state"; @@ -23,11 +26,13 @@ import { OrgKey } from "@bitwarden/common/types/key"; import { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; +import { OrganizationUserService } from "../../services/organization-user/organization-user.service"; + import { BaseBulkConfirmComponent } from "./base-bulk-confirm.component"; import { BulkUserDetails } from "./bulk-status.component"; type BulkConfirmDialogParams = { - organizationId: string; + organization: Organization; users: BulkUserDetails[]; }; @@ -36,7 +41,7 @@ type BulkConfirmDialogParams = { standalone: false, }) export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent { - organizationId: string; + organization: Organization; organizationKey$: Observable; users: BulkUserDetails[]; @@ -47,13 +52,15 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent { private organizationUserApiService: OrganizationUserApiService, protected i18nService: I18nService, private stateProvider: StateProvider, + private organizationUserService: OrganizationUserService, + private configService: ConfigService, ) { super(keyService, encryptService, i18nService); - this.organizationId = dialogParams.organizationId; + this.organization = dialogParams.organization; this.organizationKey$ = this.stateProvider.activeUserId$.pipe( switchMap((userId) => this.keyService.orgKeys$(userId)), - map((organizationKeysById) => organizationKeysById[this.organizationId as OrganizationId]), + map((organizationKeysById) => organizationKeysById[this.organization.id as OrganizationId]), takeUntilDestroyed(), ); this.users = dialogParams.users; @@ -66,7 +73,7 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent { ListResponse > => await this.organizationUserApiService.postOrganizationUsersPublicKey( - this.organizationId, + this.organization.id, this.filteredUsers.map((user) => user.id), ); @@ -76,11 +83,19 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent { protected postConfirmRequest = async ( userIdsWithKeys: { id: string; key: string }[], ): Promise> => { - const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys); - return await this.organizationUserApiService.postOrganizationUserBulkConfirm( - this.organizationId, - request, - ); + if ( + await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation)) + ) { + return await firstValueFrom( + this.organizationUserService.bulkConfirmUsers(this.organization, userIdsWithKeys), + ); + } else { + const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys); + return await this.organizationUserApiService.postOrganizationUserBulkConfirm( + this.organization.id, + request, + ); + } }; static open(dialogService: DialogService, config: DialogConfig) { diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index a9cfd79ad60..2e663115819 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -721,7 +721,7 @@ export class MembersComponent extends BaseMembersComponent const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, { data: { - organizationId: this.organization.id, + organization: this.organization, users: this.dataSource.getCheckedUsers(), }, }); diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user/organization-user.service.spec.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user/organization-user.service.spec.ts new file mode 100644 index 00000000000..2ae5aa4eb98 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user/organization-user.service.spec.ts @@ -0,0 +1,175 @@ +import { TestBed } from "@angular/core/testing"; +import { of } from "rxjs"; + +import { + OrganizationUserConfirmRequest, + OrganizationUserBulkConfirmRequest, + OrganizationUserApiService, + OrganizationUserBulkResponse, +} from "@bitwarden/admin-console/common"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { OrgKey } from "@bitwarden/common/types/key"; +import { KeyService } from "@bitwarden/key-management"; + +import { OrganizationUserView } from "../../../core/views/organization-user.view"; + +import { OrganizationUserService } from "./organization-user.service"; + +describe("OrganizationUserService", () => { + let service: OrganizationUserService; + let keyService: jest.Mocked; + let encryptService: jest.Mocked; + let organizationUserApiService: jest.Mocked; + let accountService: jest.Mocked; + let i18nService: jest.Mocked; + + const mockOrganization = new Organization(); + mockOrganization.id = "org-123" as OrganizationId; + + const mockOrganizationUser = new OrganizationUserView(); + mockOrganizationUser.id = "user-123"; + + const mockPublicKey = new Uint8Array(64) as CsprngArray; + const mockRandomBytes = new Uint8Array(64) as CsprngArray; + const mockOrgKey = new SymmetricCryptoKey(mockRandomBytes) as OrgKey; + const mockEncryptedKey = { encryptedString: "encrypted-key" } as EncString; + const mockEncryptedCollectionName = { encryptedString: "encrypted-collection-name" } as EncString; + const mockDefaultCollectionName = "My Items"; + + const setupCommonMocks = () => { + keyService.orgKeys$.mockReturnValue( + of({ [mockOrganization.id]: mockOrgKey } as Record), + ); + encryptService.encryptString.mockResolvedValue(mockEncryptedCollectionName); + i18nService.t.mockReturnValue(mockDefaultCollectionName); + }; + + beforeEach(() => { + keyService = { + orgKeys$: jest.fn(), + } as any; + + encryptService = { + encryptString: jest.fn(), + encapsulateKeyUnsigned: jest.fn(), + } as any; + + organizationUserApiService = { + postOrganizationUserConfirm: jest.fn(), + postOrganizationUserBulkConfirm: jest.fn(), + } as any; + + accountService = { + activeAccount$: of({ id: "user-123" }), + } as any; + + i18nService = { + t: jest.fn(), + } as any; + + TestBed.configureTestingModule({ + providers: [ + OrganizationUserService, + { provide: KeyService, useValue: keyService }, + { provide: EncryptService, useValue: encryptService }, + { provide: OrganizationUserApiService, useValue: organizationUserApiService }, + { provide: AccountService, useValue: accountService }, + { provide: I18nService, useValue: i18nService }, + ], + }); + + service = TestBed.inject(OrganizationUserService); + }); + + describe("confirmUser", () => { + beforeEach(() => { + setupCommonMocks(); + encryptService.encapsulateKeyUnsigned.mockResolvedValue(mockEncryptedKey); + organizationUserApiService.postOrganizationUserConfirm.mockReturnValue(Promise.resolve()); + }); + + it("should confirm a user successfully", (done) => { + service.confirmUser(mockOrganization, mockOrganizationUser, mockPublicKey).subscribe({ + next: () => { + expect(i18nService.t).toHaveBeenCalledWith("myItems"); + + expect(encryptService.encryptString).toHaveBeenCalledWith( + mockDefaultCollectionName, + mockOrgKey, + ); + expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith( + mockOrgKey, + mockPublicKey, + ); + + expect(organizationUserApiService.postOrganizationUserConfirm).toHaveBeenCalledWith( + mockOrganization.id, + mockOrganizationUser.id, + { + key: mockEncryptedKey.encryptedString, + defaultUserCollectionName: mockEncryptedCollectionName.encryptedString, + } as OrganizationUserConfirmRequest, + ); + + done(); + }, + error: done, + }); + }); + }); + + describe("bulkConfirmUsers", () => { + const mockUserIdsWithKeys = [ + { id: "user-1", key: "key-1" }, + { id: "user-2", key: "key-2" }, + ]; + + const mockBulkResponse = { + data: [ + { id: "user-1", error: null } as OrganizationUserBulkResponse, + { id: "user-2", error: null } as OrganizationUserBulkResponse, + ], + } as ListResponse; + + beforeEach(() => { + setupCommonMocks(); + organizationUserApiService.postOrganizationUserBulkConfirm.mockReturnValue( + Promise.resolve(mockBulkResponse), + ); + }); + + it("should bulk confirm users successfully", (done) => { + service.bulkConfirmUsers(mockOrganization, mockUserIdsWithKeys).subscribe({ + next: (response) => { + expect(i18nService.t).toHaveBeenCalledWith("myItems"); + + expect(encryptService.encryptString).toHaveBeenCalledWith( + mockDefaultCollectionName, + mockOrgKey, + ); + + expect(organizationUserApiService.postOrganizationUserBulkConfirm).toHaveBeenCalledWith( + mockOrganization.id, + new OrganizationUserBulkConfirmRequest( + mockUserIdsWithKeys, + mockEncryptedCollectionName.encryptedString, + ), + ); + + expect(response).toEqual(mockBulkResponse); + + done(); + }, + error: done, + }); + }); + }); +}); diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user/organization-user.service.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user/organization-user.service.ts index 79efeebca2a..f59b377e26e 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user/organization-user.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user/organization-user.service.ts @@ -3,12 +3,15 @@ import { combineLatest, filter, map, Observable, switchMap } from "rxjs"; import { OrganizationUserConfirmRequest, + OrganizationUserBulkConfirmRequest, OrganizationUserApiService, + OrganizationUserBulkResponse, } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { KeyService } from "@bitwarden/key-management"; @@ -41,11 +44,7 @@ export class OrganizationUserService { user: OrganizationUserView, publicKey: Uint8Array, ): Observable { - const encryptedCollectionName$ = this.orgKey$(organization).pipe( - switchMap((orgKey) => - this.encryptService.encryptString(this.i18nService.t("myItems"), orgKey), - ), - ); + const encryptedCollectionName$ = this.getEncryptedDefaultCollectionName$(organization); const encryptedKey$ = this.orgKey$(organization).pipe( switchMap((orgKey) => this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey)), @@ -66,4 +65,31 @@ export class OrganizationUserService { }), ); } + + bulkConfirmUsers( + organization: Organization, + userIdsWithKeys: { id: string; key: string }[], + ): Observable> { + return this.getEncryptedDefaultCollectionName$(organization).pipe( + switchMap((collectionName) => { + const request = new OrganizationUserBulkConfirmRequest( + userIdsWithKeys, + collectionName.encryptedString, + ); + + return this.organizationUserApiService.postOrganizationUserBulkConfirm( + organization.id, + request, + ); + }), + ); + } + + private getEncryptedDefaultCollectionName$(organization: Organization) { + return this.orgKey$(organization).pipe( + switchMap((orgKey) => + this.encryptService.encryptString(this.i18nService.t("myItems"), orgKey), + ), + ); + } } diff --git a/libs/admin-console/src/common/organization-user/models/requests/organization-user-bulk-confirm.request.ts b/libs/admin-console/src/common/organization-user/models/requests/organization-user-bulk-confirm.request.ts index 35e05602838..4523c3afebc 100644 --- a/libs/admin-console/src/common/organization-user/models/requests/organization-user-bulk-confirm.request.ts +++ b/libs/admin-console/src/common/organization-user/models/requests/organization-user-bulk-confirm.request.ts @@ -1,3 +1,5 @@ +import { EncString as SdkEncString } from "@bitwarden/sdk-internal"; + type OrganizationUserBulkRequestEntry = { id: string; key: string; @@ -5,8 +7,10 @@ type OrganizationUserBulkRequestEntry = { export class OrganizationUserBulkConfirmRequest { keys: OrganizationUserBulkRequestEntry[]; + defaultUserCollectionName: SdkEncString | undefined; - constructor(keys: OrganizationUserBulkRequestEntry[]) { + constructor(keys: OrganizationUserBulkRequestEntry[], defaultUserCollectionName?: SdkEncString) { this.keys = keys; + this.defaultUserCollectionName = defaultUserCollectionName; } }