From d4952d211e047972767940fc83ea83383bd4cc91 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Tue, 12 Aug 2025 12:06:55 -0400 Subject: [PATCH] [PM-24096] replace getOrgKey with orgKey$, refactor collectionAdminService (#15928) * replace getOrgKey with orgKey$, refactor collectionAdminService * clean up * uncomment accidental commet * remove cache --- .../admin-console/commands/confirm.command.ts | 15 ++++- apps/cli/src/commands/edit.command.ts | 11 +++- apps/cli/src/commands/get.command.ts | 10 +++- apps/cli/src/oss-serve-configurator.ts | 1 + apps/cli/src/vault.program.ts | 1 + .../vault-header/vault-header.component.ts | 14 ++++- .../collections/vault.component.ts | 7 ++- .../manage/group-add-edit.component.ts | 7 ++- .../member-dialog/member-dialog.component.ts | 10 +++- .../members/members.component.ts | 17 +++++- ...zation-user-reset-password.service.spec.ts | 14 ++++- ...rganization-user-reset-password.service.ts | 14 ++++- .../settings/account.component.ts | 10 +++- .../collection-dialog.component.ts | 8 ++- .../import/import-collection-admin.service.ts | 11 +++- .../vault-header/vault-header.component.ts | 14 ++++- ...console-cipher-form-config.service.spec.ts | 4 +- ...dmin-console-cipher-form-config.service.ts | 15 ++--- .../bit-cli/src/service-container.ts | 1 + .../organization-auth-request.service.spec.ts | 15 +++++ .../organization-auth-request.service.ts | 14 ++++- .../device-approvals.component.ts | 2 + .../services/web-provider.service.ts | 11 +++- .../abstractions/collection-admin.service.ts | 9 +-- .../default-collection-admin.service.ts | 59 +++++++++---------- .../src/components/import.component.ts | 2 +- .../import-collection.service.abstraction.ts | 3 +- 27 files changed, 226 insertions(+), 73 deletions(-) diff --git a/apps/cli/src/admin-console/commands/confirm.command.ts b/apps/cli/src/admin-console/commands/confirm.command.ts index f2965a2340..adae509117 100644 --- a/apps/cli/src/admin-console/commands/confirm.command.ts +++ b/apps/cli/src/admin-console/commands/confirm.command.ts @@ -1,17 +1,20 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map, switchMap } from "rxjs"; import { OrganizationUserApiService, OrganizationUserConfirmRequest, } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; import { EncString } from "@bitwarden/sdk-internal"; @@ -24,6 +27,7 @@ export class ConfirmCommand { private keyService: KeyService, private encryptService: EncryptService, private organizationUserApiService: OrganizationUserApiService, + private accountService: AccountService, private configService: ConfigService, private i18nService: I18nService, ) {} @@ -53,7 +57,14 @@ export class ConfirmCommand { return Response.badRequest("`" + options.organizationId + "` is not a GUID."); } try { - const orgKey = await this.keyService.getOrgKey(options.organizationId); + const orgKey = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + map((orgKeys) => orgKeys[options.organizationId as OrganizationId] ?? null), + ), + ); + if (orgKey == null) { throw new Error("No encryption key for this organization."); } diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index c288156865..e1f4f33149 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map, switchMap } from "rxjs"; import { CollectionRequest } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -14,6 +14,7 @@ import { CipherExport } from "@bitwarden/common/models/export/cipher.export"; import { CollectionExport } from "@bitwarden/common/models/export/collection.export"; import { FolderExport } from "@bitwarden/common/models/export/folder.export"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -201,7 +202,13 @@ export class EditCommand { return Response.badRequest("`organizationid` option does not match request object."); } try { - const orgKey = await this.keyService.getOrgKey(req.organizationId); + const orgKey = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + map((orgKeys) => orgKeys[options.organizationId as OrganizationId] ?? null), + ), + ); if (orgKey == null) { throw new Error("No encryption key for this organization."); } diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index 756316cba4..b0f0c1f7d1 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom, map, switchMap } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -485,7 +485,13 @@ export class GetCommand extends DownloadCommand { return Response.badRequest("`" + options.organizationId + "` is not a GUID."); } try { - const orgKey = await this.keyService.getOrgKey(options.organizationId); + const orgKey = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + map((orgKeys) => orgKeys[options.organizationId as OrganizationId] ?? null), + ), + ); if (orgKey == null) { throw new Error("No encryption key for this organization."); } diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts index 760fa8935b..df46e22f84 100644 --- a/apps/cli/src/oss-serve-configurator.ts +++ b/apps/cli/src/oss-serve-configurator.ts @@ -131,6 +131,7 @@ export class OssServeConfigurator { this.serviceContainer.keyService, this.serviceContainer.encryptService, this.serviceContainer.organizationUserApiService, + this.serviceContainer.accountService, this.serviceContainer.configService, this.serviceContainer.i18nService, ); diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index dfd18570e4..5b35f6b049 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -432,6 +432,7 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.keyService, this.serviceContainer.encryptService, this.serviceContainer.organizationUserApiService, + this.serviceContainer.accountService, this.serviceContainer.configService, this.serviceContainer.i18nService, ); diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts index 121a5c03ff..1be16c65cb 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts @@ -5,7 +5,7 @@ import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Input, Output } from "@angular/core"; import { Router } from "@angular/router"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, switchMap } from "rxjs"; import { CollectionAdminService, @@ -14,6 +14,8 @@ import { } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; 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 { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -99,6 +101,7 @@ export class VaultHeaderComponent { private dialogService: DialogService, private collectionAdminService: CollectionAdminService, private router: Router, + private accountService: AccountService, ) {} get title() { @@ -199,7 +202,14 @@ export class VaultHeaderComponent { async addCollection() { if (this.organization.productTierType === ProductTierType.Free) { - const collections = await this.collectionAdminService.getAll(this.organization.id); + const collections = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.collectionAdminService.collectionAdminViews$(this.organization.id, userId), + ), + ), + ); if (collections.length === this.organization.maxCollections) { this.showFreeOrgUpgradeDialog(); return; diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index 9653c40549..fe63830b89 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -363,7 +363,12 @@ export class VaultComponent implements OnInit, OnDestroy { this.allCollectionsWithoutUnassigned$ = this.refresh$.pipe( switchMap(() => organizationId$), - switchMap((orgId) => this.collectionAdminService.getAll(orgId)), + switchMap((orgId) => + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.collectionAdminService.collectionAdminViews$(orgId, userId)), + ), + ), shareReplay({ refCount: false, bufferSize: 1 }), ); diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index ca7d07220b..9b9be4e50b 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts @@ -28,6 +28,7 @@ import { } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; 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 { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -156,7 +157,11 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); - private orgCollections$ = from(this.collectionAdminService.getAll(this.organizationId)).pipe( + private orgCollections$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.collectionAdminService.collectionAdminViews$(this.organizationId, userId), + ), shareReplay({ refCount: true, bufferSize: 1 }), ); diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index c6a60165fe..b951f73d95 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -32,6 +32,7 @@ import { import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api"; 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 { ProductTierType } from "@bitwarden/common/billing/enums"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -276,9 +277,16 @@ export class MemberDialogComponent implements OnDestroy { ), ); + const collections = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.collectionAdminService.collectionAdminViews$(this.params.organizationId, userId), + ), + ); + combineLatest({ organization: this.organization$, - collections: this.collectionAdminService.getAll(this.params.organizationId), + collections, userDetails: userDetails$, groups: groups$, }) 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 2aac4d0a5c..c34b42a61b 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 @@ -200,7 +200,14 @@ export class MembersComponent extends BaseMembersComponent this.organization.canManageUsersPassword && !this.organization.hasPublicAndPrivateKeys ) { - const orgShareKey = await this.keyService.getOrgKey(this.organization.id); + const orgShareKey = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + map((orgKeys) => orgKeys[this.organization.id] ?? null), + ), + ); + const orgKeys = await this.keyService.makeKeyPair(orgShareKey); const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); const response = await this.organizationApiService.updateKeys( @@ -353,7 +360,13 @@ export class MembersComponent extends BaseMembersComponent this.organizationUserService.confirmUser(this.organization, user, publicKey), ); } else { - const orgKey = await this.keyService.getOrgKey(this.organization.id); + const orgKey = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + map((orgKeys) => orgKeys[this.organization.id] ?? null), + ), + ); const key = await this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey); const request = new OrganizationUserConfirmRequest(); request.key = key.encryptedString; diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts index db94eb2535..afc16e7237 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts @@ -17,8 +17,9 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { EncryptionType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; -import { UserId } from "@bitwarden/common/types/guid"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { UserKey, OrgKey, MasterKey } from "@bitwarden/common/types/key"; import { KdfType, KeyService } from "@bitwarden/key-management"; @@ -36,6 +37,8 @@ describe("OrganizationUserResetPasswordService", () => { let organizationUserApiService: MockProxy; let organizationApiService: MockProxy; let i18nService: MockProxy; + const mockUserId = Utils.newGuid() as UserId; + let accountService: FakeAccountService; beforeAll(() => { keyService = mock(); @@ -44,6 +47,7 @@ describe("OrganizationUserResetPasswordService", () => { organizationUserApiService = mock(); organizationApiService = mock(); i18nService = mock(); + accountService = mockAccountServiceWith(mockUserId); sut = new OrganizationUserResetPasswordService( keyService, @@ -52,6 +56,7 @@ describe("OrganizationUserResetPasswordService", () => { organizationUserApiService, organizationApiService, i18nService, + accountService, ); }); @@ -142,7 +147,10 @@ describe("OrganizationUserResetPasswordService", () => { const mockRandomBytes = new Uint8Array(64) as CsprngArray; const mockOrgKey = new SymmetricCryptoKey(mockRandomBytes) as OrgKey; - keyService.getOrgKey.mockResolvedValue(mockOrgKey); + keyService.orgKeys$.mockReturnValue( + of({ [mockOrgId]: mockOrgKey } as Record), + ); + encryptService.decryptToBytes.mockResolvedValue(mockRandomBytes); encryptService.rsaDecrypt.mockResolvedValue(mockRandomBytes); @@ -170,7 +178,7 @@ describe("OrganizationUserResetPasswordService", () => { }); it("should throw an error if the org key is null", async () => { - keyService.getOrgKey.mockResolvedValue(null); + keyService.orgKeys$.mockReturnValue(of(null)); await expect( sut.resetMasterPassword(mockNewMP, mockEmail, mockOrgUserId, mockOrgId), ).rejects.toThrow(); diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts index a1727a8cc5..df5e7e8a25 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Injectable } from "@angular/core"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map, switchMap } from "rxjs"; import { OrganizationUserApiService, @@ -10,6 +10,8 @@ import { } from "@bitwarden/admin-console/common"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +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 { EncryptedString, @@ -47,6 +49,7 @@ export class OrganizationUserResetPasswordService private organizationUserApiService: OrganizationUserApiService, private organizationApiService: OrganizationApiServiceAbstraction, private i18nService: I18nService, + private accountService: AccountService, ) {} /** @@ -111,7 +114,14 @@ export class OrganizationUserResetPasswordService } // Decrypt Organization's encrypted Private Key with org key - const orgSymKey = await this.keyService.getOrgKey(orgId); + const orgSymKey = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + map((orgKeys) => orgKeys[orgId as OrganizationId] ?? null), + ), + ); + if (orgSymKey == null) { throw new Error("No org key found"); } diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts index ecb2dbc54a..21424e8652 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.ts @@ -8,6 +8,7 @@ import { firstValueFrom, from, lastValueFrom, + map, of, Subject, switchMap, @@ -28,6 +29,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; @@ -179,7 +181,13 @@ export class AccountComponent implements OnInit, OnDestroy { // Backfill pub/priv key if necessary if (!this.org.hasPublicAndPrivateKeys) { - const orgShareKey = await this.keyService.getOrgKey(this.organizationId); + const orgShareKey = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + map((orgKeys) => orgKeys[this.organizationId as OrganizationId] ?? null), + ), + ); const orgKeys = await this.keyService.makeKeyPair(orgShareKey); request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); } diff --git a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts index a0964a90fc..003de9a54e 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts @@ -240,9 +240,15 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { return this.groupService.getAll(orgId); }), ); + + const collections = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.collectionAdminService.collectionAdminViews$(orgId, userId)), + ); + combineLatest({ organization: organization$, - collections: this.collectionAdminService.getAll(orgId), + collections, groups: groups$, users: this.organizationUserApiService.getAllMiniUserDetails(orgId), }) diff --git a/apps/web/src/app/tools/import/import-collection-admin.service.ts b/apps/web/src/app/tools/import/import-collection-admin.service.ts index 64050eb9c0..b63cd15047 100644 --- a/apps/web/src/app/tools/import/import-collection-admin.service.ts +++ b/apps/web/src/app/tools/import/import-collection-admin.service.ts @@ -1,13 +1,20 @@ import { Injectable } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { CollectionAdminService, CollectionAdminView } from "@bitwarden/admin-console/common"; import { ImportCollectionServiceAbstraction } from "@bitwarden/importer-core"; +import { UserId } from "@bitwarden/user-core"; @Injectable() export class ImportCollectionAdminService implements ImportCollectionServiceAbstraction { constructor(private collectionAdminService: CollectionAdminService) {} - async getAllAdminCollections(organizationId: string): Promise { - return await this.collectionAdminService.getAll(organizationId); + async getAllAdminCollections( + organizationId: string, + userId: UserId, + ): Promise { + return await firstValueFrom( + this.collectionAdminService.collectionAdminViews$(organizationId, userId), + ); } } diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts index 13b2ce2939..3dbf7d6c71 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from "@angular/common"; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from "@angular/core"; import { Router } from "@angular/router"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, switchMap } from "rxjs"; import { Unassigned, @@ -10,6 +10,8 @@ import { } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; 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 { ProductTierType } from "@bitwarden/common/billing/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -93,6 +95,7 @@ export class VaultHeaderComponent { private dialogService: DialogService, private router: Router, private configService: ConfigService, + private accountService: AccountService, ) {} /** @@ -225,7 +228,14 @@ export class VaultHeaderComponent { ); if (this.organizations?.length == 1 && !!organization) { - const collections = await this.collectionAdminService.getAll(organization.id); + const collections = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.collectionAdminService.collectionAdminViews$(organization.id, userId), + ), + ), + ); if (collections.length === organization.maxCollections) { await this.showFreeOrgUpgradeDialog(organization); return; diff --git a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts index 11a984c4d5..c6a7c9c830 100644 --- a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts +++ b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from "@angular/core/testing"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; import { CollectionAdminService, CollectionAdminView } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -71,7 +71,7 @@ describe("AdminConsoleCipherFormConfigService", () => { { provide: OrganizationService, useValue: { organizations$: () => orgs$ } }, { provide: CollectionAdminService, - useValue: { getAll: () => Promise.resolve([collection, collection2]) }, + useValue: { collectionAdminViews$: () => of([collection, collection2]) }, }, { provide: PolicyService, diff --git a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts index 5d23f89c82..939729568e 100644 --- a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts +++ b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts @@ -31,8 +31,9 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ private apiService: ApiService = inject(ApiService); private accountService: AccountService = inject(AccountService); - private organizationDataOwnershipDisabled$ = this.accountService.activeAccount$.pipe( - getUserId, + private userId$ = this.accountService.activeAccount$.pipe(getUserId); + + private organizationDataOwnershipDisabled$ = this.userId$.pipe( switchMap((userId) => this.policyService.policyAppliesToUser$(PolicyType.OrganizationDataOwnership, userId), ), @@ -44,9 +45,9 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ filter((filter) => filter !== undefined), ); - private allOrganizations$ = this.accountService.activeAccount$.pipe( - switchMap((account) => - this.organizationService.organizations$(account?.id).pipe( + private allOrganizations$ = this.userId$.pipe( + switchMap((userId) => + this.organizationService.organizations$(userId).pipe( map((orgs) => { return orgs.filter( (o) => o.isMember && o.enabled && o.status === OrganizationUserStatusType.Confirmed, @@ -60,8 +61,8 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ map(([orgs, orgId]) => orgs.find((o) => o.id === orgId)), ); - private allCollections$ = this.organization$.pipe( - switchMap(async (org) => await this.collectionAdminService.getAll(org.id)), + private allCollections$ = combineLatest([this.organization$, this.userId$]).pipe( + switchMap(([org, userId]) => this.collectionAdminService.collectionAdminViews$(org.id, userId)), ); async buildConfig( diff --git a/bitwarden_license/bit-cli/src/service-container.ts b/bitwarden_license/bit-cli/src/service-container.ts index 9e659c1a59..f3c2b75a7d 100644 --- a/bitwarden_license/bit-cli/src/service-container.ts +++ b/bitwarden_license/bit-cli/src/service-container.ts @@ -22,6 +22,7 @@ export class ServiceContainer extends OssServiceContainer { this.keyService, this.encryptService, this.organizationUserApiService, + this.accountService, ); } } 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 9efac2a3b9..25d12672dd 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 @@ -1,4 +1,5 @@ import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { OrganizationUserApiService, @@ -8,30 +9,42 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { newGuid } from "@bitwarden/guid"; import { KeyService } from "@bitwarden/key-management"; +import { UserId } from "@bitwarden/user-core"; 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"; +import { + FakeAccountService, + mockAccountServiceWith, +} from "@bitwarden/common/../spec/fake-account-service"; + describe("OrganizationAuthRequestService", () => { let organizationAuthRequestApiService: MockProxy; let keyService: MockProxy; let encryptService: MockProxy; let organizationUserApiService: MockProxy; let organizationAuthRequestService: OrganizationAuthRequestService; + const mockUserId = newGuid() as UserId; + let accountService: FakeAccountService; beforeEach(() => { organizationAuthRequestApiService = mock(); keyService = mock(); encryptService = mock(); organizationUserApiService = mock(); + accountService = mockAccountServiceWith(mockUserId); + organizationAuthRequestService = new OrganizationAuthRequestService( organizationAuthRequestApiService, keyService, encryptService, organizationUserApiService, + accountService, ); }); @@ -162,6 +175,7 @@ describe("OrganizationAuthRequestService", () => { describe("approvePendingRequests", () => { it("should approve the specified pending auth requests", async () => { jest.spyOn(organizationAuthRequestApiService, "bulkUpdatePendingRequests"); + jest.spyOn(keyService, "orgKeys$").mockReturnValue(of({ key: "fake-key" })); const organizationId = "organizationId"; @@ -213,6 +227,7 @@ describe("OrganizationAuthRequestService", () => { describe("approvePendingRequest", () => { it("should approve the specified pending auth request", async () => { jest.spyOn(organizationAuthRequestApiService, "approvePendingRequest"); + jest.spyOn(keyService, "orgKeys$").mockReturnValue(of({ key: "fake-key" })); const organizationId = "organizationId"; 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 f7b87d8c1a..c0ef0849bc 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 @@ -1,12 +1,17 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { firstValueFrom, map, switchMap } from "rxjs"; + import { OrganizationUserApiService, OrganizationUserResetPasswordDetailsResponse, } from "@bitwarden/admin-console/common"; +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 { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { KeyService } from "@bitwarden/key-management"; import { OrganizationAuthRequestApiService } from "./organization-auth-request-api.service"; @@ -20,6 +25,7 @@ export class OrganizationAuthRequestService { private keyService: KeyService, private encryptService: EncryptService, private organizationUserApiService: OrganizationUserApiService, + private accountService: AccountService, ) {} async listPendingRequests(organizationId: string): Promise { @@ -122,7 +128,13 @@ export class OrganizationAuthRequestService { const devicePubKey = Utils.fromB64ToArray(devicePublicKey); // Decrypt Organization's encrypted Private Key with org key - const orgSymKey = await this.keyService.getOrgKey(organizationId); + const orgSymKey = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + map((orgKeys) => orgKeys[organizationId as OrganizationId] ?? null), + ), + ); const decOrgPrivateKey = await this.encryptService.decryptBytes( new EncString(encryptedOrgPrivateKey), orgSymKey, diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts index 08c7f18130..3f698421e2 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts @@ -11,6 +11,7 @@ import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-cons import { PendingAuthRequestWithFingerprintView } from "@bitwarden/bit-common/admin-console/auth-requests/pending-auth-request-with-fingerprint.view"; import { PendingAuthRequestView } from "@bitwarden/bit-common/admin-console/auth-requests/pending-auth-request.view"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -38,6 +39,7 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared/shared.module"; KeyService, EncryptService, OrganizationUserApiService, + AccountService, ], }), ] satisfies SafeProvider[], diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts index 418b7020ff..0f3f6f412a 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts @@ -8,6 +8,8 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { ProviderAddOrganizationRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-add-organization.request"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { PlanType } from "@bitwarden/common/billing/enums"; import { CreateClientOrganizationRequest } from "@bitwarden/common/billing/models/request/create-client-organization.request"; @@ -30,10 +32,17 @@ export class WebProviderService { private billingApiService: BillingApiServiceAbstraction, private stateProvider: StateProvider, private providerApiService: ProviderApiServiceAbstraction, + private accountService: AccountService, ) {} async addOrganizationToProvider(providerId: string, organizationId: string) { - const orgKey = await this.keyService.getOrgKey(organizationId); + const orgKey = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + map((orgKeys) => orgKeys[organizationId as OrganizationId] ?? null), + ), + ); const providerKey = await this.keyService.getProviderKey(providerId); const encryptedOrgKey = await this.encryptService.wrapSymmetricKey(orgKey, providerKey); diff --git a/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts b/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts index 61faabb16b..b322825e5e 100644 --- a/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts +++ b/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts @@ -1,14 +1,15 @@ +import { Observable } from "rxjs"; + import { CollectionDetailsResponse } from "@bitwarden/admin-console/common"; import { UserId } from "@bitwarden/common/types/guid"; import { CollectionAccessSelectionView, CollectionAdminView } from "../models"; export abstract class CollectionAdminService { - abstract getAll(organizationId: string): Promise; - abstract get( + abstract collectionAdminViews$( organizationId: string, - collectionId: string, - ): Promise; + userId: UserId, + ): Observable; abstract save( collection: CollectionAdminView, userId: UserId, diff --git a/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts b/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts index a00be4e517..10ca6eb153 100644 --- a/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts +++ b/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts @@ -1,11 +1,14 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { combineLatest, firstValueFrom, from, map, Observable, of, switchMap } from "rxjs"; + import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { CollectionId, UserId } from "@bitwarden/common/types/guid"; +import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { OrgKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; import { CollectionAdminService, CollectionService } from "../abstractions"; @@ -28,37 +31,23 @@ export class DefaultCollectionAdminService implements CollectionAdminService { private collectionService: CollectionService, ) {} - async getAll(organizationId: string): Promise { - const collectionResponse = - await this.apiService.getManyCollectionsWithAccessDetails(organizationId); + collectionAdminViews$(organizationId: string, userId: UserId): Observable { + return combineLatest([ + this.keyService.orgKeys$(userId), + from(this.apiService.getManyCollectionsWithAccessDetails(organizationId)), + ]).pipe( + switchMap(([orgKey, res]) => { + if (res?.data == null || res.data.length === 0) { + return of([]); + } - if (collectionResponse?.data == null || collectionResponse.data.length === 0) { - return []; - } - - return await this.decryptMany(organizationId, collectionResponse.data); - } - - async get( - organizationId: string, - collectionId: string, - ): Promise { - const collectionResponse = await this.apiService.getCollectionAccessDetails( - organizationId, - collectionId, + return this.decryptMany(organizationId, res.data, orgKey); + }), ); - - if (collectionResponse == null) { - return undefined; - } - - const [view] = await this.decryptMany(organizationId, [collectionResponse]); - - return view; } async save(collection: CollectionAdminView, userId: UserId): Promise { - const request = await this.encrypt(collection); + const request = await this.encrypt(collection, userId); let response: CollectionDetailsResponse; if (collection.id == null) { @@ -112,13 +101,15 @@ export class DefaultCollectionAdminService implements CollectionAdminService { private async decryptMany( organizationId: string, collections: CollectionResponse[] | CollectionAccessDetailsResponse[], + orgKeys: Record, ): Promise { - const orgKey = await this.keyService.getOrgKey(organizationId); - const promises = collections.map(async (c) => { const view = new CollectionAdminView(); view.id = c.id; - view.name = await this.encryptService.decryptString(new EncString(c.name), orgKey); + view.name = await this.encryptService.decryptString( + new EncString(c.name), + orgKeys[organizationId as OrganizationId], + ); view.externalId = c.externalId; view.organizationId = c.organizationId; @@ -138,11 +129,15 @@ export class DefaultCollectionAdminService implements CollectionAdminService { return await Promise.all(promises); } - private async encrypt(model: CollectionAdminView): Promise { + private async encrypt(model: CollectionAdminView, userId: UserId): Promise { if (model.organizationId == null) { throw new Error("Collection has no organization id."); } - const key = await this.keyService.getOrgKey(model.organizationId); + const key = await firstValueFrom( + this.keyService + .orgKeys$(userId) + .pipe(map((orgKeys) => orgKeys[model.organizationId] ?? null)), + ); if (key == null) { throw new Error("No key for this collection's organization."); } diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index 985398599b..646db8d643 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -279,7 +279,7 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { this.collections$ = Utils.asyncToObservable(() => this.importCollectionService - .getAllAdminCollections(this.organizationId) + .getAllAdminCollections(this.organizationId, userId) .then((collections) => collections.sort(Utils.getSortFunction(this.i18nService, "name"))), ); diff --git a/libs/importer/src/services/import-collection.service.abstraction.ts b/libs/importer/src/services/import-collection.service.abstraction.ts index 48e6e7a4a8..f74d556b89 100644 --- a/libs/importer/src/services/import-collection.service.abstraction.ts +++ b/libs/importer/src/services/import-collection.service.abstraction.ts @@ -3,7 +3,8 @@ // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { CollectionView } from "@bitwarden/admin-console/common"; +import { UserId } from "@bitwarden/user-core"; export abstract class ImportCollectionServiceAbstraction { - getAllAdminCollections: (organizationId: string) => Promise; + getAllAdminCollections: (organizationId: string, userId: UserId) => Promise; }