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:
@@ -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;
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user