1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 00:03:56 +00:00

[PM-23626] Require userId for makeOrgKey on the key service (#15864)

* Update key service

* Update consumers

* Add unit test coverage for consumer services

* Add unit test coverage for organization-billing service
This commit is contained in:
Thomas Avery
2025-09-05 09:51:01 -05:00
committed by GitHub
parent bb6fabd292
commit a6b7c7f75c
19 changed files with 520 additions and 98 deletions

View File

@@ -0,0 +1,119 @@
import { MockProxy, mock } from "jest-mock-extended";
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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
import { PlanType } from "@bitwarden/common/billing/enums";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
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";
import { OrgKey, ProviderKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { newGuid } from "@bitwarden/guid";
import { KeyService } from "@bitwarden/key-management";
import { UserId } from "@bitwarden/user-core";
import { WebProviderService } from "./web-provider.service";
describe("WebProviderService", () => {
let sut: WebProviderService;
let keyService: MockProxy<KeyService>;
let syncService: MockProxy<SyncService>;
let apiService: MockProxy<ApiService>;
let i18nService: MockProxy<I18nService>;
let encryptService: MockProxy<EncryptService>;
let billingApiService: MockProxy<BillingApiServiceAbstraction>;
let stateProvider: MockProxy<StateProvider>;
let providerApiService: MockProxy<ProviderApiServiceAbstraction>;
let accountService: MockProxy<AccountService>;
beforeEach(() => {
keyService = mock();
syncService = mock();
apiService = mock();
i18nService = mock();
encryptService = mock();
billingApiService = mock();
stateProvider = mock();
providerApiService = mock();
accountService = mock();
sut = new WebProviderService(
keyService,
syncService,
apiService,
i18nService,
encryptService,
billingApiService,
stateProvider,
providerApiService,
accountService,
);
});
describe("createClientOrganization", () => {
const activeUserId = newGuid() as UserId;
const providerId = "provider-123";
const name = "Test Org";
const ownerEmail = "owner@example.com";
const planType = PlanType.EnterpriseAnnually;
const seats = 10;
const publicKey = "public-key";
const encryptedPrivateKey = new EncString("encrypted-private-key");
const encryptedProviderKey = new EncString("encrypted-provider-key");
const encryptedCollectionName = new EncString("encrypted-collection-name");
const defaultCollectionTranslation = "Default Collection";
const mockOrgKey = new SymmetricCryptoKey(new Uint8Array(64)) as OrgKey;
const mockProviderKey = new SymmetricCryptoKey(new Uint8Array(64)) as ProviderKey;
beforeEach(() => {
keyService.makeOrgKey.mockResolvedValue([new EncString("mockEncryptedKey"), mockOrgKey]);
keyService.makeKeyPair.mockResolvedValue([publicKey, encryptedPrivateKey]);
i18nService.t.mockReturnValue(defaultCollectionTranslation);
encryptService.encryptString.mockResolvedValue(encryptedCollectionName);
keyService.getProviderKey.mockResolvedValue(mockProviderKey);
encryptService.wrapSymmetricKey.mockResolvedValue(encryptedProviderKey);
});
it("creates a client organization and calls all dependencies with correct arguments", async () => {
await sut.createClientOrganization(
providerId,
name,
ownerEmail,
planType,
seats,
activeUserId,
);
expect(keyService.makeOrgKey).toHaveBeenCalledWith(activeUserId);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(mockOrgKey);
expect(i18nService.t).toHaveBeenCalledWith("defaultCollection");
expect(encryptService.encryptString).toHaveBeenCalledWith(
defaultCollectionTranslation,
mockOrgKey,
);
expect(keyService.getProviderKey).toHaveBeenCalledWith(providerId);
expect(encryptService.wrapSymmetricKey).toHaveBeenCalledWith(mockOrgKey, mockProviderKey);
expect(billingApiService.createProviderClientOrganization).toHaveBeenCalledWith(
providerId,
expect.objectContaining({
name,
ownerEmail,
planType,
seats,
key: encryptedProviderKey.encryptedString,
keyPair: expect.any(OrganizationKeysRequest),
collectionName: encryptedCollectionName.encryptedString,
}),
);
expect(apiService.refreshIdentityToken).toHaveBeenCalled();
expect(syncService.fullSync).toHaveBeenCalledWith(true);
});
});
});

View File

@@ -16,7 +16,7 @@ import { CreateClientOrganizationRequest } from "@bitwarden/common/billing/model
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { OrgKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { KeyService } from "@bitwarden/key-management";
@@ -78,8 +78,9 @@ export class WebProviderService {
ownerEmail: string,
planType: PlanType,
seats: number,
activeUserId: UserId,
): Promise<void> {
const organizationKey = (await this.keyService.makeOrgKey<OrgKey>())[1];
const organizationKey = (await this.keyService.makeOrgKey<OrgKey>(activeUserId))[1];
const [publicKey, encryptedPrivateKey] = await this.keyService.makeKeyPair(organizationKey);

View File

@@ -3,12 +3,14 @@
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject, switchMap } from "rxjs";
import { firstValueFrom, Subject, switchMap } from "rxjs";
import { first, takeUntil } from "rxjs/operators";
import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components";
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -49,6 +51,7 @@ export class SetupComponent implements OnInit, OnDestroy {
private providerApiService: ProviderApiServiceAbstraction,
private formBuilder: FormBuilder,
private toastService: ToastService,
private accountService: AccountService,
) {}
ngOnInit() {
@@ -118,8 +121,8 @@ export class SetupComponent implements OnInit, OnDestroy {
if (!paymentValid || !taxInformationValid || !this.formGroup.valid) {
return;
}
const providerKey = await this.keyService.makeOrgKey<ProviderKey>();
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const providerKey = await this.keyService.makeOrgKey<ProviderKey>(activeUserId);
const key = providerKey[0].encryptedString;
const request = new ProviderSetupRequest();

View File

@@ -1,6 +1,9 @@
import { Component, Inject, OnInit } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { firstValueFrom } from "rxjs";
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, ProductTierType } from "@bitwarden/common/billing/enums";
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
@@ -130,6 +133,7 @@ export class CreateClientDialogComponent implements OnInit {
private i18nService: I18nService,
private toastService: ToastService,
private webProviderService: WebProviderService,
private accountService: AccountService,
) {}
async ngOnInit(): Promise<void> {
@@ -198,13 +202,14 @@ export class CreateClientDialogComponent implements OnInit {
if (!selectedPlanCard) {
return;
}
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
await this.webProviderService.createClientOrganization(
this.dialogParams.providerId,
this.formGroup.controls.organizationName.value,
this.formGroup.controls.clientOwnerEmail.value,
selectedPlanCard.type,
this.formGroup.controls.seats.value,
activeUserId,
);
this.toastService.showToast({

View File

@@ -80,8 +80,9 @@ export class SetupBusinessUnitComponent extends BaseAcceptComponent {
map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]),
);
const userId = await firstValueFrom(activeUserId$);
const [{ encryptedString: encryptedProviderKey }, providerKey] =
await this.keyService.makeOrgKey<ProviderKey>();
await this.keyService.makeOrgKey<ProviderKey>(userId);
const organizationKey = await firstValueFrom(organizationKey$);
@@ -92,8 +93,6 @@ export class SetupBusinessUnitComponent extends BaseAcceptComponent {
return await fail();
}
const userId = await firstValueFrom(activeUserId$);
const request = {
userId,
token,