mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 23:33:31 +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:
@@ -728,7 +728,9 @@ export default class MainBackground {
|
|||||||
|
|
||||||
this.appIdService = new AppIdService(this.storageService, this.logService);
|
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.organizationService = new DefaultOrganizationService(this.stateProvider);
|
||||||
this.policyService = new DefaultPolicyService(this.stateProvider, this.organizationService);
|
this.policyService = new DefaultPolicyService(this.stateProvider, this.organizationService);
|
||||||
|
|
||||||
@@ -859,8 +861,6 @@ export default class MainBackground {
|
|||||||
this.stateProvider,
|
this.stateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
|
||||||
|
|
||||||
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
||||||
this.deviceTrustService = new DeviceTrustService(
|
this.deviceTrustService = new DeviceTrustService(
|
||||||
this.keyGenerationService,
|
this.keyGenerationService,
|
||||||
@@ -876,6 +876,7 @@ export default class MainBackground {
|
|||||||
this.userDecryptionOptionsService,
|
this.userDecryptionOptionsService,
|
||||||
this.logService,
|
this.logService,
|
||||||
this.configService,
|
this.configService,
|
||||||
|
this.accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.devicesService = new DevicesServiceImplementation(
|
this.devicesService = new DevicesServiceImplementation(
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
LoginEmailService,
|
LoginEmailService,
|
||||||
SsoUrlService,
|
SsoUrlService,
|
||||||
LogoutService,
|
LogoutService,
|
||||||
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
} from "@bitwarden/auth/common";
|
} from "@bitwarden/auth/common";
|
||||||
import { ExtensionNewDeviceVerificationComponentService } from "@bitwarden/browser/auth/services/new-device-verification/extension-new-device-verification-component.service";
|
import { ExtensionNewDeviceVerificationComponentService } from "@bitwarden/browser/auth/services/new-device-verification/extension-new-device-verification-component.service";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
@@ -607,7 +608,12 @@ const safeProviders: SafeProvider[] = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: Fido2UserVerificationService,
|
provide: Fido2UserVerificationService,
|
||||||
useClass: Fido2UserVerificationService,
|
useClass: Fido2UserVerificationService,
|
||||||
deps: [PasswordRepromptService, UserVerificationService, DialogService],
|
deps: [
|
||||||
|
PasswordRepromptService,
|
||||||
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
|
DialogService,
|
||||||
|
AccountServiceAbstraction,
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: AnimationControlService,
|
provide: AnimationControlService,
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { MockProxy, mock } from "jest-mock-extended";
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
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 { 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 { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
import { newGuid } from "@bitwarden/guid";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
// FIXME (PM-22628): Popup imports are forbidden in background
|
// FIXME (PM-22628): Popup imports are forbidden in background
|
||||||
@@ -31,21 +35,24 @@ describe("Fido2UserVerificationService", () => {
|
|||||||
let fido2UserVerificationService: Fido2UserVerificationService;
|
let fido2UserVerificationService: Fido2UserVerificationService;
|
||||||
|
|
||||||
let passwordRepromptService: MockProxy<PasswordRepromptService>;
|
let passwordRepromptService: MockProxy<PasswordRepromptService>;
|
||||||
let userVerificationService: MockProxy<UserVerificationService>;
|
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
|
||||||
let dialogService: MockProxy<DialogService>;
|
let dialogService: MockProxy<DialogService>;
|
||||||
|
let accountService: FakeAccountService;
|
||||||
let cipher: CipherView;
|
let cipher: CipherView;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
passwordRepromptService = mock<PasswordRepromptService>();
|
passwordRepromptService = mock<PasswordRepromptService>();
|
||||||
userVerificationService = mock<UserVerificationService>();
|
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||||
dialogService = mock<DialogService>();
|
dialogService = mock<DialogService>();
|
||||||
|
accountService = mockAccountServiceWith(newGuid() as UserId);
|
||||||
|
|
||||||
cipher = createCipherView();
|
cipher = createCipherView();
|
||||||
|
|
||||||
fido2UserVerificationService = new Fido2UserVerificationService(
|
fido2UserVerificationService = new Fido2UserVerificationService(
|
||||||
passwordRepromptService,
|
passwordRepromptService,
|
||||||
userVerificationService,
|
userDecryptionOptionsService,
|
||||||
dialogService,
|
dialogService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
(UserVerificationDialogComponent.open as jest.Mock).mockResolvedValue({
|
(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 () => {
|
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;
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
userVerificationService.hasMasterPassword.mockResolvedValue(true);
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(true));
|
||||||
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
||||||
|
|
||||||
const result = await fido2UserVerificationService.handleUserVerification(
|
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 () => {
|
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;
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
|
||||||
|
|
||||||
const result = await fido2UserVerificationService.handleUserVerification(
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
true,
|
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 () => {
|
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;
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
|
||||||
|
|
||||||
const result = await fido2UserVerificationService.handleUserVerification(
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
true,
|
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 () => {
|
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;
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
|
||||||
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
||||||
|
|
||||||
const result = await fido2UserVerificationService.handleUserVerification(
|
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 () => {
|
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;
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
userVerificationService.hasMasterPassword.mockResolvedValue(true);
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(true));
|
||||||
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
||||||
|
|
||||||
const result = await fido2UserVerificationService.handleUserVerification(
|
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 () => {
|
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;
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
|
||||||
|
|
||||||
const result = await fido2UserVerificationService.handleUserVerification(
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
false,
|
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 () => {
|
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;
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
|
||||||
|
|
||||||
const result = await fido2UserVerificationService.handleUserVerification(
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
false,
|
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 () => {
|
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;
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
|
||||||
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
||||||
|
|
||||||
const result = await fido2UserVerificationService.handleUserVerification(
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
@@ -15,8 +16,9 @@ import { SetPinComponent } from "../../auth/popup/components/set-pin.component";
|
|||||||
export class Fido2UserVerificationService {
|
export class Fido2UserVerificationService {
|
||||||
constructor(
|
constructor(
|
||||||
private passwordRepromptService: PasswordRepromptService,
|
private passwordRepromptService: PasswordRepromptService,
|
||||||
private userVerificationService: UserVerificationService,
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
|
private accountService: AccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,7 +80,15 @@ export class Fido2UserVerificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async handleMasterPasswordReprompt(): Promise<boolean> {
|
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
|
// TDE users have no master password, so we need to use the UserVerification prompt
|
||||||
return hasMasterPassword
|
return hasMasterPassword
|
||||||
|
|||||||
@@ -512,7 +512,9 @@ export class ServiceContainer {
|
|||||||
")";
|
")";
|
||||||
|
|
||||||
this.biometricStateService = new DefaultBiometricStateService(this.stateProvider);
|
this.biometricStateService = new DefaultBiometricStateService(this.stateProvider);
|
||||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
this.userDecryptionOptionsService = new UserDecryptionOptionsService(
|
||||||
|
this.singleUserStateProvider,
|
||||||
|
);
|
||||||
this.ssoUrlService = new SsoUrlService();
|
this.ssoUrlService = new SsoUrlService();
|
||||||
|
|
||||||
this.organizationService = new DefaultOrganizationService(this.stateProvider);
|
this.organizationService = new DefaultOrganizationService(this.stateProvider);
|
||||||
@@ -702,6 +704,7 @@ export class ServiceContainer {
|
|||||||
this.userDecryptionOptionsService,
|
this.userDecryptionOptionsService,
|
||||||
this.logService,
|
this.logService,
|
||||||
this.configService,
|
this.configService,
|
||||||
|
this.accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.loginStrategyService = new LoginStrategyService(
|
this.loginStrategyService = new LoginStrategyService(
|
||||||
|
|||||||
@@ -119,7 +119,9 @@ describe("DesktopSetInitialPasswordService", () => {
|
|||||||
|
|
||||||
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
|
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
|
||||||
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
|
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
|
||||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||||
|
userDecryptionOptionsSubject,
|
||||||
|
);
|
||||||
|
|
||||||
setPasswordRequest = new SetPasswordRequest(
|
setPasswordRequest = new SetPasswordRequest(
|
||||||
credentials.newServerMasterKeyHash,
|
credentials.newServerMasterKeyHash,
|
||||||
|
|||||||
@@ -123,7 +123,9 @@ describe("WebSetInitialPasswordService", () => {
|
|||||||
|
|
||||||
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
|
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
|
||||||
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
|
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
|
||||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||||
|
userDecryptionOptionsSubject,
|
||||||
|
);
|
||||||
|
|
||||||
setPasswordRequest = new SetPasswordRequest(
|
setPasswordRequest = new SetPasswordRequest(
|
||||||
credentials.newServerMasterKeyHash,
|
credentials.newServerMasterKeyHash,
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Component, OnInit, OnDestroy } from "@angular/core";
|
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 { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { HeaderModule } from "../../../layouts/header/header.module";
|
import { HeaderModule } from "../../../layouts/header/header.module";
|
||||||
@@ -42,8 +41,7 @@ export class AccountComponent implements OnInit, OnDestroy {
|
|||||||
constructor(
|
constructor(
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private userVerificationService: UserVerificationService,
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
private configService: ConfigService,
|
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -56,7 +54,7 @@ export class AccountComponent implements OnInit, OnDestroy {
|
|||||||
map((organizations) => organizations.some((o) => o.userIsManagedByOrganization === true)),
|
map((organizations) => organizations.some((o) => o.userIsManagedByOrganization === true)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasMasterPassword$ = from(this.userVerificationService.hasMasterPassword());
|
const hasMasterPassword$ = this.userDecryptionOptionsService.hasMasterPasswordById$(userId);
|
||||||
|
|
||||||
this.showChangeEmail$ = hasMasterPassword$;
|
this.showChangeEmail$ = hasMasterPassword$;
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { firstValueFrom } from "rxjs";
|
|||||||
import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password";
|
import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password";
|
||||||
import { InputPasswordFlow } from "@bitwarden/auth/angular";
|
import { InputPasswordFlow } from "@bitwarden/auth/angular";
|
||||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
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 { CalloutModule } from "@bitwarden/components";
|
||||||
import { I18nPipe } from "@bitwarden/ui-common";
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
|
|
||||||
@@ -24,12 +26,15 @@ export class PasswordSettingsComponent implements OnInit {
|
|||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
|
private accountService: AccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
const userHasMasterPassword = await firstValueFrom(
|
const userHasMasterPassword = await firstValueFrom(
|
||||||
this.userDecryptionOptionsService.hasMasterPassword$,
|
this.userDecryptionOptionsService.hasMasterPasswordById$(userId),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!userHasMasterPassword) {
|
if (!userHasMasterPassword) {
|
||||||
await this.router.navigate(["/settings/security/two-factor"]);
|
await this.router.navigate(["/settings/security/two-factor"]);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -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 { Component, OnInit } from "@angular/core";
|
||||||
import { firstValueFrom, map } from "rxjs";
|
import { firstValueFrom, map } from "rxjs";
|
||||||
|
|
||||||
|
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
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 { 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 { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { ChangeKdfModule } from "../../../key-management/change-kdf/change-kdf.module";
|
import { ChangeKdfModule } from "../../../key-management/change-kdf/change-kdf.module";
|
||||||
@@ -23,20 +22,28 @@ export class SecurityKeysComponent implements OnInit {
|
|||||||
showChangeKdf = true;
|
showChangeKdf = true;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private userVerificationService: UserVerificationService,
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
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() {
|
async viewUserApiKey() {
|
||||||
const entityId = await firstValueFrom(
|
const entityId = await firstValueFrom(
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!entityId) {
|
||||||
|
throw new Error("Active account not found");
|
||||||
|
}
|
||||||
|
|
||||||
await ApiKeyComponent.open(this.dialogService, {
|
await ApiKeyComponent.open(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
keyType: "user",
|
keyType: "user",
|
||||||
@@ -55,6 +62,11 @@ export class SecurityKeysComponent implements OnInit {
|
|||||||
const entityId = await firstValueFrom(
|
const entityId = await firstValueFrom(
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!entityId) {
|
||||||
|
throw new Error("Active account not found");
|
||||||
|
}
|
||||||
|
|
||||||
await ApiKeyComponent.open(this.dialogService, {
|
await ApiKeyComponent.open(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
keyType: "user",
|
keyType: "user",
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
|
|
||||||
@@ -20,7 +22,8 @@ export class SecurityComponent implements OnInit {
|
|||||||
consolidatedSessionTimeoutComponent$: Observable<boolean>;
|
consolidatedSessionTimeoutComponent$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private userVerificationService: UserVerificationService,
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
|
private accountService: AccountService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
this.consolidatedSessionTimeoutComponent$ = this.configService.getFeatureFlag$(
|
this.consolidatedSessionTimeoutComponent$ = this.configService.getFeatureFlag$(
|
||||||
@@ -29,6 +32,9 @@ export class SecurityComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,10 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
|
|||||||
combineLatest([
|
combineLatest([
|
||||||
this.organization$,
|
this.organization$,
|
||||||
resetPasswordPolicies$,
|
resetPasswordPolicies$,
|
||||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
this.accountService.activeAccount$.pipe(
|
||||||
|
getUserId,
|
||||||
|
switchMap((userId) => this.userDecryptionOptionsService.userDecryptionOptionsById$(userId)),
|
||||||
|
),
|
||||||
managingOrg$,
|
managingOrg$,
|
||||||
])
|
])
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
|||||||
@@ -198,10 +198,13 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
|||||||
userId: UserId,
|
userId: UserId,
|
||||||
) {
|
) {
|
||||||
const userDecryptionOpts = await firstValueFrom(
|
const userDecryptionOpts = await firstValueFrom(
|
||||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
|
||||||
);
|
);
|
||||||
userDecryptionOpts.hasMasterPassword = true;
|
userDecryptionOpts.hasMasterPassword = true;
|
||||||
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
|
await this.userDecryptionOptionsService.setUserDecryptionOptionsById(
|
||||||
|
userId,
|
||||||
|
userDecryptionOpts,
|
||||||
|
);
|
||||||
await this.kdfConfigService.setKdfConfig(userId, kdfConfig);
|
await this.kdfConfigService.setKdfConfig(userId, kdfConfig);
|
||||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||||
await this.keyService.setUserKey(masterKeyEncryptedUserKey[0], userId);
|
await this.keyService.setUserKey(masterKeyEncryptedUserKey[0], userId);
|
||||||
|
|||||||
@@ -149,7 +149,9 @@ describe("DefaultSetInitialPasswordService", () => {
|
|||||||
|
|
||||||
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
|
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
|
||||||
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
|
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
|
||||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||||
|
userDecryptionOptionsSubject,
|
||||||
|
);
|
||||||
|
|
||||||
setPasswordRequest = new SetPasswordRequest(
|
setPasswordRequest = new SetPasswordRequest(
|
||||||
credentials.newServerMasterKeyHash,
|
credentials.newServerMasterKeyHash,
|
||||||
@@ -362,7 +364,8 @@ describe("DefaultSetInitialPasswordService", () => {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
|
expect(userDecryptionOptionsService.setUserDecryptionOptionsById).toHaveBeenCalledWith(
|
||||||
|
userId,
|
||||||
userDecryptionOptions,
|
userDecryptionOptions,
|
||||||
);
|
);
|
||||||
expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig);
|
expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig);
|
||||||
@@ -560,7 +563,8 @@ describe("DefaultSetInitialPasswordService", () => {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
|
expect(userDecryptionOptionsService.setUserDecryptionOptionsById).toHaveBeenCalledWith(
|
||||||
|
userId,
|
||||||
userDecryptionOptions,
|
userDecryptionOptions,
|
||||||
);
|
);
|
||||||
expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig);
|
expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig);
|
||||||
|
|||||||
@@ -684,7 +684,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: InternalUserDecryptionOptionsServiceAbstraction,
|
provide: InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
useClass: UserDecryptionOptionsService,
|
useClass: UserDecryptionOptionsService,
|
||||||
deps: [StateProvider],
|
deps: [SingleUserStateProvider],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: UserDecryptionOptionsServiceAbstraction,
|
provide: UserDecryptionOptionsServiceAbstraction,
|
||||||
@@ -1292,6 +1292,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
UserDecryptionOptionsServiceAbstraction,
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
LogService,
|
LogService,
|
||||||
ConfigService,
|
ConfigService,
|
||||||
|
AccountServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export class LoginDecryptionOptionsComponent implements OnInit {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const userDecryptionOptions = await firstValueFrom(
|
const userDecryptionOptions = await firstValueFrom(
|
||||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
this.userDecryptionOptionsService.userDecryptionOptionsById$(this.activeAccountId),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -460,7 +460,7 @@ export class SsoComponent implements OnInit {
|
|||||||
|
|
||||||
// must come after 2fa check since user decryption options aren't available if 2fa is required
|
// must come after 2fa check since user decryption options aren't available if 2fa is required
|
||||||
const userDecryptionOpts = await firstValueFrom(
|
const userDecryptionOpts = await firstValueFrom(
|
||||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
this.userDecryptionOptionsService.userDecryptionOptionsById$(authResult.userId),
|
||||||
);
|
);
|
||||||
|
|
||||||
const tdeEnabled = userDecryptionOpts.trustedDeviceOption
|
const tdeEnabled = userDecryptionOpts.trustedDeviceOption
|
||||||
|
|||||||
@@ -176,7 +176,9 @@ describe("TwoFactorAuthComponent", () => {
|
|||||||
selectedUserDecryptionOptions = new BehaviorSubject<UserDecryptionOptions>(
|
selectedUserDecryptionOptions = new BehaviorSubject<UserDecryptionOptions>(
|
||||||
mockUserDecryptionOpts.withMasterPassword,
|
mockUserDecryptionOpts.withMasterPassword,
|
||||||
);
|
);
|
||||||
mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions;
|
mockUserDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||||
|
selectedUserDecryptionOptions,
|
||||||
|
);
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [TestTwoFactorComponent],
|
declarations: [TestTwoFactorComponent],
|
||||||
|
|||||||
@@ -473,7 +473,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userDecryptionOpts = await firstValueFrom(
|
const userDecryptionOpts = await firstValueFrom(
|
||||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
this.userDecryptionOptionsService.userDecryptionOptionsById$(authResult.userId),
|
||||||
);
|
);
|
||||||
|
|
||||||
const tdeEnabled = await this.isTrustedDeviceEncEnabled(userDecryptionOpts.trustedDeviceOption);
|
const tdeEnabled = await this.isTrustedDeviceEncEnabled(userDecryptionOpts.trustedDeviceOption);
|
||||||
|
|||||||
@@ -1,34 +1,45 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { UserDecryptionOptions } from "../models";
|
import { UserDecryptionOptions } from "../models";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public service for reading user decryption options.
|
||||||
|
* For use in components and services that need to evaluate user decryption settings.
|
||||||
|
*/
|
||||||
export abstract class UserDecryptionOptionsServiceAbstraction {
|
export abstract class UserDecryptionOptionsServiceAbstraction {
|
||||||
/**
|
/**
|
||||||
* Returns what decryption options are available for the current user.
|
* Returns the user decryption options for the given user id.
|
||||||
* @remark This is sent from the server on authentication.
|
* Will only emit when options are set (does not emit null/undefined
|
||||||
|
* for an unpopulated state), and should not be called in an unauthenticated context.
|
||||||
|
* @param userId The user id to check.
|
||||||
*/
|
*/
|
||||||
abstract userDecryptionOptions$: Observable<UserDecryptionOptions>;
|
abstract userDecryptionOptionsById$(userId: UserId): Observable<UserDecryptionOptions>;
|
||||||
/**
|
/**
|
||||||
* Uses user decryption options to determine if current user has a master password.
|
* Uses user decryption options to determine if current user has a master password.
|
||||||
* @remark This is sent from the server, and does not indicate if the master password
|
* @remark This is sent from the server, and does not indicate if the master password
|
||||||
* was used to login and/or if a master key is saved locally.
|
* was used to login and/or if a master key is saved locally.
|
||||||
*/
|
*/
|
||||||
abstract hasMasterPassword$: Observable<boolean>;
|
abstract hasMasterPasswordById$(userId: UserId): Observable<boolean>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the user decryption options for the given user id.
|
|
||||||
* @param userId The user id to check.
|
|
||||||
*/
|
|
||||||
abstract userDecryptionOptionsById$(userId: string): Observable<UserDecryptionOptions>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal service for managing user decryption options.
|
||||||
|
* For use only in authentication flows that need to update decryption options
|
||||||
|
* (e.g., login strategies). Extends consumer methods from {@link UserDecryptionOptionsServiceAbstraction}.
|
||||||
|
* @remarks Most consumers should use UserDecryptionOptionsServiceAbstraction instead.
|
||||||
|
*/
|
||||||
export abstract class InternalUserDecryptionOptionsServiceAbstraction extends UserDecryptionOptionsServiceAbstraction {
|
export abstract class InternalUserDecryptionOptionsServiceAbstraction extends UserDecryptionOptionsServiceAbstraction {
|
||||||
/**
|
/**
|
||||||
* Sets the current decryption options for the user, contains the current configuration
|
* Sets the current decryption options for the user. Contains the current configuration
|
||||||
* of the users account related to how they can decrypt their vault.
|
* of the users account related to how they can decrypt their vault.
|
||||||
* @remark Intended to be used when user decryption options are received from server, does
|
* @remark Intended to be used when user decryption options are received from server, does
|
||||||
* not update the server. Consider syncing instead of updating locally.
|
* not update the server. Consider syncing instead of updating locally.
|
||||||
* @param userDecryptionOptions Current user decryption options received from server.
|
* @param userDecryptionOptions Current user decryption options received from server.
|
||||||
*/
|
*/
|
||||||
abstract setUserDecryptionOptions(userDecryptionOptions: UserDecryptionOptions): Promise<void>;
|
abstract setUserDecryptionOptionsById(
|
||||||
|
userId: UserId,
|
||||||
|
userDecryptionOptions: UserDecryptionOptions,
|
||||||
|
): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,7 +257,8 @@ describe("LoginStrategy", () => {
|
|||||||
|
|
||||||
expect(environmentService.seedUserEnvironment).toHaveBeenCalled();
|
expect(environmentService.seedUserEnvironment).toHaveBeenCalled();
|
||||||
|
|
||||||
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
|
expect(userDecryptionOptionsService.setUserDecryptionOptionsById).toHaveBeenCalledWith(
|
||||||
|
userId,
|
||||||
UserDecryptionOptions.fromResponse(idTokenResponse),
|
UserDecryptionOptions.fromResponse(idTokenResponse),
|
||||||
);
|
);
|
||||||
expect(masterPasswordService.mock.setMasterPasswordUnlockData).toHaveBeenCalledWith(
|
expect(masterPasswordService.mock.setMasterPasswordUnlockData).toHaveBeenCalledWith(
|
||||||
|
|||||||
@@ -195,7 +195,8 @@ export abstract class LoginStrategy {
|
|||||||
|
|
||||||
// We must set user decryption options before retrieving vault timeout settings
|
// We must set user decryption options before retrieving vault timeout settings
|
||||||
// as the user decryption options help determine the available timeout actions.
|
// as the user decryption options help determine the available timeout actions.
|
||||||
await this.userDecryptionOptionsService.setUserDecryptionOptions(
|
await this.userDecryptionOptionsService.setUserDecryptionOptionsById(
|
||||||
|
userId,
|
||||||
UserDecryptionOptions.fromResponse(tokenResponse),
|
UserDecryptionOptions.fromResponse(tokenResponse),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,9 @@ describe("SsoLoginStrategy", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const userDecryptionOptions = new UserDecryptionOptions();
|
const userDecryptionOptions = new UserDecryptionOptions();
|
||||||
userDecryptionOptionsService.userDecryptionOptions$ = of(userDecryptionOptions);
|
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||||
|
of(userDecryptionOptions),
|
||||||
|
);
|
||||||
|
|
||||||
ssoLoginStrategy = new SsoLoginStrategy(
|
ssoLoginStrategy = new SsoLoginStrategy(
|
||||||
{} as SsoLoginStrategyData,
|
{} as SsoLoginStrategyData,
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||||||
|
|
||||||
// Check for TDE-related conditions
|
// Check for TDE-related conditions
|
||||||
const userDecryptionOptions = await firstValueFrom(
|
const userDecryptionOptions = await firstValueFrom(
|
||||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!userDecryptionOptions) {
|
if (!userDecryptionOptions) {
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { FakeSingleUserStateProvider } from "@bitwarden/common/spec";
|
||||||
import {
|
|
||||||
FakeAccountService,
|
|
||||||
FakeStateProvider,
|
|
||||||
mockAccountServiceWith,
|
|
||||||
} from "@bitwarden/common/spec";
|
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { newGuid } from "@bitwarden/guid";
|
||||||
|
|
||||||
import { UserDecryptionOptions } from "../../models/domain/user-decryption-options";
|
import { UserDecryptionOptions } from "../../models/domain/user-decryption-options";
|
||||||
|
|
||||||
@@ -17,15 +13,10 @@ import {
|
|||||||
|
|
||||||
describe("UserDecryptionOptionsService", () => {
|
describe("UserDecryptionOptionsService", () => {
|
||||||
let sut: UserDecryptionOptionsService;
|
let sut: UserDecryptionOptionsService;
|
||||||
|
let fakeStateProvider: FakeSingleUserStateProvider;
|
||||||
const fakeUserId = Utils.newGuid() as UserId;
|
|
||||||
let fakeAccountService: FakeAccountService;
|
|
||||||
let fakeStateProvider: FakeStateProvider;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fakeAccountService = mockAccountServiceWith(fakeUserId);
|
fakeStateProvider = new FakeSingleUserStateProvider();
|
||||||
fakeStateProvider = new FakeStateProvider(fakeAccountService);
|
|
||||||
|
|
||||||
sut = new UserDecryptionOptionsService(fakeStateProvider);
|
sut = new UserDecryptionOptionsService(fakeStateProvider);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -42,55 +33,71 @@ describe("UserDecryptionOptionsService", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("userDecryptionOptions$", () => {
|
describe("userDecryptionOptionsById$", () => {
|
||||||
it("should return the active user's decryption options", async () => {
|
it("should return user decryption options for a specific user", async () => {
|
||||||
await fakeStateProvider.setUserState(USER_DECRYPTION_OPTIONS, userDecryptionOptions);
|
const userId = newGuid() as UserId;
|
||||||
|
|
||||||
const result = await firstValueFrom(sut.userDecryptionOptions$);
|
fakeStateProvider.getFake(userId, USER_DECRYPTION_OPTIONS).nextState(userDecryptionOptions);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(sut.userDecryptionOptionsById$(userId));
|
||||||
|
|
||||||
expect(result).toEqual(userDecryptionOptions);
|
expect(result).toEqual(userDecryptionOptions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("hasMasterPassword$", () => {
|
describe("hasMasterPasswordById$", () => {
|
||||||
it("should return the hasMasterPassword property of the active user's decryption options", async () => {
|
it("should return true when user has a master password", async () => {
|
||||||
await fakeStateProvider.setUserState(USER_DECRYPTION_OPTIONS, userDecryptionOptions);
|
const userId = newGuid() as UserId;
|
||||||
|
|
||||||
const result = await firstValueFrom(sut.hasMasterPassword$);
|
fakeStateProvider.getFake(userId, USER_DECRYPTION_OPTIONS).nextState(userDecryptionOptions);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(sut.hasMasterPasswordById$(userId));
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe("userDecryptionOptionsById$", () => {
|
it("should return false when user does not have a master password", async () => {
|
||||||
it("should return the user decryption options for the given user", async () => {
|
const userId = newGuid() as UserId;
|
||||||
const givenUser = Utils.newGuid() as UserId;
|
const optionsWithoutMasterPassword = {
|
||||||
await fakeAccountService.addAccount(givenUser, {
|
...userDecryptionOptions,
|
||||||
name: "Test User 1",
|
hasMasterPassword: false,
|
||||||
email: "test1@email.com",
|
};
|
||||||
emailVerified: false,
|
|
||||||
});
|
|
||||||
await fakeStateProvider.setUserState(
|
|
||||||
USER_DECRYPTION_OPTIONS,
|
|
||||||
userDecryptionOptions,
|
|
||||||
givenUser,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await firstValueFrom(sut.userDecryptionOptionsById$(givenUser));
|
fakeStateProvider
|
||||||
|
.getFake(userId, USER_DECRYPTION_OPTIONS)
|
||||||
|
.nextState(optionsWithoutMasterPassword);
|
||||||
|
|
||||||
expect(result).toEqual(userDecryptionOptions);
|
const result = await firstValueFrom(sut.hasMasterPasswordById$(userId));
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("setUserDecryptionOptions", () => {
|
describe("setUserDecryptionOptionsById", () => {
|
||||||
it("should set the active user's decryption options", async () => {
|
it("should set user decryption options for a specific user", async () => {
|
||||||
await sut.setUserDecryptionOptions(userDecryptionOptions);
|
const userId = newGuid() as UserId;
|
||||||
|
|
||||||
const result = await firstValueFrom(
|
await sut.setUserDecryptionOptionsById(userId, userDecryptionOptions);
|
||||||
fakeStateProvider.getActive(USER_DECRYPTION_OPTIONS).state$,
|
|
||||||
);
|
const fakeState = fakeStateProvider.getFake(userId, USER_DECRYPTION_OPTIONS);
|
||||||
|
const result = await firstValueFrom(fakeState.state$);
|
||||||
|
|
||||||
expect(result).toEqual(userDecryptionOptions);
|
expect(result).toEqual(userDecryptionOptions);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should overwrite existing user decryption options", async () => {
|
||||||
|
const userId = newGuid() as UserId;
|
||||||
|
const initialOptions = { ...userDecryptionOptions, hasMasterPassword: false };
|
||||||
|
const updatedOptions = { ...userDecryptionOptions, hasMasterPassword: true };
|
||||||
|
|
||||||
|
const fakeState = fakeStateProvider.getFake(userId, USER_DECRYPTION_OPTIONS);
|
||||||
|
fakeState.nextState(initialOptions);
|
||||||
|
|
||||||
|
await sut.setUserDecryptionOptionsById(userId, updatedOptions);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(fakeState.state$);
|
||||||
|
|
||||||
|
expect(result).toEqual(updatedOptions);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
import { Observable, filter, map } from "rxjs";
|
||||||
// @ts-strict-ignore
|
|
||||||
import { Observable, map } from "rxjs";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ActiveUserState,
|
SingleUserStateProvider,
|
||||||
StateProvider,
|
|
||||||
USER_DECRYPTION_OPTIONS_DISK,
|
USER_DECRYPTION_OPTIONS_DISK,
|
||||||
UserKeyDefinition,
|
UserKeyDefinition,
|
||||||
} from "@bitwarden/common/platform/state";
|
} from "@bitwarden/common/platform/state";
|
||||||
// FIXME: remove `src` and fix import
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
// eslint-disable-next-line no-restricted-imports
|
|
||||||
import { UserId } from "@bitwarden/common/src/types/guid";
|
|
||||||
|
|
||||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction";
|
import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction";
|
||||||
import { UserDecryptionOptions } from "../../models";
|
import { UserDecryptionOptions } from "../../models";
|
||||||
@@ -27,25 +22,26 @@ export const USER_DECRYPTION_OPTIONS = new UserKeyDefinition<UserDecryptionOptio
|
|||||||
export class UserDecryptionOptionsService
|
export class UserDecryptionOptionsService
|
||||||
implements InternalUserDecryptionOptionsServiceAbstraction
|
implements InternalUserDecryptionOptionsServiceAbstraction
|
||||||
{
|
{
|
||||||
private userDecryptionOptionsState: ActiveUserState<UserDecryptionOptions>;
|
constructor(private singleUserStateProvider: SingleUserStateProvider) {}
|
||||||
|
|
||||||
userDecryptionOptions$: Observable<UserDecryptionOptions>;
|
userDecryptionOptionsById$(userId: UserId): Observable<UserDecryptionOptions> {
|
||||||
hasMasterPassword$: Observable<boolean>;
|
return this.singleUserStateProvider
|
||||||
|
.get(userId, USER_DECRYPTION_OPTIONS)
|
||||||
|
.state$.pipe(filter((options): options is UserDecryptionOptions => options != null));
|
||||||
|
}
|
||||||
|
|
||||||
constructor(private stateProvider: StateProvider) {
|
hasMasterPasswordById$(userId: UserId): Observable<boolean> {
|
||||||
this.userDecryptionOptionsState = this.stateProvider.getActive(USER_DECRYPTION_OPTIONS);
|
return this.userDecryptionOptionsById$(userId).pipe(
|
||||||
|
map((options) => options.hasMasterPassword ?? false),
|
||||||
this.userDecryptionOptions$ = this.userDecryptionOptionsState.state$;
|
|
||||||
this.hasMasterPassword$ = this.userDecryptionOptions$.pipe(
|
|
||||||
map((options) => options?.hasMasterPassword ?? false),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
userDecryptionOptionsById$(userId: UserId): Observable<UserDecryptionOptions> {
|
async setUserDecryptionOptionsById(
|
||||||
return this.stateProvider.getUser(userId, USER_DECRYPTION_OPTIONS).state$;
|
userId: UserId,
|
||||||
}
|
userDecryptionOptions: UserDecryptionOptions,
|
||||||
|
): Promise<void> {
|
||||||
async setUserDecryptionOptions(userDecryptionOptions: UserDecryptionOptions): Promise<void> {
|
await this.singleUserStateProvider
|
||||||
await this.userDecryptionOptionsState.update((_) => userDecryptionOptions);
|
.get(userId, USER_DECRYPTION_OPTIONS)
|
||||||
|
.update((_) => userDecryptionOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ export abstract class UserVerificationService {
|
|||||||
* @param userId The user id to check. If not provided, the current user is used
|
* @param userId The user id to check. If not provided, the current user is used
|
||||||
* @returns True if the user has a master password
|
* @returns True if the user has a master password
|
||||||
* @deprecated Use UserDecryptionOptionsService.hasMasterPassword$ instead
|
* @deprecated Use UserDecryptionOptionsService.hasMasterPassword$ instead
|
||||||
|
* @remark To facilitate deprecation, many call sites were removed as part of PM-26413.
|
||||||
|
* Those remaining are blocked by currently-disallowed imports of auth/common.
|
||||||
|
* PM-27009 has been filed to track completion of this deprecation.
|
||||||
*/
|
*/
|
||||||
abstract hasMasterPassword(userId?: string): Promise<boolean>;
|
abstract hasMasterPassword(userId?: string): Promise<boolean>;
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ import { of } from "rxjs";
|
|||||||
|
|
||||||
// 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 {
|
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
UserDecryptionOptions,
|
|
||||||
UserDecryptionOptionsServiceAbstraction,
|
|
||||||
} from "@bitwarden/auth/common";
|
|
||||||
// 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 {
|
import {
|
||||||
@@ -146,11 +143,7 @@ describe("UserVerificationService", () => {
|
|||||||
|
|
||||||
describe("server verification type", () => {
|
describe("server verification type", () => {
|
||||||
it("correctly returns master password availability", async () => {
|
it("correctly returns master password availability", async () => {
|
||||||
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(true));
|
||||||
of({
|
|
||||||
hasMasterPassword: true,
|
|
||||||
} as UserDecryptionOptions),
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await sut.getAvailableVerificationOptions("server");
|
const result = await sut.getAvailableVerificationOptions("server");
|
||||||
|
|
||||||
@@ -168,11 +161,7 @@ describe("UserVerificationService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("correctly returns OTP availability", async () => {
|
it("correctly returns OTP availability", async () => {
|
||||||
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
|
||||||
of({
|
|
||||||
hasMasterPassword: false,
|
|
||||||
} as UserDecryptionOptions),
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await sut.getAvailableVerificationOptions("server");
|
const result = await sut.getAvailableVerificationOptions("server");
|
||||||
|
|
||||||
@@ -526,11 +515,7 @@ describe("UserVerificationService", () => {
|
|||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
function setMasterPasswordAvailability(hasMasterPassword: boolean) {
|
function setMasterPasswordAvailability(hasMasterPassword: boolean) {
|
||||||
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(hasMasterPassword));
|
||||||
of({
|
|
||||||
hasMasterPassword: hasMasterPassword,
|
|
||||||
} as UserDecryptionOptions),
|
|
||||||
);
|
|
||||||
masterPasswordService.masterKeyHash$.mockReturnValue(
|
masterPasswordService.masterKeyHash$.mockReturnValue(
|
||||||
of(hasMasterPassword ? "masterKeyHash" : null),
|
of(hasMasterPassword ? "masterKeyHash" : null),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -258,16 +258,19 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
|||||||
}
|
}
|
||||||
|
|
||||||
async hasMasterPassword(userId?: string): Promise<boolean> {
|
async hasMasterPassword(userId?: string): Promise<boolean> {
|
||||||
if (userId) {
|
const resolvedUserId = userId ?? (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
const decryptionOptions = await firstValueFrom(
|
|
||||||
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (decryptionOptions?.hasMasterPassword != undefined) {
|
if (!resolvedUserId) {
|
||||||
return decryptionOptions.hasMasterPassword;
|
return false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return await firstValueFrom(this.userDecryptionOptionsService.hasMasterPassword$);
|
|
||||||
|
// Ideally, this method would accept a UserId over string. To avoid scope creep in PM-26413, we are
|
||||||
|
// doing the cast here. Future work should be done to make this type-safe, and should be considered
|
||||||
|
// as part of PM-27009.
|
||||||
|
|
||||||
|
return await firstValueFrom(
|
||||||
|
this.userDecryptionOptionsService.hasMasterPasswordById$(resolvedUserId as UserId),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasMasterPasswordAndMasterKeyHash(userId?: string): Promise<boolean> {
|
async hasMasterPasswordAndMasterKeyHash(userId?: string): Promise<boolean> {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { firstValueFrom, map, Observable, Subject } from "rxjs";
|
import { firstValueFrom, map, Observable, Subject, switchMap } from "rxjs";
|
||||||
|
|
||||||
// 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
|
||||||
@@ -9,6 +9,7 @@ import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"
|
|||||||
// 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 { AccountService } from "../../../auth/abstractions/account.service";
|
||||||
import { DeviceResponse } from "../../../auth/abstractions/devices/responses/device.response";
|
import { DeviceResponse } from "../../../auth/abstractions/devices/responses/device.response";
|
||||||
import { DevicesApiServiceAbstraction } from "../../../auth/abstractions/devices-api.service.abstraction";
|
import { DevicesApiServiceAbstraction } from "../../../auth/abstractions/devices-api.service.abstraction";
|
||||||
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
|
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
|
||||||
@@ -87,10 +88,18 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
|
|||||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
|
private accountService: AccountService,
|
||||||
) {
|
) {
|
||||||
this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe(
|
this.supportsDeviceTrust$ = this.accountService.activeAccount$.pipe(
|
||||||
map((options) => {
|
switchMap((account) => {
|
||||||
return options?.trustedDeviceOption != null;
|
if (account == null) {
|
||||||
|
return [false];
|
||||||
|
}
|
||||||
|
return this.userDecryptionOptionsService.userDecryptionOptionsById$(account.id).pipe(
|
||||||
|
map((options) => {
|
||||||
|
return options?.trustedDeviceOption != null;
|
||||||
|
}),
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -914,7 +914,7 @@ describe("deviceTrustService", () => {
|
|||||||
platformUtilsService.supportsSecureStorage.mockReturnValue(supportsSecureStorage);
|
platformUtilsService.supportsSecureStorage.mockReturnValue(supportsSecureStorage);
|
||||||
|
|
||||||
decryptionOptions.next({} as any);
|
decryptionOptions.next({} as any);
|
||||||
userDecryptionOptionsService.userDecryptionOptions$ = decryptionOptions;
|
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(decryptionOptions);
|
||||||
|
|
||||||
return new DeviceTrustService(
|
return new DeviceTrustService(
|
||||||
keyGenerationService,
|
keyGenerationService,
|
||||||
@@ -930,6 +930,7 @@ describe("deviceTrustService", () => {
|
|||||||
userDecryptionOptionsService,
|
userDecryptionOptionsService,
|
||||||
logService,
|
logService,
|
||||||
configService,
|
configService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,9 +53,11 @@ describe("VaultTimeoutSettingsService", () => {
|
|||||||
policyService = mock<PolicyService>();
|
policyService = mock<PolicyService>();
|
||||||
|
|
||||||
userDecryptionOptionsSubject = new BehaviorSubject(null);
|
userDecryptionOptionsSubject = new BehaviorSubject(null);
|
||||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||||
userDecryptionOptionsService.hasMasterPassword$ = userDecryptionOptionsSubject.pipe(
|
userDecryptionOptionsSubject,
|
||||||
map((options) => options?.hasMasterPassword ?? false),
|
);
|
||||||
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(
|
||||||
|
userDecryptionOptionsSubject.pipe(map((options) => options?.hasMasterPassword ?? false)),
|
||||||
);
|
);
|
||||||
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||||
userDecryptionOptionsSubject,
|
userDecryptionOptionsSubject,
|
||||||
@@ -127,6 +129,23 @@ describe("VaultTimeoutSettingsService", () => {
|
|||||||
|
|
||||||
expect(result).not.toContain(VaultTimeoutAction.Lock);
|
expect(result).not.toContain(VaultTimeoutAction.Lock);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should return only LogOut when userId is not provided and there is no active account", async () => {
|
||||||
|
// Set up accountService to return null for activeAccount
|
||||||
|
accountService.activeAccount$ = of(null);
|
||||||
|
pinStateService.isPinSet.mockResolvedValue(false);
|
||||||
|
biometricStateService.biometricUnlockEnabled$ = of(false);
|
||||||
|
|
||||||
|
// Call availableVaultTimeoutActions$ which internally calls userHasMasterPassword without a userId
|
||||||
|
const result = await firstValueFrom(
|
||||||
|
vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Since there's no active account, userHasMasterPassword returns false,
|
||||||
|
// meaning no master password is available, so Lock should not be available
|
||||||
|
expect(result).toEqual([VaultTimeoutAction.LogOut]);
|
||||||
|
expect(result).not.toContain(VaultTimeoutAction.Lock);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("canLock", () => {
|
describe("canLock", () => {
|
||||||
|
|||||||
@@ -290,14 +290,19 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async userHasMasterPassword(userId: string): Promise<boolean> {
|
private async userHasMasterPassword(userId: string): Promise<boolean> {
|
||||||
|
let resolvedUserId: UserId;
|
||||||
if (userId) {
|
if (userId) {
|
||||||
const decryptionOptions = await firstValueFrom(
|
resolvedUserId = userId as UserId;
|
||||||
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
|
|
||||||
);
|
|
||||||
|
|
||||||
return !!decryptionOptions?.hasMasterPassword;
|
|
||||||
} else {
|
} else {
|
||||||
return await firstValueFrom(this.userDecryptionOptionsService.hasMasterPassword$);
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
|
if (!activeAccount) {
|
||||||
|
return false; // No account, can't have master password
|
||||||
|
}
|
||||||
|
resolvedUserId = activeAccount.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await firstValueFrom(
|
||||||
|
this.userDecryptionOptionsService.hasMasterPasswordById$(resolvedUserId),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user