1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +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

@@ -1,3 +1,5 @@
import { UserId } from "@bitwarden/user-core";
import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
import { InitiationPath } from "../../models/request/reference-event.request";
import { PaymentMethodType, PlanType } from "../enums";
@@ -47,16 +49,22 @@ export abstract class OrganizationBillingServiceAbstraction {
abstract purchaseSubscription(
subscription: SubscriptionInformation,
activeUserId: UserId,
): Promise<OrganizationResponse>;
abstract purchaseSubscriptionNoPaymentMethod(
subscription: SubscriptionInformation,
activeUserId: UserId,
): Promise<OrganizationResponse>;
abstract startFree(subscription: SubscriptionInformation): Promise<OrganizationResponse>;
abstract startFree(
subscription: SubscriptionInformation,
activeUserId: UserId,
): Promise<OrganizationResponse>;
abstract restartSubscription(
organizationId: string,
subscription: SubscriptionInformation,
activeUserId: UserId,
): Promise<void>;
}

View File

@@ -4,6 +4,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction as OrganizationApiService } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import {
BillingApiServiceAbstraction,
PaymentInformation,
SubscriptionInformation,
} from "@bitwarden/common/billing/abstractions";
import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums";
@@ -11,12 +12,16 @@ import { OrganizationBillingService } from "@bitwarden/common/billing/services/o
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { newGuid } from "@bitwarden/guid";
// 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 { KeyService } from "@bitwarden/key-management";
import { UserId } from "@bitwarden/user-core";
import { OrganizationKeysRequest } from "../../admin-console/models/request/organization-keys.request";
import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
import { EncString } from "../../key-management/crypto/models/enc-string";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { OrgKey } from "../../types/key";
import { PaymentMethodResponse } from "../models/response/payment-method.response";
@@ -31,6 +36,8 @@ describe("OrganizationBillingService", () => {
let sut: OrganizationBillingService;
const mockUserId = newGuid() as UserId;
beforeEach(() => {
apiService = mock<ApiService>();
billingApiService = mock<BillingApiServiceAbstraction>();
@@ -115,12 +122,12 @@ describe("OrganizationBillingService", () => {
} as OrganizationResponse;
organizationApiService.create.mockResolvedValue(organizationResponse);
keyService.makeOrgKey.mockResolvedValue([new EncString("encrypyted-key"), {} as OrgKey]);
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypyted-key")]);
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypyted"));
keyService.makeOrgKey.mockResolvedValue([new EncString("encrypted-key"), {} as OrgKey]);
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]);
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypted"));
//Act
const response = await sut.purchaseSubscription(subscriptionInformation);
const response = await sut.purchaseSubscription(subscriptionInformation, mockUserId);
//Assert
expect(organizationApiService.create).toHaveBeenCalledTimes(1);
@@ -141,10 +148,10 @@ describe("OrganizationBillingService", () => {
organizationApiService.create.mockRejectedValue(new Error("Failed to create organization"));
keyService.makeOrgKey.mockResolvedValue([new EncString("encrypted-key"), {} as OrgKey]);
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]);
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypyted"));
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypted"));
// Act & Assert
await expect(sut.purchaseSubscription(subscriptionInformation)).rejects.toThrow(
await expect(sut.purchaseSubscription(subscriptionInformation, mockUserId)).rejects.toThrow(
"Failed to create organization",
);
});
@@ -163,7 +170,7 @@ describe("OrganizationBillingService", () => {
keyService.makeOrgKey.mockRejectedValue(new Error("Key generation failed"));
// Act & Assert
await expect(sut.purchaseSubscription(subscriptionInformation)).rejects.toThrow(
await expect(sut.purchaseSubscription(subscriptionInformation, mockUserId)).rejects.toThrow(
"Key generation failed",
);
});
@@ -180,7 +187,7 @@ describe("OrganizationBillingService", () => {
} as SubscriptionInformation;
// Act & Assert
await expect(sut.purchaseSubscription(subscriptionInformation)).rejects.toThrow();
await expect(sut.purchaseSubscription(subscriptionInformation, mockUserId)).rejects.toThrow();
});
});
@@ -204,7 +211,10 @@ describe("OrganizationBillingService", () => {
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypted"));
//Act
const response = await sut.purchaseSubscriptionNoPaymentMethod(subscriptionInformation);
const response = await sut.purchaseSubscriptionNoPaymentMethod(
subscriptionInformation,
mockUserId,
);
//Assert
expect(organizationApiService.createWithoutPayment).toHaveBeenCalledTimes(1);
@@ -223,7 +233,7 @@ describe("OrganizationBillingService", () => {
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypted"));
await expect(
sut.purchaseSubscriptionNoPaymentMethod(subscriptionInformation),
sut.purchaseSubscriptionNoPaymentMethod(subscriptionInformation, mockUserId),
).rejects.toThrow("Creation failed");
});
@@ -237,7 +247,7 @@ describe("OrganizationBillingService", () => {
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]);
await expect(
sut.purchaseSubscriptionNoPaymentMethod(subscriptionInformation),
sut.purchaseSubscriptionNoPaymentMethod(subscriptionInformation, mockUserId),
).rejects.toThrow("Key generation failed");
});
});
@@ -256,12 +266,12 @@ describe("OrganizationBillingService", () => {
} as OrganizationResponse;
organizationApiService.create.mockResolvedValue(organizationResponse);
keyService.makeOrgKey.mockResolvedValue([new EncString("encrypyted-key"), {} as OrgKey]);
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypyted-key")]);
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypyted"));
keyService.makeOrgKey.mockResolvedValue([new EncString("encrypted-key"), {} as OrgKey]);
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]);
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypted"));
//Act
const response = await sut.startFree(subscriptionInformation);
const response = await sut.startFree(subscriptionInformation, mockUserId);
//Assert
expect(organizationApiService.create).toHaveBeenCalledTimes(1);
@@ -277,7 +287,9 @@ describe("OrganizationBillingService", () => {
keyService.makeOrgKey.mockRejectedValue(new Error("Key generation failed"));
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]);
await expect(sut.startFree(subscriptionInformation)).rejects.toThrow("Key generation failed");
await expect(sut.startFree(subscriptionInformation, mockUserId)).rejects.toThrow(
"Key generation failed",
);
});
it("given organization creation fails, then it throws an error", async () => {
@@ -290,11 +302,162 @@ describe("OrganizationBillingService", () => {
organizationApiService.create.mockRejectedValue(new Error("Failed to create organization"));
keyService.makeOrgKey.mockResolvedValue([new EncString("encrypted-key"), {} as OrgKey]);
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]);
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypyted"));
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypted"));
// Act & Assert
await expect(sut.startFree(subscriptionInformation)).rejects.toThrow(
await expect(sut.startFree(subscriptionInformation, mockUserId)).rejects.toThrow(
"Failed to create organization",
);
});
});
describe("organization key creation methods", () => {
const organizationKeys = {
orgKey: new SymmetricCryptoKey(new Uint8Array(64)) as OrgKey,
publicKeyEncapsulatedOrgKey: new EncString("encryptedOrgKey"),
publicKey: "public-key",
encryptedPrivateKey: new EncString("encryptedPrivateKey"),
};
const encryptedCollectionName = new EncString("encryptedCollectionName");
const mockSubscription = {
organization: {
name: "Test Org",
businessName: "Test Business",
billingEmail: "test@example.com",
initiationPath: "Registration form",
},
plan: {
type: 0, // Free plan
passwordManagerSeats: 0,
subscribeToSecretsManager: false,
isFromSecretsManagerTrial: false,
},
} as SubscriptionInformation;
const mockResponse = { id: "org-id" } as OrganizationResponse;
const expectedRequestObject = {
name: "Test Org",
businessName: "Test Business",
billingEmail: "test@example.com",
initiationPath: "Registration form",
planType: 0,
key: organizationKeys.publicKeyEncapsulatedOrgKey.encryptedString,
keys: new OrganizationKeysRequest(
organizationKeys.publicKey,
organizationKeys.encryptedPrivateKey.encryptedString!,
),
collectionName: encryptedCollectionName.encryptedString,
};
beforeEach(() => {
keyService.makeOrgKey.mockResolvedValue([
organizationKeys.publicKeyEncapsulatedOrgKey,
organizationKeys.orgKey,
]);
keyService.makeKeyPair.mockResolvedValue([
organizationKeys.publicKey,
organizationKeys.encryptedPrivateKey,
]);
encryptService.encryptString.mockResolvedValueOnce(encryptedCollectionName);
i18nService.t.mockReturnValue("Default Collection");
organizationApiService.create.mockResolvedValue(mockResponse);
});
describe("purchaseSubscription", () => {
it("sets the correct organization keys on the organization creation request", async () => {
const subscriptionWithPayment = {
...mockSubscription,
payment: {
paymentMethod: ["test-token", PaymentMethodType.Card],
billing: {
postalCode: "12345",
country: "US",
},
} as PaymentInformation,
} as SubscriptionInformation;
const result = await sut.purchaseSubscription(subscriptionWithPayment, mockUserId);
expect(keyService.makeOrgKey).toHaveBeenCalledWith(mockUserId);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(organizationKeys.orgKey);
expect(encryptService.encryptString).toHaveBeenCalledWith(
"Default Collection",
organizationKeys.orgKey,
);
expect(organizationApiService.create).toHaveBeenCalledWith(
expect.objectContaining(expectedRequestObject),
);
expect(apiService.refreshIdentityToken).toHaveBeenCalled();
expect(syncService.fullSync).toHaveBeenCalledWith(true);
expect(result).toBe(mockResponse);
});
});
describe("purchaseSubscriptionNoPaymentMethod", () => {
it("sets the correct organization keys on the organization creation request", async () => {
organizationApiService.createWithoutPayment.mockResolvedValue(mockResponse);
const result = await sut.purchaseSubscriptionNoPaymentMethod(mockSubscription, mockUserId);
expect(keyService.makeOrgKey).toHaveBeenCalledWith(mockUserId);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(organizationKeys.orgKey);
expect(encryptService.encryptString).toHaveBeenCalledWith(
"Default Collection",
organizationKeys.orgKey,
);
expect(organizationApiService.createWithoutPayment).toHaveBeenCalledWith(
expect.objectContaining(expectedRequestObject),
);
expect(apiService.refreshIdentityToken).toHaveBeenCalled();
expect(syncService.fullSync).toHaveBeenCalledWith(true);
expect(result).toBe(mockResponse);
});
});
describe("startFree", () => {
it("sets the correct organization keys on the organization creation request", async () => {
const result = await sut.startFree(mockSubscription, mockUserId);
expect(keyService.makeOrgKey).toHaveBeenCalledWith(mockUserId);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(organizationKeys.orgKey);
expect(encryptService.encryptString).toHaveBeenCalledWith(
"Default Collection",
organizationKeys.orgKey,
);
expect(organizationApiService.create).toHaveBeenCalledWith(
expect.objectContaining(expectedRequestObject),
);
expect(apiService.refreshIdentityToken).toHaveBeenCalled();
expect(syncService.fullSync).toHaveBeenCalledWith(true);
expect(result).toBe(mockResponse);
});
});
describe("restartSubscription", () => {
it("sets the correct organization keys on the organization creation request", async () => {
const subscriptionWithPayment = {
...mockSubscription,
payment: {
paymentMethod: ["test-token", PaymentMethodType.Card],
billing: {
postalCode: "12345",
country: "US",
},
} as PaymentInformation,
} as SubscriptionInformation;
await sut.restartSubscription("org-id", subscriptionWithPayment, mockUserId);
expect(keyService.makeOrgKey).toHaveBeenCalledWith(mockUserId);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(organizationKeys.orgKey);
expect(encryptService.encryptString).toHaveBeenCalledWith(
"Default Collection",
organizationKeys.orgKey,
);
expect(billingApiService.restartSubscription).toHaveBeenCalledWith(
"org-id",
expect.objectContaining(expectedRequestObject),
);
});
});
});
});

View File

@@ -3,6 +3,7 @@
// 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 { KeyService } from "@bitwarden/key-management";
import { UserId } from "@bitwarden/user-core";
import { ApiService } from "../../abstractions/api.service";
import { OrganizationApiServiceAbstraction as OrganizationApiService } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
@@ -49,10 +50,13 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
return paymentMethod?.paymentSource;
}
async purchaseSubscription(subscription: SubscriptionInformation): Promise<OrganizationResponse> {
async purchaseSubscription(
subscription: SubscriptionInformation,
activeUserId: UserId,
): Promise<OrganizationResponse> {
const request = new OrganizationCreateRequest();
const organizationKeys = await this.makeOrganizationKeys();
const organizationKeys = await this.makeOrganizationKeys(activeUserId);
this.setOrganizationKeys(request, organizationKeys);
@@ -73,10 +77,11 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
async purchaseSubscriptionNoPaymentMethod(
subscription: SubscriptionInformation,
activeUserId: UserId,
): Promise<OrganizationResponse> {
const request = new OrganizationNoPaymentMethodCreateRequest();
const organizationKeys = await this.makeOrganizationKeys();
const organizationKeys = await this.makeOrganizationKeys(activeUserId);
this.setOrganizationKeys(request, organizationKeys);
@@ -93,10 +98,13 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
return response;
}
async startFree(subscription: SubscriptionInformation): Promise<OrganizationResponse> {
async startFree(
subscription: SubscriptionInformation,
activeUserId: UserId,
): Promise<OrganizationResponse> {
const request = new OrganizationCreateRequest();
const organizationKeys = await this.makeOrganizationKeys();
const organizationKeys = await this.makeOrganizationKeys(activeUserId);
this.setOrganizationKeys(request, organizationKeys);
@@ -113,8 +121,8 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
return response;
}
private async makeOrganizationKeys(): Promise<OrganizationKeys> {
const [encryptedKey, key] = await this.keyService.makeOrgKey<OrgKey>();
private async makeOrganizationKeys(activeUserId: UserId): Promise<OrganizationKeys> {
const [encryptedKey, key] = await this.keyService.makeOrgKey<OrgKey>(activeUserId);
const [publicKey, encryptedPrivateKey] = await this.keyService.makeKeyPair(key);
const encryptedCollectionName = await this.encryptService.encryptString(
this.i18nService.t("defaultCollection"),
@@ -214,9 +222,10 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
async restartSubscription(
organizationId: string,
subscription: SubscriptionInformation,
activeUserId: UserId,
): Promise<void> {
const request = new OrganizationCreateRequest();
const organizationKeys = await this.makeOrganizationKeys();
const organizationKeys = await this.makeOrganizationKeys(activeUserId);
this.setOrganizationKeys(request, organizationKeys);
this.setOrganizationInformation(request, subscription.organization);
this.setPlanInformation(request, subscription.plan);