1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

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

* Update key service

* Update consumers

* Add unit test coverage for consumer services

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

View File

@@ -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;

View File

@@ -21,7 +21,9 @@ import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-st
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { 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(

View File

@@ -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"),

View File

@@ -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;
}

View File

@@ -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() {

View File

@@ -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"),

View File

@@ -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"),

View File

@@ -29,6 +29,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { 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();