mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +00:00
[PM-23626] Require userId for makeOrgKey on the key service (#15864)
* Update key service * Update consumers * Add unit test coverage for consumer services * Add unit test coverage for organization-billing service
This commit is contained in:
@@ -2,8 +2,11 @@
|
||||
// @ts-strict-ignore
|
||||
import { Component } from "@angular/core";
|
||||
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 { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite";
|
||||
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -29,13 +32,18 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent {
|
||||
protected authService: AuthService,
|
||||
private acceptOrganizationInviteService: AcceptOrganizationInviteService,
|
||||
private organizationInviteService: OrganizationInviteService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
super(router, platformUtilsService, i18nService, route, authService);
|
||||
}
|
||||
|
||||
async authedHandler(qParams: Params): Promise<void> {
|
||||
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) {
|
||||
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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { I18nService } from "../../core/i18n.service";
|
||||
|
||||
@@ -73,10 +75,13 @@ describe("AcceptOrganizationInviteService", () => {
|
||||
});
|
||||
|
||||
describe("validateAndAcceptInvite", () => {
|
||||
const activeUserId = newGuid() as UserId;
|
||||
|
||||
it("initializes an organization when given an invite where initOrganization is true", async () => {
|
||||
const mockOrgKey = "orgPrivateKey" as unknown as OrgKey;
|
||||
keyService.makeOrgKey.mockResolvedValue([
|
||||
{ encryptedString: "string" } as EncString,
|
||||
"orgPrivateKey" as unknown as OrgKey,
|
||||
mockOrgKey,
|
||||
]);
|
||||
keyService.makeKeyPair.mockResolvedValue([
|
||||
"orgPublicKey",
|
||||
@@ -88,10 +93,12 @@ describe("AcceptOrganizationInviteService", () => {
|
||||
encryptService.encryptString.mockResolvedValue({ encryptedString: "string" } as EncString);
|
||||
const invite = createOrgInvite({ initOrganization: true });
|
||||
|
||||
const result = await sut.validateAndAcceptInvite(invite);
|
||||
const result = await sut.validateAndAcceptInvite(invite, activeUserId);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(organizationUserApiService.postOrganizationUserAcceptInit).toHaveBeenCalled();
|
||||
expect(keyService.makeOrgKey).toHaveBeenCalledWith(activeUserId);
|
||||
expect(keyService.makeKeyPair).toHaveBeenCalledWith(mockOrgKey);
|
||||
expect(apiService.refreshIdentityToken).toHaveBeenCalled();
|
||||
expect(organizationUserApiService.postOrganizationUserAccept).not.toHaveBeenCalled();
|
||||
expect(organizationInviteService.getOrganizationInvite).not.toHaveBeenCalled();
|
||||
@@ -109,7 +116,7 @@ describe("AcceptOrganizationInviteService", () => {
|
||||
} as Policy,
|
||||
]);
|
||||
|
||||
const result = await sut.validateAndAcceptInvite(invite);
|
||||
const result = await sut.validateAndAcceptInvite(invite, activeUserId);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(authService.logOut).toHaveBeenCalled();
|
||||
@@ -130,7 +137,7 @@ describe("AcceptOrganizationInviteService", () => {
|
||||
} as Policy,
|
||||
]);
|
||||
|
||||
const result = await sut.validateAndAcceptInvite(providedInvite);
|
||||
const result = await sut.validateAndAcceptInvite(providedInvite, activeUserId);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(authService.logOut).toHaveBeenCalled();
|
||||
@@ -145,7 +152,7 @@ describe("AcceptOrganizationInviteService", () => {
|
||||
const invite = createOrgInvite();
|
||||
policyApiService.getPoliciesByToken.mockResolvedValue([]);
|
||||
|
||||
const result = await sut.validateAndAcceptInvite(invite);
|
||||
const result = await sut.validateAndAcceptInvite(invite, activeUserId);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled();
|
||||
@@ -175,7 +182,7 @@ describe("AcceptOrganizationInviteService", () => {
|
||||
false,
|
||||
]);
|
||||
|
||||
const result = await sut.validateAndAcceptInvite(invite);
|
||||
const result = await sut.validateAndAcceptInvite(invite, activeUserId);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled();
|
||||
@@ -214,7 +221,7 @@ describe("AcceptOrganizationInviteService", () => {
|
||||
true,
|
||||
]);
|
||||
|
||||
const result = await sut.validateAndAcceptInvite(invite);
|
||||
const result = await sut.validateAndAcceptInvite(invite, activeUserId);
|
||||
|
||||
expect(result).toBe(true);
|
||||
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 { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
@Injectable()
|
||||
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
|
||||
* 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 activeUserId the user ID of the active user accepting the invite
|
||||
* @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) {
|
||||
throw new Error("Invite cannot be null.");
|
||||
}
|
||||
|
||||
// Creation of a new org
|
||||
if (invite.initOrganization) {
|
||||
await this.acceptAndInitOrganization(invite);
|
||||
await this.acceptAndInitOrganization(invite, activeUserId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -81,8 +86,11 @@ export class AcceptOrganizationInviteService {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async acceptAndInitOrganization(invite: OrganizationInvite): Promise<void> {
|
||||
await this.prepareAcceptAndInitRequest(invite).then((request) =>
|
||||
private async acceptAndInitOrganization(
|
||||
invite: OrganizationInvite,
|
||||
activeUserId: UserId,
|
||||
): Promise<void> {
|
||||
await this.prepareAcceptAndInitRequest(invite, activeUserId).then((request) =>
|
||||
this.organizationUserApiService.postOrganizationUserAcceptInit(
|
||||
invite.organizationId,
|
||||
invite.organizationUserId,
|
||||
@@ -95,11 +103,12 @@ export class AcceptOrganizationInviteService {
|
||||
|
||||
private async prepareAcceptAndInitRequest(
|
||||
invite: OrganizationInvite,
|
||||
activeUserId: UserId,
|
||||
): Promise<OrganizationUserAcceptInitRequest> {
|
||||
const request = new OrganizationUserAcceptInitRequest();
|
||||
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 collection = await this.encryptService.encryptString(
|
||||
this.i18nService.t("defaultCollection"),
|
||||
|
||||
@@ -10,10 +10,12 @@ import {
|
||||
ViewChild,
|
||||
} from "@angular/core";
|
||||
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 { 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 {
|
||||
BillingInformation,
|
||||
OrganizationBillingServiceAbstraction as OrganizationBillingService,
|
||||
@@ -107,6 +109,7 @@ export class TrialBillingStepComponent implements OnInit, OnDestroy {
|
||||
private organizationBillingService: OrganizationBillingService,
|
||||
private toastService: ToastService,
|
||||
private taxService: TaxServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
@@ -190,6 +193,7 @@ export class TrialBillingStepComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private async createOrganization(): Promise<string> {
|
||||
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
const planResponse = this.findPlanFor(this.formGroup.value.cadence);
|
||||
|
||||
const { type, token } = await this.paymentComponent.tokenize();
|
||||
@@ -221,11 +225,14 @@ export class TrialBillingStepComponent implements OnInit, OnDestroy {
|
||||
skipTrial: this.trialLength === 0,
|
||||
};
|
||||
|
||||
const response = await this.organizationBillingService.purchaseSubscription({
|
||||
organization,
|
||||
plan,
|
||||
payment,
|
||||
});
|
||||
const response = await this.organizationBillingService.purchaseSubscription(
|
||||
{
|
||||
organization,
|
||||
plan,
|
||||
payment,
|
||||
},
|
||||
activeUserId,
|
||||
);
|
||||
|
||||
return response.id;
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { BillingNotificationService } from "../services/billing-notification.service";
|
||||
import { BillingSharedModule } from "../shared/billing-shared.module";
|
||||
@@ -769,6 +770,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
const doSubmit = async (): Promise<string> => {
|
||||
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
let orgId: string = null;
|
||||
const sub = this.sub?.subscription;
|
||||
const isCanceled = sub?.status === "canceled";
|
||||
@@ -776,7 +778,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
sub?.cancelled && this.organization.productTierType === ProductTierType.Free;
|
||||
|
||||
if (isCanceled || isCancelledDowngradedToFreeOrg) {
|
||||
await this.restartSubscription();
|
||||
await this.restartSubscription(activeUserId);
|
||||
orgId = this.organizationId;
|
||||
} else {
|
||||
orgId = await this.updateOrganization();
|
||||
@@ -816,7 +818,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
this.dialogRef.close();
|
||||
};
|
||||
|
||||
private async restartSubscription() {
|
||||
private async restartSubscription(activeUserId: UserId) {
|
||||
const org = await this.organizationApiService.get(this.organizationId);
|
||||
const organization: OrganizationInformation = {
|
||||
name: org.name,
|
||||
@@ -848,11 +850,15 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
billing: this.getBillingInformationFromTaxInfoComponent(),
|
||||
};
|
||||
|
||||
await this.organizationBillingService.restartSubscription(this.organization.id, {
|
||||
organization,
|
||||
plan,
|
||||
payment,
|
||||
});
|
||||
await this.organizationBillingService.restartSubscription(
|
||||
this.organization.id,
|
||||
{
|
||||
organization,
|
||||
plan,
|
||||
payment,
|
||||
},
|
||||
activeUserId,
|
||||
);
|
||||
}
|
||||
|
||||
private async updateOrganization() {
|
||||
|
||||
@@ -624,7 +624,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
const doSubmit = async (): Promise<string> => {
|
||||
let orgId: string;
|
||||
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 collection = await this.encryptService.encryptString(
|
||||
this.i18nService.t("defaultCollection"),
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
// @ts-strict-ignore
|
||||
import { Component, EventEmitter, Output } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
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 { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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 organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private readonly syncService: SyncService,
|
||||
private readonly accountService: AccountService,
|
||||
) {
|
||||
super(formBuilder, i18nService, platformUtilsService, toastService, tokenService);
|
||||
}
|
||||
|
||||
protected async submit(): Promise<void> {
|
||||
await super.submit();
|
||||
|
||||
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 collection = await this.encryptService.encryptString(
|
||||
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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import {
|
||||
OrganizationCreatedEvent,
|
||||
@@ -227,13 +228,14 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async orgNameEntrySubmit(): Promise<void> {
|
||||
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
const isTrialPaymentOptional = await firstValueFrom(this.trialPaymentOptional$);
|
||||
|
||||
/** Only skip payment if the flag is on AND trialLength > 0 */
|
||||
if (isTrialPaymentOptional && this.trialLength > 0) {
|
||||
await this.createOrganizationOnTrial();
|
||||
await this.createOrganizationOnTrial(activeUserId);
|
||||
} 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 */
|
||||
async createOrganizationOnTrial() {
|
||||
async createOrganizationOnTrial(activeUserId: UserId) {
|
||||
this.loading = true;
|
||||
let trialInitiationPath: InitiationPath = "Password Manager trial from marketing website";
|
||||
let plan: PlanInformation = {
|
||||
@@ -272,10 +274,13 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
initiationPath: trialInitiationPath,
|
||||
};
|
||||
|
||||
const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({
|
||||
organization,
|
||||
plan,
|
||||
});
|
||||
const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod(
|
||||
{
|
||||
organization,
|
||||
plan,
|
||||
},
|
||||
activeUserId,
|
||||
);
|
||||
|
||||
this.orgId = response?.id;
|
||||
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 */
|
||||
async conditionallyCreateOrganization(): Promise<void> {
|
||||
async conditionallyCreateOrganization(activeUserId: UserId): Promise<void> {
|
||||
if (!this.isSecretsManagerFree) {
|
||||
this.verticalStepper.next();
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await this.organizationBillingService.startFree({
|
||||
organization: {
|
||||
name: this.orgInfoFormGroup.value.name == null ? "" : this.orgInfoFormGroup.value.name,
|
||||
billingEmail:
|
||||
this.orgInfoFormGroup.value.billingEmail == null
|
||||
? ""
|
||||
: this.orgInfoFormGroup.value.billingEmail,
|
||||
initiationPath: "Password Manager trial from marketing website",
|
||||
const response = await this.organizationBillingService.startFree(
|
||||
{
|
||||
organization: {
|
||||
name: this.orgInfoFormGroup.value.name == null ? "" : this.orgInfoFormGroup.value.name,
|
||||
billingEmail:
|
||||
this.orgInfoFormGroup.value.billingEmail == null
|
||||
? ""
|
||||
: this.orgInfoFormGroup.value.billingEmail,
|
||||
initiationPath: "Password Manager trial from marketing website",
|
||||
},
|
||||
plan: {
|
||||
type: 0,
|
||||
subscribeToSecretsManager: true,
|
||||
isFromSecretsManagerTrial: true,
|
||||
},
|
||||
},
|
||||
plan: {
|
||||
type: 0,
|
||||
subscribeToSecretsManager: true,
|
||||
isFromSecretsManagerTrial: true,
|
||||
},
|
||||
});
|
||||
activeUserId,
|
||||
);
|
||||
|
||||
this.orgId = response.id;
|
||||
this.verticalStepper.next();
|
||||
|
||||
Reference in New Issue
Block a user