1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

feat(user-decryption-options) [PM-26413]: Remove ActiveUserState from UserDecryptionOptionsService (#16894)

* feat(user-decryption-options) [PM-26413]: Update UserDecryptionOptionsService and tests to use UserId-only APIs.

* feat(user-decryption-options) [PM-26413]: Update InternalUserDecryptionOptionsService call sites to use UserId-only API.

* feat(user-decryption-options) [PM-26413] Update userDecryptionOptions$ call sites to use the UserId-only API.

* feat(user-decryption-options) [PM-26413]: Update additional call sites.

* feat(user-decryption-options) [PM-26413]: Update dependencies and an additional call site.

* feat(user-verification-service) [PM-26413]: Replace where allowed by unrestricted imports invocation of UserVerificationService.hasMasterPassword (deprecated) with UserDecryptionOptions.hasMasterPasswordById$. Additional work to complete as tech debt tracked in PM-27009.

* feat(user-decryption-options) [PM-26413]: Update for non-null strict adherence.

* feat(user-decryption-options) [PM-26413]: Update type safety and defensive returns.

* chore(user-decryption-options) [PM-26413]: Comment cleanup.

* feat(user-decryption-options) [PM-26413]: Update tests.

* feat(user-decryption-options) [PM-26413]: Standardize null-checking on active account id for new API consumption.

* feat(vault-timeout-settings-service) [PM-26413]: Add test cases to illustrate null active account from AccountService.

* fix(fido2-user-verification-service-spec) [PM-26413]: Update test harness to use FakeAccountService.

* fix(downstream-components) [PM-26413]: Prefer use of the getUserId operator in all authenticated contexts for user id provided to UserDecryptionOptionsService.

---------

Co-authored-by: bnagawiecki <107435978+bnagawiecki@users.noreply.github.com>
This commit is contained in:
Dave
2025-11-25 11:23:22 -05:00
committed by GitHub
parent c04c1757ea
commit cf6569bfea
33 changed files with 280 additions and 172 deletions

View File

@@ -728,7 +728,9 @@ export default class MainBackground {
this.appIdService = new AppIdService(this.storageService, this.logService);
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
this.userDecryptionOptionsService = new UserDecryptionOptionsService(
this.singleUserStateProvider,
);
this.organizationService = new DefaultOrganizationService(this.stateProvider);
this.policyService = new DefaultPolicyService(this.stateProvider, this.organizationService);
@@ -859,8 +861,6 @@ export default class MainBackground {
this.stateProvider,
);
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
this.deviceTrustService = new DeviceTrustService(
this.keyGenerationService,
@@ -876,6 +876,7 @@ export default class MainBackground {
this.userDecryptionOptionsService,
this.logService,
this.configService,
this.accountService,
);
this.devicesService = new DevicesServiceImplementation(

View File

@@ -36,6 +36,7 @@ import {
LoginEmailService,
SsoUrlService,
LogoutService,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ExtensionNewDeviceVerificationComponentService } from "@bitwarden/browser/auth/services/new-device-verification/extension-new-device-verification-component.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -607,7 +608,12 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: Fido2UserVerificationService,
useClass: Fido2UserVerificationService,
deps: [PasswordRepromptService, UserVerificationService, DialogService],
deps: [
PasswordRepromptService,
UserDecryptionOptionsServiceAbstraction,
DialogService,
AccountServiceAbstraction,
],
}),
safeProvider({
provide: AnimationControlService,

View File

@@ -1,11 +1,15 @@
import { MockProxy, mock } from "jest-mock-extended";
import { of } from "rxjs";
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { newGuid } from "@bitwarden/guid";
import { PasswordRepromptService } from "@bitwarden/vault";
// FIXME (PM-22628): Popup imports are forbidden in background
@@ -31,21 +35,24 @@ describe("Fido2UserVerificationService", () => {
let fido2UserVerificationService: Fido2UserVerificationService;
let passwordRepromptService: MockProxy<PasswordRepromptService>;
let userVerificationService: MockProxy<UserVerificationService>;
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let dialogService: MockProxy<DialogService>;
let accountService: FakeAccountService;
let cipher: CipherView;
beforeEach(() => {
passwordRepromptService = mock<PasswordRepromptService>();
userVerificationService = mock<UserVerificationService>();
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
dialogService = mock<DialogService>();
accountService = mockAccountServiceWith(newGuid() as UserId);
cipher = createCipherView();
fido2UserVerificationService = new Fido2UserVerificationService(
passwordRepromptService,
userVerificationService,
userDecryptionOptionsService,
dialogService,
accountService,
);
(UserVerificationDialogComponent.open as jest.Mock).mockResolvedValue({
@@ -67,7 +74,7 @@ describe("Fido2UserVerificationService", () => {
it("should call master password reprompt dialog if user is redirected from lock screen, has master password and master password reprompt is required", async () => {
cipher.reprompt = CipherRepromptType.Password;
userVerificationService.hasMasterPassword.mockResolvedValue(true);
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(true));
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
const result = await fido2UserVerificationService.handleUserVerification(
@@ -82,7 +89,7 @@ describe("Fido2UserVerificationService", () => {
it("should call user verification dialog if user is redirected from lock screen, does not have a master password and master password reprompt is required", async () => {
cipher.reprompt = CipherRepromptType.Password;
userVerificationService.hasMasterPassword.mockResolvedValue(false);
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
const result = await fido2UserVerificationService.handleUserVerification(
true,
@@ -98,7 +105,7 @@ describe("Fido2UserVerificationService", () => {
it("should call user verification dialog if user is not redirected from lock screen, does not have a master password and master password reprompt is required", async () => {
cipher.reprompt = CipherRepromptType.Password;
userVerificationService.hasMasterPassword.mockResolvedValue(false);
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
const result = await fido2UserVerificationService.handleUserVerification(
true,
@@ -114,7 +121,7 @@ describe("Fido2UserVerificationService", () => {
it("should call master password reprompt dialog if user is not redirected from lock screen, has a master password and master password reprompt is required", async () => {
cipher.reprompt = CipherRepromptType.Password;
userVerificationService.hasMasterPassword.mockResolvedValue(false);
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
const result = await fido2UserVerificationService.handleUserVerification(
@@ -176,7 +183,7 @@ describe("Fido2UserVerificationService", () => {
it("should call master password reprompt dialog if user is redirected from lock screen, has master password and master password reprompt is required", async () => {
cipher.reprompt = CipherRepromptType.Password;
userVerificationService.hasMasterPassword.mockResolvedValue(true);
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(true));
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
const result = await fido2UserVerificationService.handleUserVerification(
@@ -191,7 +198,7 @@ describe("Fido2UserVerificationService", () => {
it("should call user verification dialog if user is redirected from lock screen, does not have a master password and master password reprompt is required", async () => {
cipher.reprompt = CipherRepromptType.Password;
userVerificationService.hasMasterPassword.mockResolvedValue(false);
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
const result = await fido2UserVerificationService.handleUserVerification(
false,
@@ -207,7 +214,7 @@ describe("Fido2UserVerificationService", () => {
it("should call user verification dialog if user is not redirected from lock screen, does not have a master password and master password reprompt is required", async () => {
cipher.reprompt = CipherRepromptType.Password;
userVerificationService.hasMasterPassword.mockResolvedValue(false);
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
const result = await fido2UserVerificationService.handleUserVerification(
false,
@@ -223,7 +230,7 @@ describe("Fido2UserVerificationService", () => {
it("should call master password reprompt dialog if user is not redirected from lock screen, has a master password and master password reprompt is required", async () => {
cipher.reprompt = CipherRepromptType.Password;
userVerificationService.hasMasterPassword.mockResolvedValue(false);
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
const result = await fido2UserVerificationService.handleUserVerification(

View File

@@ -3,7 +3,8 @@
import { firstValueFrom } from "rxjs";
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
@@ -15,8 +16,9 @@ import { SetPinComponent } from "../../auth/popup/components/set-pin.component";
export class Fido2UserVerificationService {
constructor(
private passwordRepromptService: PasswordRepromptService,
private userVerificationService: UserVerificationService,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private dialogService: DialogService,
private accountService: AccountService,
) {}
/**
@@ -78,7 +80,15 @@ export class Fido2UserVerificationService {
}
private async handleMasterPasswordReprompt(): Promise<boolean> {
const hasMasterPassword = await this.userVerificationService.hasMasterPassword();
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
if (!activeAccount?.id) {
return false;
}
const hasMasterPassword = await firstValueFrom(
this.userDecryptionOptionsService.hasMasterPasswordById$(activeAccount.id),
);
// TDE users have no master password, so we need to use the UserVerification prompt
return hasMasterPassword

View File

@@ -512,7 +512,9 @@ export class ServiceContainer {
")";
this.biometricStateService = new DefaultBiometricStateService(this.stateProvider);
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
this.userDecryptionOptionsService = new UserDecryptionOptionsService(
this.singleUserStateProvider,
);
this.ssoUrlService = new SsoUrlService();
this.organizationService = new DefaultOrganizationService(this.stateProvider);
@@ -702,6 +704,7 @@ export class ServiceContainer {
this.userDecryptionOptionsService,
this.logService,
this.configService,
this.accountService,
);
this.loginStrategyService = new LoginStrategyService(

View File

@@ -119,7 +119,9 @@ describe("DesktopSetInitialPasswordService", () => {
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
userDecryptionOptionsSubject,
);
setPasswordRequest = new SetPasswordRequest(
credentials.newServerMasterKeyHash,

View File

@@ -123,7 +123,9 @@ describe("WebSetInitialPasswordService", () => {
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
userDecryptionOptionsSubject,
);
setPasswordRequest = new SetPasswordRequest(
credentials.newServerMasterKeyHash,

View File

@@ -1,11 +1,10 @@
import { Component, OnInit, OnDestroy } from "@angular/core";
import { firstValueFrom, from, lastValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
import { firstValueFrom, lastValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { DialogService } from "@bitwarden/components";
import { HeaderModule } from "../../../layouts/header/header.module";
@@ -42,8 +41,7 @@ export class AccountComponent implements OnInit, OnDestroy {
constructor(
private accountService: AccountService,
private dialogService: DialogService,
private userVerificationService: UserVerificationService,
private configService: ConfigService,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private organizationService: OrganizationService,
) {}
@@ -56,7 +54,7 @@ export class AccountComponent implements OnInit, OnDestroy {
map((organizations) => organizations.some((o) => o.userIsManagedByOrganization === true)),
);
const hasMasterPassword$ = from(this.userVerificationService.hasMasterPassword());
const hasMasterPassword$ = this.userDecryptionOptionsService.hasMasterPasswordById$(userId);
this.showChangeEmail$ = hasMasterPassword$;

View File

@@ -5,6 +5,8 @@ import { firstValueFrom } from "rxjs";
import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password";
import { InputPasswordFlow } from "@bitwarden/auth/angular";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { CalloutModule } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -24,12 +26,15 @@ export class PasswordSettingsComponent implements OnInit {
constructor(
private router: Router,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private accountService: AccountService,
) {}
async ngOnInit() {
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const userHasMasterPassword = await firstValueFrom(
this.userDecryptionOptionsService.hasMasterPassword$,
this.userDecryptionOptionsService.hasMasterPasswordById$(userId),
);
if (!userHasMasterPassword) {
await this.router.navigate(["/settings/security/two-factor"]);
return;

View File

@@ -1,11 +1,10 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { firstValueFrom, map } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { DialogService } from "@bitwarden/components";
import { ChangeKdfModule } from "../../../key-management/change-kdf/change-kdf.module";
@@ -23,20 +22,28 @@ export class SecurityKeysComponent implements OnInit {
showChangeKdf = true;
constructor(
private userVerificationService: UserVerificationService,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private accountService: AccountService,
private apiService: ApiService,
private dialogService: DialogService,
) {}
async ngOnInit() {
this.showChangeKdf = await this.userVerificationService.hasMasterPassword();
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.showChangeKdf = await firstValueFrom(
this.userDecryptionOptionsService.hasMasterPasswordById$(userId),
);
}
async viewUserApiKey() {
const entityId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
if (!entityId) {
throw new Error("Active account not found");
}
await ApiKeyComponent.open(this.dialogService, {
data: {
keyType: "user",
@@ -55,6 +62,11 @@ export class SecurityKeysComponent implements OnInit {
const entityId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
if (!entityId) {
throw new Error("Active account not found");
}
await ApiKeyComponent.open(this.dialogService, {
data: {
keyType: "user",

View File

@@ -1,7 +1,9 @@
import { Component, OnInit } from "@angular/core";
import { Observable } from "rxjs";
import { firstValueFrom, Observable } from "rxjs";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@@ -20,7 +22,8 @@ export class SecurityComponent implements OnInit {
consolidatedSessionTimeoutComponent$: Observable<boolean>;
constructor(
private userVerificationService: UserVerificationService,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private accountService: AccountService,
private configService: ConfigService,
) {
this.consolidatedSessionTimeoutComponent$ = this.configService.getFeatureFlag$(
@@ -29,6 +32,9 @@ export class SecurityComponent implements OnInit {
}
async ngOnInit() {
this.showChangePassword = await this.userVerificationService.hasMasterPassword();
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.showChangePassword = userId
? await firstValueFrom(this.userDecryptionOptionsService.hasMasterPasswordById$(userId))
: false;
}
}

View File

@@ -95,7 +95,10 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
combineLatest([
this.organization$,
resetPasswordPolicies$,
this.userDecryptionOptionsService.userDecryptionOptions$,
this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.userDecryptionOptionsService.userDecryptionOptionsById$(userId)),
),
managingOrg$,
])
.pipe(takeUntil(this.destroy$))