1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +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

@@ -2,8 +2,11 @@
// @ts-strict-ignore // @ts-strict-ignore
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { ActivatedRoute, Params, Router } from "@angular/router"; import { ActivatedRoute, Params, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite"; import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite";
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service"; import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -29,13 +32,18 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent {
protected authService: AuthService, protected authService: AuthService,
private acceptOrganizationInviteService: AcceptOrganizationInviteService, private acceptOrganizationInviteService: AcceptOrganizationInviteService,
private organizationInviteService: OrganizationInviteService, private organizationInviteService: OrganizationInviteService,
private accountService: AccountService,
) { ) {
super(router, platformUtilsService, i18nService, route, authService); super(router, platformUtilsService, i18nService, route, authService);
} }
async authedHandler(qParams: Params): Promise<void> { async authedHandler(qParams: Params): Promise<void> {
const invite = this.fromParams(qParams); const invite = this.fromParams(qParams);
const success = await this.acceptOrganizationInviteService.validateAndAcceptInvite(invite); const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const success = await this.acceptOrganizationInviteService.validateAndAcceptInvite(
invite,
activeUserId,
);
if (!success) { if (!success) {
return; return;

View File

@@ -21,7 +21,9 @@ import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-st
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { OrgKey } from "@bitwarden/common/types/key"; import { OrgKey } from "@bitwarden/common/types/key";
import { newGuid } from "@bitwarden/guid";
import { KeyService } from "@bitwarden/key-management"; import { KeyService } from "@bitwarden/key-management";
import { UserId } from "@bitwarden/user-core";
import { I18nService } from "../../core/i18n.service"; import { I18nService } from "../../core/i18n.service";
@@ -73,10 +75,13 @@ describe("AcceptOrganizationInviteService", () => {
}); });
describe("validateAndAcceptInvite", () => { describe("validateAndAcceptInvite", () => {
const activeUserId = newGuid() as UserId;
it("initializes an organization when given an invite where initOrganization is true", async () => { it("initializes an organization when given an invite where initOrganization is true", async () => {
const mockOrgKey = "orgPrivateKey" as unknown as OrgKey;
keyService.makeOrgKey.mockResolvedValue([ keyService.makeOrgKey.mockResolvedValue([
{ encryptedString: "string" } as EncString, { encryptedString: "string" } as EncString,
"orgPrivateKey" as unknown as OrgKey, mockOrgKey,
]); ]);
keyService.makeKeyPair.mockResolvedValue([ keyService.makeKeyPair.mockResolvedValue([
"orgPublicKey", "orgPublicKey",
@@ -88,10 +93,12 @@ describe("AcceptOrganizationInviteService", () => {
encryptService.encryptString.mockResolvedValue({ encryptedString: "string" } as EncString); encryptService.encryptString.mockResolvedValue({ encryptedString: "string" } as EncString);
const invite = createOrgInvite({ initOrganization: true }); const invite = createOrgInvite({ initOrganization: true });
const result = await sut.validateAndAcceptInvite(invite); const result = await sut.validateAndAcceptInvite(invite, activeUserId);
expect(result).toBe(true); expect(result).toBe(true);
expect(organizationUserApiService.postOrganizationUserAcceptInit).toHaveBeenCalled(); expect(organizationUserApiService.postOrganizationUserAcceptInit).toHaveBeenCalled();
expect(keyService.makeOrgKey).toHaveBeenCalledWith(activeUserId);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(mockOrgKey);
expect(apiService.refreshIdentityToken).toHaveBeenCalled(); expect(apiService.refreshIdentityToken).toHaveBeenCalled();
expect(organizationUserApiService.postOrganizationUserAccept).not.toHaveBeenCalled(); expect(organizationUserApiService.postOrganizationUserAccept).not.toHaveBeenCalled();
expect(organizationInviteService.getOrganizationInvite).not.toHaveBeenCalled(); expect(organizationInviteService.getOrganizationInvite).not.toHaveBeenCalled();
@@ -109,7 +116,7 @@ describe("AcceptOrganizationInviteService", () => {
} as Policy, } as Policy,
]); ]);
const result = await sut.validateAndAcceptInvite(invite); const result = await sut.validateAndAcceptInvite(invite, activeUserId);
expect(result).toBe(false); expect(result).toBe(false);
expect(authService.logOut).toHaveBeenCalled(); expect(authService.logOut).toHaveBeenCalled();
@@ -130,7 +137,7 @@ describe("AcceptOrganizationInviteService", () => {
} as Policy, } as Policy,
]); ]);
const result = await sut.validateAndAcceptInvite(providedInvite); const result = await sut.validateAndAcceptInvite(providedInvite, activeUserId);
expect(result).toBe(false); expect(result).toBe(false);
expect(authService.logOut).toHaveBeenCalled(); expect(authService.logOut).toHaveBeenCalled();
@@ -145,7 +152,7 @@ describe("AcceptOrganizationInviteService", () => {
const invite = createOrgInvite(); const invite = createOrgInvite();
policyApiService.getPoliciesByToken.mockResolvedValue([]); policyApiService.getPoliciesByToken.mockResolvedValue([]);
const result = await sut.validateAndAcceptInvite(invite); const result = await sut.validateAndAcceptInvite(invite, activeUserId);
expect(result).toBe(true); expect(result).toBe(true);
expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled(); expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled();
@@ -175,7 +182,7 @@ describe("AcceptOrganizationInviteService", () => {
false, false,
]); ]);
const result = await sut.validateAndAcceptInvite(invite); const result = await sut.validateAndAcceptInvite(invite, activeUserId);
expect(result).toBe(true); expect(result).toBe(true);
expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled(); expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled();
@@ -214,7 +221,7 @@ describe("AcceptOrganizationInviteService", () => {
true, true,
]); ]);
const result = await sut.validateAndAcceptInvite(invite); const result = await sut.validateAndAcceptInvite(invite, activeUserId);
expect(result).toBe(true); expect(result).toBe(true);
expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith( expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(

View File

@@ -25,6 +25,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { OrgKey } from "@bitwarden/common/types/key"; import { OrgKey } from "@bitwarden/common/types/key";
import { KeyService } from "@bitwarden/key-management"; import { KeyService } from "@bitwarden/key-management";
import { UserId } from "@bitwarden/user-core";
@Injectable() @Injectable()
export class AcceptOrganizationInviteService { export class AcceptOrganizationInviteService {
@@ -54,16 +55,20 @@ export class AcceptOrganizationInviteService {
* Note: Users might need to pass a MP policy check before accepting an invite to an existing organization. If the user * Note: Users might need to pass a MP policy check before accepting an invite to an existing organization. If the user
* has not passed this check, they will be logged out and the invite will be stored for later use. * has not passed this check, they will be logged out and the invite will be stored for later use.
* @param invite an organization invite * @param invite an organization invite
* @param activeUserId the user ID of the active user accepting the invite
* @returns a promise that resolves a boolean indicating if the invite was accepted. * @returns a promise that resolves a boolean indicating if the invite was accepted.
*/ */
async validateAndAcceptInvite(invite: OrganizationInvite): Promise<boolean> { async validateAndAcceptInvite(
invite: OrganizationInvite,
activeUserId: UserId,
): Promise<boolean> {
if (invite == null) { if (invite == null) {
throw new Error("Invite cannot be null."); throw new Error("Invite cannot be null.");
} }
// Creation of a new org // Creation of a new org
if (invite.initOrganization) { if (invite.initOrganization) {
await this.acceptAndInitOrganization(invite); await this.acceptAndInitOrganization(invite, activeUserId);
return true; return true;
} }
@@ -81,8 +86,11 @@ export class AcceptOrganizationInviteService {
return true; return true;
} }
private async acceptAndInitOrganization(invite: OrganizationInvite): Promise<void> { private async acceptAndInitOrganization(
await this.prepareAcceptAndInitRequest(invite).then((request) => invite: OrganizationInvite,
activeUserId: UserId,
): Promise<void> {
await this.prepareAcceptAndInitRequest(invite, activeUserId).then((request) =>
this.organizationUserApiService.postOrganizationUserAcceptInit( this.organizationUserApiService.postOrganizationUserAcceptInit(
invite.organizationId, invite.organizationId,
invite.organizationUserId, invite.organizationUserId,
@@ -95,11 +103,12 @@ export class AcceptOrganizationInviteService {
private async prepareAcceptAndInitRequest( private async prepareAcceptAndInitRequest(
invite: OrganizationInvite, invite: OrganizationInvite,
activeUserId: UserId,
): Promise<OrganizationUserAcceptInitRequest> { ): Promise<OrganizationUserAcceptInitRequest> {
const request = new OrganizationUserAcceptInitRequest(); const request = new OrganizationUserAcceptInitRequest();
request.token = invite.token; request.token = invite.token;
const [encryptedOrgKey, orgKey] = await this.keyService.makeOrgKey<OrgKey>(); const [encryptedOrgKey, orgKey] = await this.keyService.makeOrgKey<OrgKey>(activeUserId);
const [orgPublicKey, encryptedOrgPrivateKey] = await this.keyService.makeKeyPair(orgKey); const [orgPublicKey, encryptedOrgPrivateKey] = await this.keyService.makeKeyPair(orgKey);
const collection = await this.encryptService.encryptString( const collection = await this.encryptService.encryptString(
this.i18nService.t("defaultCollection"), this.i18nService.t("defaultCollection"),

View File

@@ -10,10 +10,12 @@ import {
ViewChild, ViewChild,
} from "@angular/core"; } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms"; import { FormBuilder, Validators } from "@angular/forms";
import { from, Subject, switchMap, takeUntil } from "rxjs"; import { firstValueFrom, from, Subject, switchMap, takeUntil } from "rxjs";
import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 { import {
BillingInformation, BillingInformation,
OrganizationBillingServiceAbstraction as OrganizationBillingService, OrganizationBillingServiceAbstraction as OrganizationBillingService,
@@ -107,6 +109,7 @@ export class TrialBillingStepComponent implements OnInit, OnDestroy {
private organizationBillingService: OrganizationBillingService, private organizationBillingService: OrganizationBillingService,
private toastService: ToastService, private toastService: ToastService,
private taxService: TaxServiceAbstraction, private taxService: TaxServiceAbstraction,
private accountService: AccountService,
) {} ) {}
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
@@ -190,6 +193,7 @@ export class TrialBillingStepComponent implements OnInit, OnDestroy {
} }
private async createOrganization(): Promise<string> { private async createOrganization(): Promise<string> {
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const planResponse = this.findPlanFor(this.formGroup.value.cadence); const planResponse = this.findPlanFor(this.formGroup.value.cadence);
const { type, token } = await this.paymentComponent.tokenize(); const { type, token } = await this.paymentComponent.tokenize();
@@ -221,11 +225,14 @@ export class TrialBillingStepComponent implements OnInit, OnDestroy {
skipTrial: this.trialLength === 0, skipTrial: this.trialLength === 0,
}; };
const response = await this.organizationBillingService.purchaseSubscription({ const response = await this.organizationBillingService.purchaseSubscription(
organization, {
plan, organization,
payment, plan,
}); payment,
},
activeUserId,
);
return response.id; return response.id;
} }

View File

@@ -64,6 +64,7 @@ import {
ToastService, ToastService,
} from "@bitwarden/components"; } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management"; import { KeyService } from "@bitwarden/key-management";
import { UserId } from "@bitwarden/user-core";
import { BillingNotificationService } from "../services/billing-notification.service"; import { BillingNotificationService } from "../services/billing-notification.service";
import { BillingSharedModule } from "../shared/billing-shared.module"; import { BillingSharedModule } from "../shared/billing-shared.module";
@@ -769,6 +770,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
} }
const doSubmit = async (): Promise<string> => { const doSubmit = async (): Promise<string> => {
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
let orgId: string = null; let orgId: string = null;
const sub = this.sub?.subscription; const sub = this.sub?.subscription;
const isCanceled = sub?.status === "canceled"; const isCanceled = sub?.status === "canceled";
@@ -776,7 +778,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
sub?.cancelled && this.organization.productTierType === ProductTierType.Free; sub?.cancelled && this.organization.productTierType === ProductTierType.Free;
if (isCanceled || isCancelledDowngradedToFreeOrg) { if (isCanceled || isCancelledDowngradedToFreeOrg) {
await this.restartSubscription(); await this.restartSubscription(activeUserId);
orgId = this.organizationId; orgId = this.organizationId;
} else { } else {
orgId = await this.updateOrganization(); orgId = await this.updateOrganization();
@@ -816,7 +818,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
this.dialogRef.close(); this.dialogRef.close();
}; };
private async restartSubscription() { private async restartSubscription(activeUserId: UserId) {
const org = await this.organizationApiService.get(this.organizationId); const org = await this.organizationApiService.get(this.organizationId);
const organization: OrganizationInformation = { const organization: OrganizationInformation = {
name: org.name, name: org.name,
@@ -848,11 +850,15 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
billing: this.getBillingInformationFromTaxInfoComponent(), billing: this.getBillingInformationFromTaxInfoComponent(),
}; };
await this.organizationBillingService.restartSubscription(this.organization.id, { await this.organizationBillingService.restartSubscription(
organization, this.organization.id,
plan, {
payment, organization,
}); plan,
payment,
},
activeUserId,
);
} }
private async updateOrganization() { private async updateOrganization() {

View File

@@ -624,7 +624,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
const doSubmit = async (): Promise<string> => { const doSubmit = async (): Promise<string> => {
let orgId: string; let orgId: string;
if (this.createOrganization) { if (this.createOrganization) {
const orgKey = await this.keyService.makeOrgKey<OrgKey>(); const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const orgKey = await this.keyService.makeOrgKey<OrgKey>(activeUserId);
const key = orgKey[0].encryptedString; const key = orgKey[0].encryptedString;
const collection = await this.encryptService.encryptString( const collection = await this.encryptService.encryptString(
this.i18nService.t("defaultCollection"), this.i18nService.t("defaultCollection"),

View File

@@ -2,11 +2,14 @@
// @ts-strict-ignore // @ts-strict-ignore
import { Component, EventEmitter, Output } from "@angular/core"; import { Component, EventEmitter, Output } from "@angular/core";
import { FormBuilder } from "@angular/forms"; import { FormBuilder } from "@angular/forms";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -43,14 +46,15 @@ export class OrganizationSelfHostingLicenseUploaderComponent extends AbstractSel
private readonly keyService: KeyService, private readonly keyService: KeyService,
private readonly organizationApiService: OrganizationApiServiceAbstraction, private readonly organizationApiService: OrganizationApiServiceAbstraction,
private readonly syncService: SyncService, private readonly syncService: SyncService,
private readonly accountService: AccountService,
) { ) {
super(formBuilder, i18nService, platformUtilsService, toastService, tokenService); super(formBuilder, i18nService, platformUtilsService, toastService, tokenService);
} }
protected async submit(): Promise<void> { protected async submit(): Promise<void> {
await super.submit(); await super.submit();
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const orgKey = await this.keyService.makeOrgKey<OrgKey>(); const orgKey = await this.keyService.makeOrgKey<OrgKey>(activeUserId);
const key = orgKey[0].encryptedString; const key = orgKey[0].encryptedString;
const collection = await this.encryptService.encryptString( const collection = await this.encryptService.encryptString(
this.i18nService.t("defaultCollection"), this.i18nService.t("defaultCollection"),

View File

@@ -29,6 +29,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { ToastService } from "@bitwarden/components"; import { ToastService } from "@bitwarden/components";
import { UserId } from "@bitwarden/user-core";
import { import {
OrganizationCreatedEvent, OrganizationCreatedEvent,
@@ -227,13 +228,14 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
} }
async orgNameEntrySubmit(): Promise<void> { async orgNameEntrySubmit(): Promise<void> {
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const isTrialPaymentOptional = await firstValueFrom(this.trialPaymentOptional$); const isTrialPaymentOptional = await firstValueFrom(this.trialPaymentOptional$);
/** Only skip payment if the flag is on AND trialLength > 0 */ /** Only skip payment if the flag is on AND trialLength > 0 */
if (isTrialPaymentOptional && this.trialLength > 0) { if (isTrialPaymentOptional && this.trialLength > 0) {
await this.createOrganizationOnTrial(); await this.createOrganizationOnTrial(activeUserId);
} else { } else {
await this.conditionallyCreateOrganization(); await this.conditionallyCreateOrganization(activeUserId);
} }
} }
@@ -245,7 +247,7 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
} }
/** create an organization on trial without payment method */ /** create an organization on trial without payment method */
async createOrganizationOnTrial() { async createOrganizationOnTrial(activeUserId: UserId) {
this.loading = true; this.loading = true;
let trialInitiationPath: InitiationPath = "Password Manager trial from marketing website"; let trialInitiationPath: InitiationPath = "Password Manager trial from marketing website";
let plan: PlanInformation = { let plan: PlanInformation = {
@@ -272,10 +274,13 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
initiationPath: trialInitiationPath, initiationPath: trialInitiationPath,
}; };
const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({ const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod(
organization, {
plan, organization,
}); plan,
},
activeUserId,
);
this.orgId = response?.id; this.orgId = response?.id;
this.billingSubLabel = response.name.toString(); this.billingSubLabel = response.name.toString();
@@ -351,27 +356,30 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
); );
/** Create an organization unless the trial is for secrets manager */ /** Create an organization unless the trial is for secrets manager */
async conditionallyCreateOrganization(): Promise<void> { async conditionallyCreateOrganization(activeUserId: UserId): Promise<void> {
if (!this.isSecretsManagerFree) { if (!this.isSecretsManagerFree) {
this.verticalStepper.next(); this.verticalStepper.next();
return; return;
} }
const response = await this.organizationBillingService.startFree({ const response = await this.organizationBillingService.startFree(
organization: { {
name: this.orgInfoFormGroup.value.name == null ? "" : this.orgInfoFormGroup.value.name, organization: {
billingEmail: name: this.orgInfoFormGroup.value.name == null ? "" : this.orgInfoFormGroup.value.name,
this.orgInfoFormGroup.value.billingEmail == null billingEmail:
? "" this.orgInfoFormGroup.value.billingEmail == null
: this.orgInfoFormGroup.value.billingEmail, ? ""
initiationPath: "Password Manager trial from marketing website", : this.orgInfoFormGroup.value.billingEmail,
initiationPath: "Password Manager trial from marketing website",
},
plan: {
type: 0,
subscribeToSecretsManager: true,
isFromSecretsManagerTrial: true,
},
}, },
plan: { activeUserId,
type: 0, );
subscribeToSecretsManager: true,
isFromSecretsManagerTrial: true,
},
});
this.orgId = response.id; this.orgId = response.id;
this.verticalStepper.next(); this.verticalStepper.next();

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
import { UserId } from "@bitwarden/user-core";
import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
import { InitiationPath } from "../../models/request/reference-event.request"; import { InitiationPath } from "../../models/request/reference-event.request";
import { PaymentMethodType, PlanType } from "../enums"; import { PaymentMethodType, PlanType } from "../enums";
@@ -47,16 +49,22 @@ export abstract class OrganizationBillingServiceAbstraction {
abstract purchaseSubscription( abstract purchaseSubscription(
subscription: SubscriptionInformation, subscription: SubscriptionInformation,
activeUserId: UserId,
): Promise<OrganizationResponse>; ): Promise<OrganizationResponse>;
abstract purchaseSubscriptionNoPaymentMethod( abstract purchaseSubscriptionNoPaymentMethod(
subscription: SubscriptionInformation, subscription: SubscriptionInformation,
activeUserId: UserId,
): Promise<OrganizationResponse>; ): Promise<OrganizationResponse>;
abstract startFree(subscription: SubscriptionInformation): Promise<OrganizationResponse>; abstract startFree(
subscription: SubscriptionInformation,
activeUserId: UserId,
): Promise<OrganizationResponse>;
abstract restartSubscription( abstract restartSubscription(
organizationId: string, organizationId: string,
subscription: SubscriptionInformation, subscription: SubscriptionInformation,
activeUserId: UserId,
): Promise<void>; ): 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 { OrganizationApiServiceAbstraction as OrganizationApiService } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { import {
BillingApiServiceAbstraction, BillingApiServiceAbstraction,
PaymentInformation,
SubscriptionInformation, SubscriptionInformation,
} from "@bitwarden/common/billing/abstractions"; } from "@bitwarden/common/billing/abstractions";
import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums"; 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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SyncService } from "@bitwarden/common/platform/sync"; 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. // 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 // eslint-disable-next-line no-restricted-imports
import { KeyService } from "@bitwarden/key-management"; 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 { OrganizationResponse } from "../../admin-console/models/response/organization.response";
import { EncString } from "../../key-management/crypto/models/enc-string"; import { EncString } from "../../key-management/crypto/models/enc-string";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { OrgKey } from "../../types/key"; import { OrgKey } from "../../types/key";
import { PaymentMethodResponse } from "../models/response/payment-method.response"; import { PaymentMethodResponse } from "../models/response/payment-method.response";
@@ -31,6 +36,8 @@ describe("OrganizationBillingService", () => {
let sut: OrganizationBillingService; let sut: OrganizationBillingService;
const mockUserId = newGuid() as UserId;
beforeEach(() => { beforeEach(() => {
apiService = mock<ApiService>(); apiService = mock<ApiService>();
billingApiService = mock<BillingApiServiceAbstraction>(); billingApiService = mock<BillingApiServiceAbstraction>();
@@ -115,12 +122,12 @@ describe("OrganizationBillingService", () => {
} as OrganizationResponse; } as OrganizationResponse;
organizationApiService.create.mockResolvedValue(organizationResponse); organizationApiService.create.mockResolvedValue(organizationResponse);
keyService.makeOrgKey.mockResolvedValue([new EncString("encrypyted-key"), {} as OrgKey]); keyService.makeOrgKey.mockResolvedValue([new EncString("encrypted-key"), {} as OrgKey]);
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypyted-key")]); keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]);
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypyted")); encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypted"));
//Act //Act
const response = await sut.purchaseSubscription(subscriptionInformation); const response = await sut.purchaseSubscription(subscriptionInformation, mockUserId);
//Assert //Assert
expect(organizationApiService.create).toHaveBeenCalledTimes(1); expect(organizationApiService.create).toHaveBeenCalledTimes(1);
@@ -141,10 +148,10 @@ describe("OrganizationBillingService", () => {
organizationApiService.create.mockRejectedValue(new Error("Failed to create organization")); organizationApiService.create.mockRejectedValue(new Error("Failed to create organization"));
keyService.makeOrgKey.mockResolvedValue([new EncString("encrypted-key"), {} as OrgKey]); keyService.makeOrgKey.mockResolvedValue([new EncString("encrypted-key"), {} as OrgKey]);
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]); keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]);
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypyted")); encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypted"));
// Act & Assert // Act & Assert
await expect(sut.purchaseSubscription(subscriptionInformation)).rejects.toThrow( await expect(sut.purchaseSubscription(subscriptionInformation, mockUserId)).rejects.toThrow(
"Failed to create organization", "Failed to create organization",
); );
}); });
@@ -163,7 +170,7 @@ describe("OrganizationBillingService", () => {
keyService.makeOrgKey.mockRejectedValue(new Error("Key generation failed")); keyService.makeOrgKey.mockRejectedValue(new Error("Key generation failed"));
// Act & Assert // Act & Assert
await expect(sut.purchaseSubscription(subscriptionInformation)).rejects.toThrow( await expect(sut.purchaseSubscription(subscriptionInformation, mockUserId)).rejects.toThrow(
"Key generation failed", "Key generation failed",
); );
}); });
@@ -180,7 +187,7 @@ describe("OrganizationBillingService", () => {
} as SubscriptionInformation; } as SubscriptionInformation;
// Act & Assert // 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")); encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypted"));
//Act //Act
const response = await sut.purchaseSubscriptionNoPaymentMethod(subscriptionInformation); const response = await sut.purchaseSubscriptionNoPaymentMethod(
subscriptionInformation,
mockUserId,
);
//Assert //Assert
expect(organizationApiService.createWithoutPayment).toHaveBeenCalledTimes(1); expect(organizationApiService.createWithoutPayment).toHaveBeenCalledTimes(1);
@@ -223,7 +233,7 @@ describe("OrganizationBillingService", () => {
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypted")); encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypted"));
await expect( await expect(
sut.purchaseSubscriptionNoPaymentMethod(subscriptionInformation), sut.purchaseSubscriptionNoPaymentMethod(subscriptionInformation, mockUserId),
).rejects.toThrow("Creation failed"); ).rejects.toThrow("Creation failed");
}); });
@@ -237,7 +247,7 @@ describe("OrganizationBillingService", () => {
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]); keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]);
await expect( await expect(
sut.purchaseSubscriptionNoPaymentMethod(subscriptionInformation), sut.purchaseSubscriptionNoPaymentMethod(subscriptionInformation, mockUserId),
).rejects.toThrow("Key generation failed"); ).rejects.toThrow("Key generation failed");
}); });
}); });
@@ -256,12 +266,12 @@ describe("OrganizationBillingService", () => {
} as OrganizationResponse; } as OrganizationResponse;
organizationApiService.create.mockResolvedValue(organizationResponse); organizationApiService.create.mockResolvedValue(organizationResponse);
keyService.makeOrgKey.mockResolvedValue([new EncString("encrypyted-key"), {} as OrgKey]); keyService.makeOrgKey.mockResolvedValue([new EncString("encrypted-key"), {} as OrgKey]);
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypyted-key")]); keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]);
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypyted")); encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypted"));
//Act //Act
const response = await sut.startFree(subscriptionInformation); const response = await sut.startFree(subscriptionInformation, mockUserId);
//Assert //Assert
expect(organizationApiService.create).toHaveBeenCalledTimes(1); expect(organizationApiService.create).toHaveBeenCalledTimes(1);
@@ -277,7 +287,9 @@ describe("OrganizationBillingService", () => {
keyService.makeOrgKey.mockRejectedValue(new Error("Key generation failed")); keyService.makeOrgKey.mockRejectedValue(new Error("Key generation failed"));
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]); 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 () => { 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")); organizationApiService.create.mockRejectedValue(new Error("Failed to create organization"));
keyService.makeOrgKey.mockResolvedValue([new EncString("encrypted-key"), {} as OrgKey]); keyService.makeOrgKey.mockResolvedValue([new EncString("encrypted-key"), {} as OrgKey]);
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]); keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]);
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypyted")); encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypted"));
// Act & Assert // Act & Assert
await expect(sut.startFree(subscriptionInformation)).rejects.toThrow( await expect(sut.startFree(subscriptionInformation, mockUserId)).rejects.toThrow(
"Failed to create organization", "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. // 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 // eslint-disable-next-line no-restricted-imports
import { KeyService } from "@bitwarden/key-management"; import { KeyService } from "@bitwarden/key-management";
import { UserId } from "@bitwarden/user-core";
import { ApiService } from "../../abstractions/api.service"; import { ApiService } from "../../abstractions/api.service";
import { OrganizationApiServiceAbstraction as OrganizationApiService } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; 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; return paymentMethod?.paymentSource;
} }
async purchaseSubscription(subscription: SubscriptionInformation): Promise<OrganizationResponse> { async purchaseSubscription(
subscription: SubscriptionInformation,
activeUserId: UserId,
): Promise<OrganizationResponse> {
const request = new OrganizationCreateRequest(); const request = new OrganizationCreateRequest();
const organizationKeys = await this.makeOrganizationKeys(); const organizationKeys = await this.makeOrganizationKeys(activeUserId);
this.setOrganizationKeys(request, organizationKeys); this.setOrganizationKeys(request, organizationKeys);
@@ -73,10 +77,11 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
async purchaseSubscriptionNoPaymentMethod( async purchaseSubscriptionNoPaymentMethod(
subscription: SubscriptionInformation, subscription: SubscriptionInformation,
activeUserId: UserId,
): Promise<OrganizationResponse> { ): Promise<OrganizationResponse> {
const request = new OrganizationNoPaymentMethodCreateRequest(); const request = new OrganizationNoPaymentMethodCreateRequest();
const organizationKeys = await this.makeOrganizationKeys(); const organizationKeys = await this.makeOrganizationKeys(activeUserId);
this.setOrganizationKeys(request, organizationKeys); this.setOrganizationKeys(request, organizationKeys);
@@ -93,10 +98,13 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
return response; return response;
} }
async startFree(subscription: SubscriptionInformation): Promise<OrganizationResponse> { async startFree(
subscription: SubscriptionInformation,
activeUserId: UserId,
): Promise<OrganizationResponse> {
const request = new OrganizationCreateRequest(); const request = new OrganizationCreateRequest();
const organizationKeys = await this.makeOrganizationKeys(); const organizationKeys = await this.makeOrganizationKeys(activeUserId);
this.setOrganizationKeys(request, organizationKeys); this.setOrganizationKeys(request, organizationKeys);
@@ -113,8 +121,8 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
return response; return response;
} }
private async makeOrganizationKeys(): Promise<OrganizationKeys> { private async makeOrganizationKeys(activeUserId: UserId): Promise<OrganizationKeys> {
const [encryptedKey, key] = await this.keyService.makeOrgKey<OrgKey>(); const [encryptedKey, key] = await this.keyService.makeOrgKey<OrgKey>(activeUserId);
const [publicKey, encryptedPrivateKey] = await this.keyService.makeKeyPair(key); const [publicKey, encryptedPrivateKey] = await this.keyService.makeKeyPair(key);
const encryptedCollectionName = await this.encryptService.encryptString( const encryptedCollectionName = await this.encryptService.encryptString(
this.i18nService.t("defaultCollection"), this.i18nService.t("defaultCollection"),
@@ -214,9 +222,10 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
async restartSubscription( async restartSubscription(
organizationId: string, organizationId: string,
subscription: SubscriptionInformation, subscription: SubscriptionInformation,
activeUserId: UserId,
): Promise<void> { ): Promise<void> {
const request = new OrganizationCreateRequest(); const request = new OrganizationCreateRequest();
const organizationKeys = await this.makeOrganizationKeys(); const organizationKeys = await this.makeOrganizationKeys(activeUserId);
this.setOrganizationKeys(request, organizationKeys); this.setOrganizationKeys(request, organizationKeys);
this.setOrganizationInformation(request, subscription.organization); this.setOrganizationInformation(request, subscription.organization);
this.setPlanInformation(request, subscription.plan); this.setPlanInformation(request, subscription.plan);

View File

@@ -259,11 +259,12 @@ export abstract class KeyService {
/** /**
* Creates a new organization key and encrypts it with the user's public key. * Creates a new organization key and encrypts it with the user's public key.
* This method can also return Provider keys for creating new Provider users. * This method can also return Provider keys for creating new Provider users.
* * @param userId The user id of the target user's public key to use.
* @throws Error when no active user or user have no public key * @throws Error when userId is null or undefined.
* @returns The new encrypted org key and the decrypted key itself * @throws Error when no public key is found for the target user.
* @returns The new encrypted OrgKey | ProviderKey and the decrypted key itself
*/ */
abstract makeOrgKey<T extends OrgKey | ProviderKey>(): Promise<[EncString, T]>; abstract makeOrgKey<T extends OrgKey | ProviderKey>(userId: UserId): Promise<[EncString, T]>;
/** /**
* Sets the user's encrypted private key in storage and * Sets the user's encrypted private key in storage and
* clears the decrypted private key from memory * clears the decrypted private key from memory

View File

@@ -39,7 +39,13 @@ import {
} from "@bitwarden/common/spec"; } from "@bitwarden/common/spec";
import { CsprngArray } from "@bitwarden/common/types/csprng"; import { CsprngArray } from "@bitwarden/common/types/csprng";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { UserKey, MasterKey } from "@bitwarden/common/types/key"; import {
UserKey,
MasterKey,
UserPublicKey,
OrgKey,
ProviderKey,
} from "@bitwarden/common/types/key";
import { KdfConfigService } from "./abstractions/kdf-config.service"; import { KdfConfigService } from "./abstractions/kdf-config.service";
import { UserPrivateKeyDecryptionFailedError } from "./abstractions/key.service"; import { UserPrivateKeyDecryptionFailedError } from "./abstractions/key.service";
@@ -1029,6 +1035,66 @@ describe("keyService", () => {
}); });
}); });
describe("makeOrgKey", () => {
const mockUserPublicKey = new Uint8Array(64) as UserPublicKey;
const shareKey = new SymmetricCryptoKey(new Uint8Array(64));
const mockEncapsulatedKey = new EncString("mockEncapsulatedKey");
beforeEach(() => {
keyService.userPublicKey$ = jest
.fn()
.mockReturnValueOnce(new BehaviorSubject(mockUserPublicKey));
keyGenerationService.createKey.mockResolvedValue(shareKey);
encryptService.encapsulateKeyUnsigned.mockResolvedValue(mockEncapsulatedKey);
});
it("creates a new OrgKey and encapsulates it with the user's public key", async () => {
const result = await keyService.makeOrgKey<OrgKey>(mockUserId);
expect(result).toEqual([mockEncapsulatedKey, shareKey as OrgKey]);
expect(keyService.userPublicKey$).toHaveBeenCalledWith(mockUserId);
expect(keyGenerationService.createKey).toHaveBeenCalledWith(512);
expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(
shareKey,
mockUserPublicKey,
);
});
it("creates a new ProviderKey and encapsulates it with the user's public key", async () => {
const result = await keyService.makeOrgKey<ProviderKey>(mockUserId);
expect(result).toEqual([mockEncapsulatedKey, shareKey as ProviderKey]);
expect(keyService.userPublicKey$).toHaveBeenCalledWith(mockUserId);
expect(keyGenerationService.createKey).toHaveBeenCalledWith(512);
expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(
shareKey,
mockUserPublicKey,
);
});
test.each([null as unknown as UserId, undefined as unknown as UserId])(
"throws when the provided userId is %s",
async (userId) => {
await expect(keyService.makeOrgKey(userId)).rejects.toThrow("UserId is required");
expect(keyService.userPublicKey$).not.toHaveBeenCalled();
expect(keyGenerationService.createKey).not.toHaveBeenCalled();
expect(encryptService.encapsulateKeyUnsigned).not.toHaveBeenCalled();
},
);
it("throws if the user's public key is not found", async () => {
keyService.userPublicKey$ = jest.fn().mockReturnValueOnce(new BehaviorSubject(null));
await expect(keyService.makeOrgKey(mockUserId)).rejects.toThrow(
"No public key found for user " + mockUserId,
);
expect(keyGenerationService.createKey).not.toHaveBeenCalled();
expect(encryptService.encapsulateKeyUnsigned).not.toHaveBeenCalled();
});
});
describe("userEncryptionKeyPair$", () => { describe("userEncryptionKeyPair$", () => {
type SetupKeysParams = { type SetupKeysParams = {
makeMasterKey: boolean; makeMasterKey: boolean;

View File

@@ -446,19 +446,17 @@ export class DefaultKeyService implements KeyServiceAbstraction {
await this.stateProvider.setUserState(USER_ENCRYPTED_PROVIDER_KEYS, null, userId); await this.stateProvider.setUserState(USER_ENCRYPTED_PROVIDER_KEYS, null, userId);
} }
// TODO: Make userId required async makeOrgKey<T extends OrgKey | ProviderKey>(userId: UserId): Promise<[EncString, T]> {
async makeOrgKey<T extends OrgKey | ProviderKey>(userId?: UserId): Promise<[EncString, T]> {
const shareKey = await this.keyGenerationService.createKey(512);
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) { if (userId == null) {
throw new Error("No active user found."); throw new Error("UserId is required");
} }
const publicKey = await firstValueFrom(this.userPublicKey$(userId)); const publicKey = await firstValueFrom(this.userPublicKey$(userId));
if (publicKey == null) { if (publicKey == null) {
throw new Error("No public key found."); throw new Error("No public key found for user " + userId);
} }
const shareKey = await this.keyGenerationService.createKey(512);
const encShareKey = await this.encryptService.encapsulateKeyUnsigned(shareKey, publicKey); const encShareKey = await this.encryptService.encapsulateKeyUnsigned(shareKey, publicKey);
return [encShareKey, shareKey as T]; return [encShareKey, shareKey as T];
} }