diff --git a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts index c414300431..83ebcaa11e 100644 --- a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts @@ -4,6 +4,10 @@ import { policyServiceFactory, PolicyServiceInitOptions, } from "../../../admin-console/background/service-factories/policy-service.factory"; +import { + vaultTimeoutSettingsServiceFactory, + VaultTimeoutSettingsServiceInitOptions, +} from "../../../background/service-factories/vault-timeout-settings-service.factory"; import { apiServiceFactory, ApiServiceInitOptions, @@ -108,6 +112,7 @@ export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions UserDecryptionOptionsServiceInitOptions & GlobalStateProviderInitOptions & BillingAccountProfileStateServiceInitOptions & + VaultTimeoutSettingsServiceInitOptions & KdfConfigServiceInitOptions; export function loginStrategyServiceFactory( @@ -142,6 +147,7 @@ export function loginStrategyServiceFactory( await internalUserDecryptionOptionServiceFactory(cache, opts), await globalStateProviderFactory(cache, opts), await billingAccountProfileStateServiceFactory(cache, opts), + await vaultTimeoutSettingsServiceFactory(cache, opts), await kdfConfigServiceFactory(cache, opts), ), ); diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 51959ba03c..ea7c3a5e8d 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -31,6 +31,11 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { + VaultTimeout, + VaultTimeoutOption, + VaultTimeoutStringType, +} from "@bitwarden/common/types/vault-timeout.type"; import { DialogService } from "@bitwarden/components"; import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; @@ -50,7 +55,7 @@ export class AccountSecurityComponent implements OnInit { protected readonly VaultTimeoutAction = VaultTimeoutAction; availableVaultTimeoutActions: VaultTimeoutAction[] = []; - vaultTimeoutOptions: any[]; + vaultTimeoutOptions: VaultTimeoutOption[]; vaultTimeoutPolicyCallout: Observable<{ timeout: { hours: number; minutes: number }; action: VaultTimeoutAction; @@ -60,7 +65,7 @@ export class AccountSecurityComponent implements OnInit { accountSwitcherEnabled = false; form = this.formBuilder.group({ - vaultTimeout: [null as number | null], + vaultTimeout: [null as VaultTimeout | null], vaultTimeoutAction: [VaultTimeoutAction.Lock], pin: [null as boolean | null], biometric: false, @@ -118,20 +123,31 @@ export class AccountSecurityComponent implements OnInit { { name: this.i18nService.t("thirtyMinutes"), value: 30 }, { name: this.i18nService.t("oneHour"), value: 60 }, { name: this.i18nService.t("fourHours"), value: 240 }, - // { name: i18nService.t('onIdle'), value: -4 }, - // { name: i18nService.t('onSleep'), value: -3 }, ]; if (showOnLocked) { - this.vaultTimeoutOptions.push({ name: this.i18nService.t("onLocked"), value: -2 }); + this.vaultTimeoutOptions.push({ + name: this.i18nService.t("onLocked"), + value: VaultTimeoutStringType.OnLocked, + }); } - this.vaultTimeoutOptions.push({ name: this.i18nService.t("onRestart"), value: -1 }); - this.vaultTimeoutOptions.push({ name: this.i18nService.t("never"), value: null }); + this.vaultTimeoutOptions.push({ + name: this.i18nService.t("onRestart"), + value: VaultTimeoutStringType.OnRestart, + }); + this.vaultTimeoutOptions.push({ + name: this.i18nService.t("never"), + value: VaultTimeoutStringType.Never, + }); - let timeout = await this.vaultTimeoutSettingsService.getVaultTimeout(); - if (timeout === -2 && !showOnLocked) { - timeout = -1; + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + + let timeout = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(activeAccount.id), + ); + if (timeout === VaultTimeoutStringType.OnLocked && !showOnLocked) { + timeout = VaultTimeoutStringType.OnRestart; } this.form.controls.vaultTimeout.valueChanges @@ -159,7 +175,7 @@ export class AccountSecurityComponent implements OnInit { const initialValues = { vaultTimeout: timeout, vaultTimeoutAction: await firstValueFrom( - this.vaultTimeoutSettingsService.vaultTimeoutAction$(), + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id), ), pin: await this.pinService.isPinSet(userId), biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(), @@ -203,7 +219,7 @@ export class AccountSecurityComponent implements OnInit { switchMap(() => combineLatest([ this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(), - this.vaultTimeoutSettingsService.vaultTimeoutAction$(), + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id), ]), ), takeUntil(this.destroy$), @@ -237,7 +253,7 @@ export class AccountSecurityComponent implements OnInit { }); } - async saveVaultTimeout(previousValue: number, newValue: number) { + async saveVaultTimeout(previousValue: VaultTimeout, newValue: VaultTimeout) { if (newValue == null) { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "warning" }, @@ -262,9 +278,16 @@ export class AccountSecurityComponent implements OnInit { return; } + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + + const vaultTimeoutAction = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id), + ); + await this.vaultTimeoutSettingsService.setVaultTimeoutOptions( + activeAccount.id, newValue, - await firstValueFrom(this.vaultTimeoutSettingsService.vaultTimeoutAction$()), + vaultTimeoutAction, ); if (newValue == null) { this.messagingService.send("bgReseedStorage"); @@ -296,7 +319,10 @@ export class AccountSecurityComponent implements OnInit { return; } + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + await this.vaultTimeoutSettingsService.setVaultTimeoutOptions( + activeAccount.id, this.form.value.vaultTimeout, newValue, ); diff --git a/apps/browser/src/autofill/types/index.ts b/apps/browser/src/autofill/types/index.ts index 8ed893e733..a14ef1330c 100644 --- a/apps/browser/src/autofill/types/index.ts +++ b/apps/browser/src/autofill/types/index.ts @@ -1,5 +1,6 @@ import { Region } from "@bitwarden/common/platform/abstractions/environment.service"; import { VaultTimeoutAction } from "@bitwarden/common/src/enums/vault-timeout-action.enum"; +import { VaultTimeout } from "@bitwarden/common/types/vault-timeout.type"; import { CipherType } from "@bitwarden/common/vault/enums"; export type UserSettings = { @@ -31,7 +32,7 @@ export type UserSettings = { utcDate: string; version: string; }; - vaultTimeout: number; + vaultTimeout: VaultTimeout; vaultTimeoutAction: VaultTimeoutAction; }; diff --git a/apps/browser/src/background/idle.background.ts b/apps/browser/src/background/idle.background.ts index 7b273459ad..eef033b364 100644 --- a/apps/browser/src/background/idle.background.ts +++ b/apps/browser/src/background/idle.background.ts @@ -1,9 +1,11 @@ import { firstValueFrom } from "rxjs"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; @@ -19,6 +21,7 @@ export default class IdleBackground { private stateService: BrowserStateService, private notificationsService: NotificationsService, private accountService: AccountService, + private vaultTimeoutSettingsService: VaultTimeoutSettingsService, ) { this.idle = chrome.idle || (browser != null ? browser.idle : null); } @@ -54,10 +57,14 @@ export default class IdleBackground { const allUsers = await firstValueFrom(this.accountService.accounts$); for (const userId in allUsers) { // If the screen is locked or the screensaver activates - const timeout = await this.stateService.getVaultTimeout({ userId: userId }); - if (timeout === -2) { + const timeout = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId), + ); + if (timeout === VaultTimeoutStringType.OnLocked) { // On System Lock vault timeout option - const action = await this.stateService.getVaultTimeoutAction({ userId: userId }); + const action = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId), + ); if (action === VaultTimeoutAction.LogOut) { await this.vaultTimeoutService.logOut(userId); } else { diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 6120155334..004714258e 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -154,6 +154,7 @@ import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-st import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { InternalSendService as InternalSendServiceAbstraction } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; +import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService as CollectionServiceAbstraction } from "@bitwarden/common/vault/abstractions/collection.service"; import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } from "@bitwarden/common/vault/abstractions/fido2/fido2-authenticator.service.abstraction"; @@ -581,12 +582,30 @@ export default class MainBackground { ); this.appIdService = new AppIdService(this.globalStateProvider); + + this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); + this.organizationService = new OrganizationService(this.stateProvider); + this.policyService = new PolicyService(this.stateProvider, this.organizationService); + + this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( + this.accountService, + this.pinService, + this.userDecryptionOptionsService, + this.cryptoService, + this.tokenService, + this.policyService, + this.biometricStateService, + this.stateProvider, + this.logService, + VaultTimeoutStringType.OnRestart, // default vault timeout + ); + this.apiService = new ApiService( this.tokenService, this.platformUtilsService, this.environmentService, this.appIdService, - this.stateService, + this.vaultTimeoutSettingsService, (expired: boolean) => this.logout(expired), ); this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider); @@ -603,8 +622,7 @@ export default class MainBackground { this.stateProvider, ); this.syncNotifierService = new SyncNotifierService(); - this.organizationService = new OrganizationService(this.stateProvider); - this.policyService = new PolicyService(this.stateProvider, this.organizationService); + this.autofillSettingsService = new AutofillSettingsService( this.stateProvider, this.policyService, @@ -710,17 +728,6 @@ export default class MainBackground { ); this.folderApiService = new FolderApiService(this.folderService, this.apiService); - this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( - this.accountService, - this.pinService, - this.userDecryptionOptionsService, - this.cryptoService, - this.tokenService, - this.policyService, - this.stateService, - this.biometricStateService, - ); - this.userVerificationService = new UserVerificationService( this.stateService, this.cryptoService, @@ -1056,6 +1063,7 @@ export default class MainBackground { this.stateService, this.notificationsService, this.accountService, + this.vaultTimeoutSettingsService, ); this.usernameGenerationService = new UsernameGenerationService( @@ -1263,7 +1271,7 @@ export default class MainBackground { ]); //Needs to be checked before state is cleaned - const needStorageReseed = await this.needsStorageReseed(); + const needStorageReseed = await this.needsStorageReseed(userId); const newActiveUser = userBeingLoggedOut === activeUserId @@ -1307,9 +1315,11 @@ export default class MainBackground { await this.systemService.startProcessReload(this.authService); } - private async needsStorageReseed(): Promise { - const currentVaultTimeout = await this.stateService.getVaultTimeout(); - return currentVaultTimeout == null ? false : true; + private async needsStorageReseed(userId: UserId): Promise { + const currentVaultTimeout = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId), + ); + return currentVaultTimeout == VaultTimeoutStringType.Never ? false : true; } async collectPageDetailsForContentScript(tab: any, sender: string, frameId: number = null) { diff --git a/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts b/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts index 6c5ea63eba..5f98d9764c 100644 --- a/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts +++ b/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts @@ -1,5 +1,6 @@ import { VaultTimeoutSettingsService as AbstractVaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; +import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { policyServiceFactory, @@ -35,9 +36,13 @@ import { FactoryOptions, } from "../../platform/background/service-factories/factory-options"; import { - StateServiceInitOptions, - stateServiceFactory, -} from "../../platform/background/service-factories/state-service.factory"; + logServiceFactory, + LogServiceInitOptions, +} from "../../platform/background/service-factories/log-service.factory"; +import { + StateProviderInitOptions, + stateProviderFactory, +} from "../../platform/background/service-factories/state-provider.factory"; type VaultTimeoutSettingsServiceFactoryOptions = FactoryOptions; @@ -48,8 +53,9 @@ export type VaultTimeoutSettingsServiceInitOptions = VaultTimeoutSettingsService CryptoServiceInitOptions & TokenServiceInitOptions & PolicyServiceInitOptions & - StateServiceInitOptions & - BiometricStateServiceInitOptions; + BiometricStateServiceInitOptions & + StateProviderInitOptions & + LogServiceInitOptions; export function vaultTimeoutSettingsServiceFactory( cache: { vaultTimeoutSettingsService?: AbstractVaultTimeoutSettingsService } & CachedServices, @@ -67,8 +73,10 @@ export function vaultTimeoutSettingsServiceFactory( await cryptoServiceFactory(cache, opts), await tokenServiceFactory(cache, opts), await policyServiceFactory(cache, opts), - await stateServiceFactory(cache, opts), await biometricStateServiceFactory(cache, opts), + await stateProviderFactory(cache, opts), + await logServiceFactory(cache, opts), + VaultTimeoutStringType.OnRestart, // default vault timeout ), ); } diff --git a/apps/browser/src/models/account.ts b/apps/browser/src/models/account.ts index 57d7844fde..519f1bda6b 100644 --- a/apps/browser/src/models/account.ts +++ b/apps/browser/src/models/account.ts @@ -1,28 +1,12 @@ import { Jsonify } from "type-fest"; -import { - Account as BaseAccount, - AccountSettings as BaseAccountSettings, -} from "@bitwarden/common/platform/models/domain/account"; +import { Account as BaseAccount } from "@bitwarden/common/platform/models/domain/account"; import { BrowserComponentState } from "./browserComponentState"; import { BrowserGroupingsComponentState } from "./browserGroupingsComponentState"; import { BrowserSendComponentState } from "./browserSendComponentState"; -export class AccountSettings extends BaseAccountSettings { - vaultTimeout = -1; // On Restart - - static fromJSON(json: Jsonify): AccountSettings { - if (json == null) { - return null; - } - - return Object.assign(new AccountSettings(), json, super.fromJSON(json)); - } -} - export class Account extends BaseAccount { - settings?: AccountSettings = new AccountSettings(); groupings?: BrowserGroupingsComponentState; send?: BrowserSendComponentState; ciphers?: BrowserComponentState; @@ -30,10 +14,7 @@ export class Account extends BaseAccount { constructor(init: Partial) { super(init); - Object.assign(this.settings, { - ...new AccountSettings(), - ...this.settings, - }); + this.groupings = init?.groupings ?? new BrowserGroupingsComponentState(); this.send = init?.send ?? new BrowserSendComponentState(); this.ciphers = init?.ciphers ?? new BrowserComponentState(); @@ -46,7 +27,6 @@ export class Account extends BaseAccount { } return Object.assign(new Account({}), json, super.fromJSON(json), { - settings: AccountSettings.fromJSON(json.settings), groupings: BrowserGroupingsComponentState.fromJSON(json.groupings), send: BrowserSendComponentState.fromJSON(json.send), ciphers: BrowserComponentState.fromJSON(json.ciphers), diff --git a/apps/browser/src/platform/background/service-factories/api-service.factory.ts b/apps/browser/src/platform/background/service-factories/api-service.factory.ts index 57cd500441..bfae93f3d8 100644 --- a/apps/browser/src/platform/background/service-factories/api-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/api-service.factory.ts @@ -5,6 +5,10 @@ import { tokenServiceFactory, TokenServiceInitOptions, } from "../../../auth/background/service-factories/token-service.factory"; +import { + vaultTimeoutSettingsServiceFactory, + VaultTimeoutSettingsServiceInitOptions, +} from "../../../background/service-factories/vault-timeout-settings-service.factory"; import { CachedServices, factory, @@ -20,7 +24,6 @@ import { PlatformUtilsServiceInitOptions, platformUtilsServiceFactory, } from "./platform-utils-service.factory"; -import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory"; type ApiServiceFactoryOptions = FactoryOptions & { apiServiceOptions: { @@ -34,7 +37,7 @@ export type ApiServiceInitOptions = ApiServiceFactoryOptions & PlatformUtilsServiceInitOptions & EnvironmentServiceInitOptions & AppIdServiceInitOptions & - StateServiceInitOptions; + VaultTimeoutSettingsServiceInitOptions; export function apiServiceFactory( cache: { apiService?: AbstractApiService } & CachedServices, @@ -50,7 +53,7 @@ export function apiServiceFactory( await platformUtilsServiceFactory(cache, opts), await environmentServiceFactory(cache, opts), await appIdServiceFactory(cache, opts), - await stateServiceFactory(cache, opts), + await vaultTimeoutSettingsServiceFactory(cache, opts), opts.apiServiceOptions.logoutCallback, opts.apiServiceOptions.customUserAgent, ), diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 2313833ab8..0b9c8f6fe6 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -12,6 +12,7 @@ import { OBSERVABLE_MEMORY_STORAGE, SYSTEM_THEME_OBSERVABLE, SafeInjectionToken, + DEFAULT_VAULT_TIMEOUT, INTRAPROCESS_MESSAGING_SUBJECT, CLIENT_TYPE, } from "@bitwarden/angular/services/injection-tokens"; @@ -82,6 +83,7 @@ import { import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/inline-derived-state"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; +import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -161,6 +163,10 @@ const safeProviders: SafeProvider[] = [ safeProvider(DebounceNavigationService), safeProvider(DialogService), safeProvider(PopupCloseWarningService), + safeProvider({ + provide: DEFAULT_VAULT_TIMEOUT, + useValue: VaultTimeoutStringType.OnRestart, + }), safeProvider({ provide: APP_INITIALIZER as SafeInjectionToken<() => Promise>, useFactory: (initService: InitService) => initService.init(), diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 2f5eefdbb2..cab38965f8 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -116,6 +116,7 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider"; import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { UserId } from "@bitwarden/common/types/guid"; +import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/services/collection.service"; @@ -403,12 +404,32 @@ export class Main { " (" + this.platformUtilsService.getDeviceString().toUpperCase() + ")"; + + this.biometricStateService = new DefaultBiometricStateService(this.stateProvider); + this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); + + this.organizationService = new OrganizationService(this.stateProvider); + this.policyService = new PolicyService(this.stateProvider, this.organizationService); + + this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( + this.accountService, + this.pinService, + this.userDecryptionOptionsService, + this.cryptoService, + this.tokenService, + this.policyService, + this.biometricStateService, + this.stateProvider, + this.logService, + VaultTimeoutStringType.Never, // default vault timeout + ); + this.apiService = new NodeApiService( this.tokenService, this.platformUtilsService, this.environmentService, this.appIdService, - this.stateService, + this.vaultTimeoutSettingsService, async (expired: boolean) => await this.logout(), customUserAgent, ); @@ -454,12 +475,8 @@ export class Main { this.providerService = new ProviderService(this.stateProvider); - this.organizationService = new OrganizationService(this.stateProvider); - this.organizationUserService = new OrganizationUserServiceImplementation(this.apiService); - this.policyService = new PolicyService(this.stateProvider, this.organizationService); - this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.keyConnectorService = new KeyConnectorService( @@ -489,8 +506,6 @@ export class Main { this.stateService, ); - this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); - this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); this.deviceTrustService = new DeviceTrustService( this.keyGenerationService, @@ -543,6 +558,7 @@ export class Main { this.userDecryptionOptionsService, this.globalStateProvider, this.billingAccountProfileStateService, + this.vaultTimeoutSettingsService, this.kdfConfigService, ); @@ -590,19 +606,6 @@ export class Main { const lockedCallback = async (userId?: string) => await this.cryptoService.clearStoredUserKey(KeySuffixOptions.Auto); - this.biometricStateService = new DefaultBiometricStateService(this.stateProvider); - - this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( - this.accountService, - this.pinService, - this.userDecryptionOptionsService, - this.cryptoService, - this.tokenService, - this.policyService, - this.stateService, - this.biometricStateService, - ); - this.userVerificationService = new UserVerificationService( this.stateService, this.cryptoService, diff --git a/apps/cli/src/platform/services/node-api.service.ts b/apps/cli/src/platform/services/node-api.service.ts index 9099fd2760..4849aef151 100644 --- a/apps/cli/src/platform/services/node-api.service.ts +++ b/apps/cli/src/platform/services/node-api.service.ts @@ -2,11 +2,11 @@ import * as FormData from "form-data"; import { HttpsProxyAgent } from "https-proxy-agent"; import * as fe from "node-fetch"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ApiService } from "@bitwarden/common/services/api.service"; (global as any).fetch = fe.default; @@ -21,7 +21,7 @@ export class NodeApiService extends ApiService { platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService, appIdService: AppIdService, - stateService: StateService, + vaultTimeoutSettingsService: VaultTimeoutSettingsService, logoutCallback: (expired: boolean) => Promise, customUserAgent: string = null, ) { @@ -30,7 +30,7 @@ export class NodeApiService extends ApiService { platformUtilsService, environmentService, appIdService, - stateService, + vaultTimeoutSettingsService, logoutCallback, customUserAgent, ); diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 80f634d0c5..eedf072a81 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -24,6 +24,11 @@ import { KeySuffixOptions, ThemeType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { UserId } from "@bitwarden/common/types/guid"; +import { + VaultTimeout, + VaultTimeoutOption, + VaultTimeoutStringType, +} from "@bitwarden/common/types/vault-timeout.type"; import { DialogService } from "@bitwarden/components"; import { SetPinComponent } from "../../auth/components/set-pin.component"; @@ -41,7 +46,7 @@ export class SettingsComponent implements OnInit { protected readonly VaultTimeoutAction = VaultTimeoutAction; showMinToTray = false; - vaultTimeoutOptions: any[]; + vaultTimeoutOptions: VaultTimeoutOption[]; localeOptions: any[]; themeOptions: any[]; clearClipboardOptions: any[]; @@ -72,14 +77,14 @@ export class SettingsComponent implements OnInit { timeout: { hours: number; minutes: number }; action: "lock" | "logOut"; }>; - previousVaultTimeout: number = null; + previousVaultTimeout: VaultTimeout = null; userHasMasterPassword: boolean; userHasPinSet: boolean; form = this.formBuilder.group({ // Security - vaultTimeout: [null as number | null], + vaultTimeout: [null as VaultTimeout | null], vaultTimeoutAction: [VaultTimeoutAction.Lock], pin: [null as boolean | null], biometric: false, @@ -159,24 +164,26 @@ export class SettingsComponent implements OnInit { this.showDuckDuckGoIntegrationOption = isMac; this.vaultTimeoutOptions = [ - // { name: i18nService.t('immediately'), value: 0 }, { name: this.i18nService.t("oneMinute"), value: 1 }, { name: this.i18nService.t("fiveMinutes"), value: 5 }, { name: this.i18nService.t("fifteenMinutes"), value: 15 }, { name: this.i18nService.t("thirtyMinutes"), value: 30 }, { name: this.i18nService.t("oneHour"), value: 60 }, { name: this.i18nService.t("fourHours"), value: 240 }, - { name: this.i18nService.t("onIdle"), value: -4 }, - { name: this.i18nService.t("onSleep"), value: -3 }, + { name: this.i18nService.t("onIdle"), value: VaultTimeoutStringType.OnIdle }, + { name: this.i18nService.t("onSleep"), value: VaultTimeoutStringType.OnSleep }, ]; if (this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop) { - this.vaultTimeoutOptions.push({ name: this.i18nService.t("onLocked"), value: -2 }); + this.vaultTimeoutOptions.push({ + name: this.i18nService.t("onLocked"), + value: VaultTimeoutStringType.OnLocked, + }); } this.vaultTimeoutOptions = this.vaultTimeoutOptions.concat([ - { name: this.i18nService.t("onRestart"), value: -1 }, - { name: this.i18nService.t("never"), value: null }, + { name: this.i18nService.t("onRestart"), value: VaultTimeoutStringType.OnRestart }, + { name: this.i18nService.t("never"), value: VaultTimeoutStringType.Never }, ]); const localeOptions: any[] = []; @@ -251,10 +258,14 @@ export class SettingsComponent implements OnInit { // Load initial values this.userHasPinSet = await this.pinService.isPinSet(userId); + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + const initialValues = { - vaultTimeout: await this.vaultTimeoutSettingsService.getVaultTimeout(), + vaultTimeout: await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(activeAccount.id), + ), vaultTimeoutAction: await firstValueFrom( - this.vaultTimeoutSettingsService.vaultTimeoutAction$(), + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id), ), pin: this.userHasPinSet, biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(), @@ -299,7 +310,9 @@ export class SettingsComponent implements OnInit { this.refreshTimeoutSettings$ .pipe( - switchMap(() => this.vaultTimeoutSettingsService.vaultTimeoutAction$()), + switchMap(() => + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id), + ), takeUntil(this.destroy$), ) .subscribe((action) => { @@ -357,7 +370,7 @@ export class SettingsComponent implements OnInit { }); } - async saveVaultTimeout(newValue: number) { + async saveVaultTimeout(newValue: VaultTimeout) { if (newValue == null) { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "warning" }, @@ -387,7 +400,10 @@ export class SettingsComponent implements OnInit { this.previousVaultTimeout = this.form.value.vaultTimeout; + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + await this.vaultTimeoutSettingsService.setVaultTimeoutOptions( + activeAccount.id, newValue, this.form.value.vaultTimeoutAction, ); @@ -418,7 +434,10 @@ export class SettingsComponent implements OnInit { return; } + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + await this.vaultTimeoutSettingsService.setVaultTimeoutOptions( + activeAccount.id, this.form.value.vaultTimeout, newValue, ); diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 056fb3f51e..806fa6de1b 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -41,6 +41,7 @@ import { BiometricStateService } from "@bitwarden/common/platform/biometrics/bio import { StateEventRunnerService } from "@bitwarden/common/platform/state"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { UserId } from "@bitwarden/common/types/guid"; +import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -64,12 +65,6 @@ const BroadcasterSubscriptionId = "AppComponent"; const IdleTimeout = 60000 * 10; // 10 minutes const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours -const systemTimeoutOptions = { - onLock: -2, - onSuspend: -3, - onIdle: -4, -}; - @Component({ selector: "app-root", styles: [], @@ -430,13 +425,13 @@ export class AppComponent implements OnInit, OnDestroy { break; } case "systemSuspended": - await this.checkForSystemTimeout(systemTimeoutOptions.onSuspend); + await this.checkForSystemTimeout(VaultTimeoutStringType.OnSleep); break; case "systemLocked": - await this.checkForSystemTimeout(systemTimeoutOptions.onLock); + await this.checkForSystemTimeout(VaultTimeoutStringType.OnLocked); break; case "systemIdle": - await this.checkForSystemTimeout(systemTimeoutOptions.onIdle); + await this.checkForSystemTimeout(VaultTimeoutStringType.OnIdle); break; case "openLoginApproval": if (message.notificationId != null) { @@ -721,7 +716,7 @@ export class AppComponent implements OnInit, OnDestroy { } } - private async checkForSystemTimeout(timeout: number): Promise { + private async checkForSystemTimeout(timeout: VaultTimeout): Promise { const accounts = await firstValueFrom(this.accountService.accounts$); for (const userId in accounts) { if (userId == null) { @@ -738,9 +733,13 @@ export class AppComponent implements OnInit, OnDestroy { } } - private async getVaultTimeoutOptions(userId: string): Promise<[number, string]> { - const timeout = await this.stateService.getVaultTimeout({ userId: userId }); - const action = await this.stateService.getVaultTimeoutAction({ userId: userId }); + private async getVaultTimeoutOptions(userId: string): Promise<[VaultTimeout, string]> { + const timeout = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId), + ); + const action = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId), + ); return [timeout, action]; } diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index a5f62804aa..8d80097053 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -14,6 +14,7 @@ import { SYSTEM_THEME_OBSERVABLE, SafeInjectionToken, STATE_FACTORY, + DEFAULT_VAULT_TIMEOUT, INTRAPROCESS_MESSAGING_SUBJECT, CLIENT_TYPE, } from "@bitwarden/angular/services/injection-tokens"; @@ -56,6 +57,7 @@ import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/s // eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; +import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; import { DialogService } from "@bitwarden/components"; @@ -138,6 +140,10 @@ const safeProviders: SafeProvider[] = [ provide: SUPPORTS_SECURE_STORAGE, useValue: ELECTRON_SUPPORTS_SECURE_STORAGE, }), + safeProvider({ + provide: DEFAULT_VAULT_TIMEOUT, + useValue: VaultTimeoutStringType.OnRestart, + }), safeProvider({ provide: I18nServiceAbstraction, useClass: I18nRendererService, diff --git a/apps/desktop/src/models/account.ts b/apps/desktop/src/models/account.ts index 0291fdeb28..b3d3128413 100644 --- a/apps/desktop/src/models/account.ts +++ b/apps/desktop/src/models/account.ts @@ -4,7 +4,6 @@ import { } from "@bitwarden/common/platform/models/domain/account"; export class AccountSettings extends BaseAccountSettings { - vaultTimeout = -1; // On Restart dismissedBiometricRequirePasswordOnStartCallout?: boolean; } diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index c60280014c..a7578d0ae2 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -13,6 +13,7 @@ import { OBSERVABLE_DISK_LOCAL_STORAGE, WINDOW, SafeInjectionToken, + DEFAULT_VAULT_TIMEOUT, CLIENT_TYPE, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; @@ -41,6 +42,7 @@ import { DefaultThemeStateService, ThemeStateService, } from "@bitwarden/common/platform/theming/theme-state.service"; +import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { PolicyListService } from "../admin-console/core/policy-list.service"; import { HtmlStorageService } from "../core/html-storage.service"; @@ -69,6 +71,12 @@ const safeProviders: SafeProvider[] = [ safeProvider(RouterService), safeProvider(EventService), safeProvider(PolicyListService), + safeProvider({ + provide: DEFAULT_VAULT_TIMEOUT, + deps: [PlatformUtilsServiceAbstraction], + useFactory: (platformUtilsService: PlatformUtilsServiceAbstraction): VaultTimeout => + platformUtilsService.isDev() ? VaultTimeoutStringType.Never : 15, + }), safeProvider({ provide: APP_INITIALIZER as SafeInjectionToken<() => void>, useFactory: (initService: InitService) => initService.init(), diff --git a/apps/web/src/app/core/state/account.ts b/apps/web/src/app/core/state/account.ts index 0cb16505e3..b6beafe31f 100644 --- a/apps/web/src/app/core/state/account.ts +++ b/apps/web/src/app/core/state/account.ts @@ -1,20 +1,8 @@ -import { - Account as BaseAccount, - AccountSettings as BaseAccountSettings, -} from "@bitwarden/common/platform/models/domain/account"; - -export class AccountSettings extends BaseAccountSettings { - vaultTimeout: number = process.env.NODE_ENV === "development" ? null : 15; -} +import { Account as BaseAccount } from "@bitwarden/common/platform/models/domain/account"; +// TODO: platform to clean up accounts in later PR export class Account extends BaseAccount { - settings?: AccountSettings = new AccountSettings(); - constructor(init: Partial) { super(init); - Object.assign(this.settings, { - ...new AccountSettings(), - ...this.settings, - }); } } diff --git a/apps/web/src/app/settings/preferences.component.ts b/apps/web/src/app/settings/preferences.component.ts index 7f9eabb6b3..a6443b453e 100644 --- a/apps/web/src/app/settings/preferences.component.ts +++ b/apps/web/src/app/settings/preferences.component.ts @@ -5,6 +5,7 @@ import { concatMap, filter, firstValueFrom, map, Observable, Subject, takeUntil, import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -12,6 +13,11 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { ThemeType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { + VaultTimeout, + VaultTimeoutOption, + VaultTimeoutStringType, +} from "@bitwarden/common/types/vault-timeout.type"; import { DialogService } from "@bitwarden/components"; @Component({ @@ -28,7 +34,7 @@ export class PreferencesComponent implements OnInit { timeout: { hours: number; minutes: number }; action: VaultTimeoutAction; }>; - vaultTimeoutOptions: { name: string; value: number }[]; + vaultTimeoutOptions: VaultTimeoutOption[]; localeOptions: any[]; themeOptions: any[]; @@ -36,7 +42,7 @@ export class PreferencesComponent implements OnInit { private destroy$ = new Subject(); form = this.formBuilder.group({ - vaultTimeout: [null as number | null], + vaultTimeout: [null as VaultTimeout | null], vaultTimeoutAction: [VaultTimeoutAction.Lock], enableFavicons: true, theme: [ThemeType.Light], @@ -52,6 +58,7 @@ export class PreferencesComponent implements OnInit { private themeStateService: ThemeStateService, private domainSettingsService: DomainSettingsService, private dialogService: DialogService, + private accountService: AccountService, ) { this.vaultTimeoutOptions = [ { name: i18nService.t("oneMinute"), value: 1 }, @@ -60,10 +67,13 @@ export class PreferencesComponent implements OnInit { { name: i18nService.t("thirtyMinutes"), value: 30 }, { name: i18nService.t("oneHour"), value: 60 }, { name: i18nService.t("fourHours"), value: 240 }, - { name: i18nService.t("onRefresh"), value: -1 }, + { name: i18nService.t("onRefresh"), value: VaultTimeoutStringType.OnRestart }, ]; if (this.platformUtilsService.isDev()) { - this.vaultTimeoutOptions.push({ name: i18nService.t("never"), value: null }); + this.vaultTimeoutOptions.push({ + name: i18nService.t("never"), + value: VaultTimeoutStringType.Never, + }); } const localeOptions: any[] = []; @@ -130,10 +140,15 @@ export class PreferencesComponent implements OnInit { takeUntil(this.destroy$), ) .subscribe(); + + const activeAcct = await firstValueFrom(this.accountService.activeAccount$); + const initialFormValues = { - vaultTimeout: await this.vaultTimeoutSettingsService.getVaultTimeout(), + vaultTimeout: await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(activeAcct.id), + ), vaultTimeoutAction: await firstValueFrom( - this.vaultTimeoutSettingsService.vaultTimeoutAction$(), + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAcct.id), ), enableFavicons: await firstValueFrom(this.domainSettingsService.showFavicons$), theme: await firstValueFrom(this.themeStateService.selectedTheme$), @@ -154,7 +169,10 @@ export class PreferencesComponent implements OnInit { } const values = this.form.value; + const activeAcct = await firstValueFrom(this.accountService.activeAccount$); + await this.vaultTimeoutSettingsService.setVaultTimeoutOptions( + activeAcct.id, values.vaultTimeout, values.vaultTimeoutAction, ); diff --git a/libs/angular/src/components/settings/vault-timeout-input.component.ts b/libs/angular/src/components/settings/vault-timeout-input.component.ts index 0b0fb13d0f..f5afd181f2 100644 --- a/libs/angular/src/components/settings/vault-timeout-input.component.ts +++ b/libs/angular/src/components/settings/vault-timeout-input.component.ts @@ -14,9 +14,10 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { VaultTimeout, VaultTimeoutOption } from "@bitwarden/common/types/vault-timeout.type"; interface VaultTimeoutFormValue { - vaultTimeout: number | null; + vaultTimeout: VaultTimeout | null; custom: { hours: number | null; minutes: number | null; @@ -48,14 +49,14 @@ export class VaultTimeoutInputComponent }), }); - @Input() vaultTimeoutOptions: { name: string; value: number }[]; + @Input() vaultTimeoutOptions: VaultTimeoutOption[]; vaultTimeoutPolicy: Policy; vaultTimeoutPolicyHours: number; vaultTimeoutPolicyMinutes: number; protected canLockVault$: Observable; - private onChange: (vaultTimeout: number) => void; + private onChange: (vaultTimeout: VaultTimeout) => void; private validatorChange: () => void; private destroy$ = new Subject(); @@ -198,12 +199,24 @@ export class VaultTimeoutInputComponent this.vaultTimeoutPolicyHours = Math.floor(this.vaultTimeoutPolicy.data.minutes / 60); this.vaultTimeoutPolicyMinutes = this.vaultTimeoutPolicy.data.minutes % 60; - this.vaultTimeoutOptions = this.vaultTimeoutOptions.filter( - (t) => - t.value <= this.vaultTimeoutPolicy.data.minutes && - (t.value > 0 || t.value === VaultTimeoutInputComponent.CUSTOM_VALUE) && - t.value != null, - ); - this.validatorChange(); + this.vaultTimeoutOptions = this.vaultTimeoutOptions.filter((vaultTimeoutOption) => { + // Always include the custom option + if (vaultTimeoutOption.value === VaultTimeoutInputComponent.CUSTOM_VALUE) { + return true; + } + + if (typeof vaultTimeoutOption.value === "number") { + // Include numeric values that are less than or equal to the policy minutes + return vaultTimeoutOption.value <= this.vaultTimeoutPolicy.data.minutes; + } + + // Exclude all string cases when there's a numeric policy defined + return false; + }); + + // Only call validator change if it's been set + if (this.validatorChange) { + this.validatorChange(); + } } } diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index c58931ce55..9a94659e69 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -9,6 +9,7 @@ import { import { ThemeType } from "@bitwarden/common/platform/enums"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Message } from "@bitwarden/common/platform/messaging"; +import { VaultTimeout } from "@bitwarden/common/types/vault-timeout.type"; declare const tag: unique symbol; /** @@ -47,6 +48,7 @@ export const LOG_MAC_FAILURES = new SafeInjectionToken("LOG_MAC_FAILURE export const SYSTEM_THEME_OBSERVABLE = new SafeInjectionToken>( "SYSTEM_THEME_OBSERVABLE", ); +export const DEFAULT_VAULT_TIMEOUT = new SafeInjectionToken("DEFAULT_VAULT_TIMEOUT"); export const INTRAPROCESS_MESSAGING_SUBJECT = new SafeInjectionToken>>( "INTRAPROCESS_MESSAGING_SUBJECT", ); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 3d68e1240b..ee14d97e68 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -274,6 +274,7 @@ import { SYSTEM_LANGUAGE, SYSTEM_THEME_OBSERVABLE, WINDOW, + DEFAULT_VAULT_TIMEOUT, INTRAPROCESS_MESSAGING_SUBJECT, CLIENT_TYPE, } from "./injection-tokens"; @@ -392,6 +393,7 @@ const safeProviders: SafeProvider[] = [ InternalUserDecryptionOptionsServiceAbstraction, GlobalStateProvider, BillingAccountProfileStateService, + VaultTimeoutSettingsServiceAbstraction, KdfConfigServiceAbstraction, ], }), @@ -573,7 +575,7 @@ const safeProviders: SafeProvider[] = [ PlatformUtilsServiceAbstraction, EnvironmentService, AppIdServiceAbstraction, - StateServiceAbstraction, + VaultTimeoutSettingsServiceAbstraction, LOGOUT_CALLBACK, ], }), @@ -646,8 +648,10 @@ const safeProviders: SafeProvider[] = [ CryptoServiceAbstraction, TokenServiceAbstraction, PolicyServiceAbstraction, - StateServiceAbstraction, BiometricStateService, + StateProvider, + LogService, + DEFAULT_VAULT_TIMEOUT, ], }), safeProvider({ diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index 76c0444b5a..d2aac323bf 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -1,4 +1,5 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; @@ -8,6 +9,7 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -16,6 +18,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; @@ -45,6 +48,7 @@ describe("AuthRequestLoginStrategy", () => { let userDecryptionOptions: MockProxy; let deviceTrustService: MockProxy; let billingAccountProfileStateService: MockProxy; + let vaultTimeoutSettingsService: MockProxy; let kdfConfigService: MockProxy; const mockUserId = Utils.newGuid() as UserId; @@ -79,6 +83,7 @@ describe("AuthRequestLoginStrategy", () => { userDecryptionOptions = mock(); deviceTrustService = mock(); billingAccountProfileStateService = mock(); + vaultTimeoutSettingsService = mock(); kdfConfigService = mock(); accountService = mockAccountServiceWith(mockUserId); @@ -106,11 +111,27 @@ describe("AuthRequestLoginStrategy", () => { userDecryptionOptions, deviceTrustService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); tokenResponse = identityTokenResponseFactory(); apiService.postIdentityToken.mockResolvedValue(tokenResponse); + + const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeoutActionBSub = new BehaviorSubject( + mockVaultTimeoutAction, + ); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + mockVaultTimeoutActionBSub.asObservable(), + ); + + const mockVaultTimeout = 1000; + + const mockVaultTimeoutBSub = new BehaviorSubject(mockVaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue( + mockVaultTimeoutBSub.asObservable(), + ); }); it("sets keys after a successful authentication when masterKey and masterKeyHash provided in login credentials", async () => { diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts index 43c9af35cd..54654e1d82 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts @@ -2,6 +2,7 @@ import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; @@ -64,6 +65,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private deviceTrustService: DeviceTrustServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, + vaultTimeoutSettingsService: VaultTimeoutSettingsService, kdfConfigService: KdfConfigService, ) { super( @@ -80,6 +82,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index 0c2205262c..627c852076 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -1,6 +1,8 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -114,6 +116,7 @@ describe("LoginStrategy", () => { let policyService: MockProxy; let passwordStrengthService: MockProxy; let billingAccountProfileStateService: MockProxy; + let vaultTimeoutSettingsService: MockProxy; let kdfConfigService: MockProxy; let passwordLoginStrategy: PasswordLoginStrategy; @@ -139,6 +142,8 @@ describe("LoginStrategy", () => { passwordStrengthService = mock(); billingAccountProfileStateService = mock(); + vaultTimeoutSettingsService = mock(); + appIdService.getAppId.mockResolvedValue(deviceId); tokenService.decodeAccessToken.calledWith(accessToken).mockResolvedValue(decodedToken); @@ -161,6 +166,7 @@ describe("LoginStrategy", () => { policyService, loginStrategyService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); credentials = new PasswordLoginCredentials(email, masterPassword); @@ -179,6 +185,21 @@ describe("LoginStrategy", () => { masterKey = new SymmetricCryptoKey( new Uint8Array(masterKeyBytesLength).buffer as CsprngArray, ) as MasterKey; + + const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeoutActionBSub = new BehaviorSubject( + mockVaultTimeoutAction, + ); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + mockVaultTimeoutActionBSub.asObservable(), + ); + + const mockVaultTimeout = 1000; + + const mockVaultTimeoutBSub = new BehaviorSubject(mockVaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue( + mockVaultTimeoutBSub.asObservable(), + ); }); it("sets the local environment after a successful login with master password", async () => { @@ -186,10 +207,19 @@ describe("LoginStrategy", () => { apiService.postIdentityToken.mockResolvedValue(idTokenResponse); const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeoutActionBSub = new BehaviorSubject( + mockVaultTimeoutAction, + ); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + mockVaultTimeoutActionBSub.asObservable(), + ); + const mockVaultTimeout = 1000; - stateService.getVaultTimeoutAction.mockResolvedValue(mockVaultTimeoutAction); - stateService.getVaultTimeout.mockResolvedValue(mockVaultTimeout); + const mockVaultTimeoutBSub = new BehaviorSubject(mockVaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue( + mockVaultTimeoutBSub.asObservable(), + ); await passwordLoginStrategy.logIn(credentials); @@ -223,10 +253,20 @@ describe("LoginStrategy", () => { apiService.postIdentityToken.mockResolvedValue(idTokenResponse); const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + + const mockVaultTimeoutActionBSub = new BehaviorSubject( + mockVaultTimeoutAction, + ); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + mockVaultTimeoutActionBSub.asObservable(), + ); + const mockVaultTimeout = 1000; - stateService.getVaultTimeoutAction.mockResolvedValue(mockVaultTimeoutAction); - stateService.getVaultTimeout.mockResolvedValue(mockVaultTimeout); + const mockVaultTimeoutBSub = new BehaviorSubject(mockVaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue( + mockVaultTimeoutBSub.asObservable(), + ); accountService.switchAccount = jest.fn(); // block internal switch to new account accountService.activeAccountSubject.next(null); // simulate no active account @@ -297,6 +337,22 @@ describe("LoginStrategy", () => { }); describe("Two-factor authentication", () => { + beforeEach(() => { + const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeoutActionBSub = new BehaviorSubject( + mockVaultTimeoutAction, + ); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + mockVaultTimeoutActionBSub.asObservable(), + ); + + const mockVaultTimeout = 1000; + const mockVaultTimeoutBSub = new BehaviorSubject(mockVaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue( + mockVaultTimeoutBSub.asObservable(), + ); + }); + it("rejects login if 2FA is required", async () => { // Sample response where TOTP 2FA required const tokenResponse = new IdentityTwoFactorResponse({ @@ -421,6 +477,7 @@ describe("LoginStrategy", () => { policyService, loginStrategyService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index adcf753325..2065f898be 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -1,6 +1,7 @@ import { BehaviorSubject, filter, firstValueFrom, timeout } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; @@ -75,6 +76,7 @@ export abstract class LoginStrategy { protected twoFactorService: TwoFactorService, protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected billingAccountProfileStateService: BillingAccountProfileStateService, + protected vaultTimeoutSettingsService: VaultTimeoutSettingsService, protected KdfConfigService: KdfConfigService, ) {} @@ -163,27 +165,14 @@ export abstract class LoginStrategy { */ protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise { const accountInformation = await this.tokenService.decodeAccessToken(tokenResponse.accessToken); - const userId = accountInformation.sub as UserId; - const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId }); - const vaultTimeout = await this.stateService.getVaultTimeout({ userId }); - await this.accountService.addAccount(userId, { name: accountInformation.name, email: accountInformation.email, emailVerified: accountInformation.email_verified, }); - // set access token and refresh token before account initialization so authN status can be accurate - // User id will be derived from the access token. - await this.tokenService.setTokens( - tokenResponse.accessToken, - vaultTimeoutAction as VaultTimeoutAction, - vaultTimeout, - tokenResponse.refreshToken, // Note: CLI login via API key sends undefined for refresh token. - ); - await this.accountService.switchAccount(userId); await this.stateService.addAccount( @@ -201,10 +190,27 @@ export abstract class LoginStrategy { await this.verifyAccountAdded(userId); + // We must set user decryption options before retrieving vault timeout settings + // as the user decryption options help determine the available timeout actions. await this.userDecryptionOptionsService.setUserDecryptionOptions( UserDecryptionOptions.fromResponse(tokenResponse), ); + const vaultTimeoutAction = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId), + ); + const vaultTimeout = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId), + ); + + // User id will be derived from the access token. + await this.tokenService.setTokens( + tokenResponse.accessToken, + vaultTimeoutAction as VaultTimeoutAction, + vaultTimeout, + tokenResponse.refreshToken, // Note: CLI login via API key sends undefined for refresh token. + ); + await this.KdfConfigService.setKdfConfig( userId as UserId, tokenResponse.kdf === KdfType.PBKDF2_SHA256 diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index 5c0ec93771..b6d1e07a26 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -1,4 +1,5 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -12,6 +13,7 @@ import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/respons import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -21,6 +23,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { HashPurpose } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction, @@ -72,6 +75,7 @@ describe("PasswordLoginStrategy", () => { let policyService: MockProxy; let passwordStrengthService: MockProxy; let billingAccountProfileStateService: MockProxy; + let vaultTimeoutSettingsService: MockProxy; let kdfConfigService: MockProxy; let passwordLoginStrategy: PasswordLoginStrategy; @@ -96,6 +100,7 @@ describe("PasswordLoginStrategy", () => { policyService = mock(); passwordStrengthService = mock(); billingAccountProfileStateService = mock(); + vaultTimeoutSettingsService = mock(); kdfConfigService = mock(); appIdService.getAppId.mockResolvedValue(deviceId); @@ -132,12 +137,28 @@ describe("PasswordLoginStrategy", () => { policyService, loginStrategyService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); credentials = new PasswordLoginCredentials(email, masterPassword); tokenResponse = identityTokenResponseFactory(masterPasswordPolicy); apiService.postIdentityToken.mockResolvedValue(tokenResponse); + + const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeoutActionBSub = new BehaviorSubject( + mockVaultTimeoutAction, + ); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + mockVaultTimeoutActionBSub.asObservable(), + ); + + const mockVaultTimeout = 1000; + + const mockVaultTimeoutBSub = new BehaviorSubject(mockVaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue( + mockVaultTimeoutBSub.asObservable(), + ); }); it("sends master password credentials to the server", async () => { diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index f78897d99f..b855e25e1d 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -2,6 +2,7 @@ import { BehaviorSubject, firstValueFrom, map, Observable } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -90,6 +91,7 @@ export class PasswordLoginStrategy extends LoginStrategy { private policyService: PolicyService, private loginStrategyService: LoginStrategyServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, + vaultTimeoutSettingsService: VaultTimeoutSettingsService, kdfConfigService: KdfConfigService, ) { super( @@ -106,6 +108,7 @@ export class PasswordLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index f821ce9a6b..b6290742be 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -1,4 +1,5 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; @@ -12,6 +13,7 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -22,6 +24,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; @@ -55,6 +58,7 @@ describe("SsoLoginStrategy", () => { let authRequestService: MockProxy; let i18nService: MockProxy; let billingAccountProfileStateService: MockProxy; + let vaultTimeoutSettingsService: MockProxy; let kdfConfigService: MockProxy; let ssoLoginStrategy: SsoLoginStrategy; @@ -88,6 +92,7 @@ describe("SsoLoginStrategy", () => { authRequestService = mock(); i18nService = mock(); billingAccountProfileStateService = mock(); + vaultTimeoutSettingsService = mock(); kdfConfigService = mock(); tokenService.getTwoFactorToken.mockResolvedValue(null); @@ -96,6 +101,21 @@ describe("SsoLoginStrategy", () => { sub: userId, }); + const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeoutActionBSub = new BehaviorSubject( + mockVaultTimeoutAction, + ); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + mockVaultTimeoutActionBSub.asObservable(), + ); + + const mockVaultTimeout = 1000; + + const mockVaultTimeoutBSub = new BehaviorSubject(mockVaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue( + mockVaultTimeoutBSub.asObservable(), + ); + ssoLoginStrategy = new SsoLoginStrategy( null, accountService, @@ -115,6 +135,7 @@ describe("SsoLoginStrategy", () => { authRequestService, i18nService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); credentials = new SsoLoginCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index c73252a1de..414af4c1a3 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -2,6 +2,7 @@ import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; @@ -100,6 +101,7 @@ export class SsoLoginStrategy extends LoginStrategy { private authRequestService: AuthRequestServiceAbstraction, private i18nService: I18nService, billingAccountProfileStateService: BillingAccountProfileStateService, + vaultTimeoutSettingsService: VaultTimeoutSettingsService, kdfConfigService: KdfConfigService, ) { super( @@ -116,6 +118,7 @@ export class SsoLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts index 820af8613e..8120a5ad39 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts @@ -21,6 +21,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; @@ -50,11 +51,15 @@ describe("UserApiLoginStrategy", () => { let keyConnectorService: MockProxy; let environmentService: MockProxy; let billingAccountProfileStateService: MockProxy; + let vaultTimeoutSettingsService: MockProxy; let kdfConfigService: MockProxy; let apiLogInStrategy: UserApiLoginStrategy; let credentials: UserApiLoginCredentials; + const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeout = 1000; + const userId = Utils.newGuid() as UserId; const deviceId = Utils.newGuid(); const keyConnectorUrl = "KEY_CONNECTOR_URL"; @@ -78,6 +83,7 @@ describe("UserApiLoginStrategy", () => { keyConnectorService = mock(); environmentService = mock(); billingAccountProfileStateService = mock(); + vaultTimeoutSettingsService = mock(); kdfConfigService = mock(); appIdService.getAppId.mockResolvedValue(deviceId); @@ -103,10 +109,23 @@ describe("UserApiLoginStrategy", () => { environmentService, keyConnectorService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); credentials = new UserApiLoginCredentials(apiClientId, apiClientSecret); + + const mockVaultTimeoutActionBSub = new BehaviorSubject( + mockVaultTimeoutAction, + ); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + mockVaultTimeoutActionBSub.asObservable(), + ); + + const mockVaultTimeoutBSub = new BehaviorSubject(mockVaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue( + mockVaultTimeoutBSub.asObservable(), + ); }); it("sends api key credentials to the server", async () => { @@ -131,11 +150,6 @@ describe("UserApiLoginStrategy", () => { it("sets the local environment after a successful login", async () => { apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory()); - const mockVaultTimeoutAction = VaultTimeoutAction.Lock; - const mockVaultTimeout = 60; - stateService.getVaultTimeoutAction.mockResolvedValue(mockVaultTimeoutAction); - stateService.getVaultTimeout.mockResolvedValue(mockVaultTimeout); - await apiLogInStrategy.logIn(credentials); expect(tokenService.setClientId).toHaveBeenCalledWith( diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts index 5e4124492d..86113d3655 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts @@ -2,6 +2,7 @@ import { firstValueFrom, BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; @@ -58,6 +59,7 @@ export class UserApiLoginStrategy extends LoginStrategy { private environmentService: EnvironmentService, private keyConnectorService: KeyConnectorService, billingAccountProfileStateService: BillingAccountProfileStateService, + vaultTimeoutSettingsService: VaultTimeoutSettingsService, protected kdfConfigService: KdfConfigService, ) { super( @@ -74,6 +76,7 @@ export class UserApiLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); this.cache = new BehaviorSubject(data); @@ -130,8 +133,12 @@ export class UserApiLoginStrategy extends LoginStrategy { protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise { const userId = await super.saveAccountInformation(tokenResponse); - const vaultTimeout = await this.stateService.getVaultTimeout(); - const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction(); + const vaultTimeoutAction = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId), + ); + const vaultTimeout = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId), + ); const tokenRequest = this.cache.value.tokenRequest; diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts index afac2c2e6a..0db41c1e64 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts @@ -1,4 +1,5 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; @@ -10,6 +11,7 @@ import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/mod import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -18,6 +20,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; import { PrfKey, UserKey } from "@bitwarden/common/types/key"; @@ -44,6 +47,7 @@ describe("WebAuthnLoginStrategy", () => { let twoFactorService!: MockProxy; let userDecryptionOptionsService: MockProxy; let billingAccountProfileStateService: MockProxy; + let vaultTimeoutSettingsService: MockProxy; let kdfConfigService: MockProxy; let webAuthnLoginStrategy!: WebAuthnLoginStrategy; @@ -85,6 +89,7 @@ describe("WebAuthnLoginStrategy", () => { twoFactorService = mock(); userDecryptionOptionsService = mock(); billingAccountProfileStateService = mock(); + vaultTimeoutSettingsService = mock(); kdfConfigService = mock(); tokenService.getTwoFactorToken.mockResolvedValue(null); @@ -108,6 +113,7 @@ describe("WebAuthnLoginStrategy", () => { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); @@ -116,6 +122,22 @@ describe("WebAuthnLoginStrategy", () => { const deviceResponse = new WebAuthnLoginAssertionResponseRequest(publicKeyCredential); const prfKey = new SymmetricCryptoKey(randomBytes(32)) as PrfKey; webAuthnCredentials = new WebAuthnLoginCredentials(token, deviceResponse, prfKey); + + // Mock vault timeout settings + const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeoutActionBSub = new BehaviorSubject( + mockVaultTimeoutAction, + ); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + mockVaultTimeoutActionBSub.asObservable(), + ); + + const mockVaultTimeout = 1000; + + const mockVaultTimeoutBSub = new BehaviorSubject(mockVaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue( + mockVaultTimeoutBSub.asObservable(), + ); }); afterAll(() => { diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts index 4b5441d00a..226ab1799a 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts @@ -2,6 +2,7 @@ import { BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; @@ -58,6 +59,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { twoFactorService: TwoFactorService, userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, + vaultTimeoutSettingsService: VaultTimeoutSettingsService, kdfConfigService: KdfConfigService, ) { super( @@ -74,6 +76,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index f1b5590404..f0a8d81bea 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -1,6 +1,8 @@ import { MockProxy, mock } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; @@ -14,6 +16,7 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -67,6 +70,7 @@ describe("LoginStrategyService", () => { let authRequestService: MockProxy; let userDecryptionOptionsService: MockProxy; let billingAccountProfileStateService: MockProxy; + let vaultTimeoutSettingsService: MockProxy; let kdfConfigService: MockProxy; let stateProvider: FakeGlobalStateProvider; @@ -97,6 +101,7 @@ describe("LoginStrategyService", () => { userDecryptionOptionsService = mock(); billingAccountProfileStateService = mock(); stateProvider = new FakeGlobalStateProvider(); + vaultTimeoutSettingsService = mock(); kdfConfigService = mock(); sut = new LoginStrategyService( @@ -122,10 +127,26 @@ describe("LoginStrategyService", () => { userDecryptionOptionsService, stateProvider, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY); + + const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeoutActionBSub = new BehaviorSubject( + mockVaultTimeoutAction, + ); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + mockVaultTimeoutActionBSub.asObservable(), + ); + + const mockVaultTimeout = 1000; + + const mockVaultTimeoutBSub = new BehaviorSubject(mockVaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue( + mockVaultTimeoutBSub.asObservable(), + ); }); it("should return an AuthResult on successful login", async () => { diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 13cca69b3a..46d785f9b5 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -8,6 +8,7 @@ import { } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; @@ -110,6 +111,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected stateProvider: GlobalStateProvider, protected billingAccountProfileStateService: BillingAccountProfileStateService, + protected vaultTimeoutSettingsService: VaultTimeoutSettingsService, protected kdfConfigService: KdfConfigService, ) { this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY); @@ -361,6 +363,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.policyService, this, this.billingAccountProfileStateService, + this.vaultTimeoutSettingsService, this.kdfConfigService, ); case AuthenticationType.Sso: @@ -383,6 +386,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.authRequestService, this.i18nService, this.billingAccountProfileStateService, + this.vaultTimeoutSettingsService, this.kdfConfigService, ); case AuthenticationType.UserApiKey: @@ -403,6 +407,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.environmentService, this.keyConnectorService, this.billingAccountProfileStateService, + this.vaultTimeoutSettingsService, this.kdfConfigService, ); case AuthenticationType.AuthRequest: @@ -422,6 +427,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.userDecryptionOptionsService, this.deviceTrustService, this.billingAccountProfileStateService, + this.vaultTimeoutSettingsService, this.kdfConfigService, ); case AuthenticationType.WebAuthn: @@ -440,6 +446,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.twoFactorService, this.userDecryptionOptionsService, this.billingAccountProfileStateService, + this.vaultTimeoutSettingsService, this.kdfConfigService, ); } diff --git a/libs/common/src/abstractions/vault-timeout/vault-timeout-settings.service.ts b/libs/common/src/abstractions/vault-timeout/vault-timeout-settings.service.ts index 03ae320e54..5bf38f3b57 100644 --- a/libs/common/src/abstractions/vault-timeout/vault-timeout-settings.service.ts +++ b/libs/common/src/abstractions/vault-timeout/vault-timeout-settings.service.ts @@ -1,16 +1,19 @@ import { Observable } from "rxjs"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { UserId } from "../../types/guid"; +import { VaultTimeout } from "../../types/vault-timeout.type"; export abstract class VaultTimeoutSettingsService { /** * Set the vault timeout options for the user * @param vaultTimeout The vault timeout in minutes * @param vaultTimeoutAction The vault timeout action - * @param userId The user id to set. If not provided, the current user is used + * @param userId The user id to set the data for. */ setVaultTimeoutOptions: ( - vaultTimeout: number, + userId: UserId, + vaultTimeout: VaultTimeout, vaultTimeoutAction: VaultTimeoutAction, ) => Promise; @@ -23,19 +26,23 @@ export abstract class VaultTimeoutSettingsService { availableVaultTimeoutActions$: (userId?: string) => Observable; /** - * Get the current vault timeout action for the user. This is not the same as the current state, it is - * calculated based on the current state, the user's policy, and the user's available unlock methods. + * Gets the vault timeout action for the given user id. The returned value is + * calculated based on the current state, if a max vault timeout policy applies to the user, + * and what the user's available unlock methods are. + * + * A new action will be emitted if the current state changes or if the user's policy changes and the new policy affects the action. + * @param userId - the user id to get the vault timeout action for */ - getVaultTimeout: (userId?: string) => Promise; + getVaultTimeoutActionByUserId$: (userId: string) => Observable; /** - * Observe the vault timeout action for the user. This is calculated based on users preferred lock action saved in the state, - * the user's policy, and the user's available unlock methods. + * Get the vault timeout for the given user id. The returned value is calculated based on the current state + * and if a max vault timeout policy applies to the user. * - * **NOTE:** This observable is not yet connected to the state service, so it will not update when the state changes - * @param userId The user id to check. If not provided, the current user is used + * A new timeout will be emitted if the current state changes or if the user's policy changes and the new policy affects the timeout. + * @param userId The user id to get the vault timeout for */ - vaultTimeoutAction$: (userId?: string) => Observable; + getVaultTimeoutByUserId$: (userId: string) => Observable; /** * Has the user enabled unlock with Biometric. diff --git a/libs/common/src/auth/abstractions/token.service.ts b/libs/common/src/auth/abstractions/token.service.ts index fc3bd317f4..d078051f64 100644 --- a/libs/common/src/auth/abstractions/token.service.ts +++ b/libs/common/src/auth/abstractions/token.service.ts @@ -2,6 +2,7 @@ import { Observable } from "rxjs"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { UserId } from "../../types/guid"; +import { VaultTimeout } from "../../types/vault-timeout.type"; import { DecodedAccessToken } from "../services/token.service"; export abstract class TokenService { @@ -27,7 +28,7 @@ export abstract class TokenService { setTokens: ( accessToken: string, vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, refreshToken?: string, clientIdClientSecret?: [string, string], ) => Promise; @@ -51,7 +52,7 @@ export abstract class TokenService { setAccessToken: ( accessToken: string, vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, ) => Promise; // TODO: revisit having this public clear method approach once the state service is fully deprecated. @@ -90,7 +91,7 @@ export abstract class TokenService { setClientId: ( clientId: string, vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, userId?: UserId, ) => Promise; @@ -110,7 +111,7 @@ export abstract class TokenService { setClientSecret: ( clientSecret: string, vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, userId?: UserId, ) => Promise; diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index 3e92053d2f..9c5dd9fc91 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -10,9 +10,10 @@ import { AbstractStorageService } from "../../platform/abstractions/storage.serv import { StorageLocation } from "../../platform/enums"; import { StorageOptions } from "../../platform/models/domain/storage-options"; import { UserId } from "../../types/guid"; +import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type"; import { ACCOUNT_ACTIVE_ACCOUNT_ID } from "./account.service"; -import { DecodedAccessToken, TokenService } from "./token.service"; +import { DecodedAccessToken, TokenService, TokenStorageLocation } from "./token.service"; import { ACCESS_TOKEN_DISK, ACCESS_TOKEN_MEMORY, @@ -37,10 +38,10 @@ describe("TokenService", () => { let logService: MockProxy; const memoryVaultTimeoutAction = VaultTimeoutAction.LogOut; - const memoryVaultTimeout = 30; + const memoryVaultTimeout: VaultTimeout = 30; const diskVaultTimeoutAction = VaultTimeoutAction.Lock; - const diskVaultTimeout: number = null; + const diskVaultTimeout: VaultTimeout = VaultTimeoutStringType.Never; const accessTokenJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0IiwibmJmIjoxNzA5MzI0MTExLCJpYXQiOjE3MDkzMjQxMTEsImV4cCI6MTcwOTMyNzcxMSwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl0sImNsaWVudF9pZCI6IndlYiIsInN1YiI6ImVjZTcwYTEzLTcyMTYtNDNjNC05OTc3LWIxMDMwMTQ2ZTFlNyIsImF1dGhfdGltZSI6MTcwOTMyNDEwNCwiaWRwIjoiYml0d2FyZGVuIiwicHJlbWl1bSI6ZmFsc2UsImVtYWlsIjoiZXhhbXBsZUBiaXR3YXJkZW4uY29tIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJzc3RhbXAiOiJHWTdKQU82NENLS1RLQkI2WkVBVVlMMldPUVU3QVNUMiIsIm5hbWUiOiJUZXN0IFVzZXIiLCJvcmdvd25lciI6WyI5MmI0OTkwOC1iNTE0LTQ1YTgtYmFkYi1iMTAzMDE0OGZlNTMiLCIzOGVkZTMyMi1iNGI0LTRiZDgtOWUwOS1iMTA3MDExMmRjMTEiLCJiMmQwNzAyOC1hNTgzLTRjM2UtOGQ2MC1iMTA3MDExOThjMjkiLCJiZjkzNGJhMi0wZmQ0LTQ5ZjItYTk1ZS1iMTA3MDExZmM5ZTYiLCJjMGI3Zjc1ZC0wMTVmLTQyYzktYjNhNi1iMTA4MDE3NjA3Y2EiXSwiZGV2aWNlIjoiNGI4NzIzNjctMGRhNi00MWEwLWFkY2ItNzdmMmZlZWZjNGY0IiwianRpIjoiNzUxNjFCRTQxMzFGRjVBMkRFNTExQjhDNEUyRkY4OUEifQ.n7roP8sSbfwcYdvRxZNZds27IK32TW6anorE6BORx_Q"; @@ -163,21 +164,53 @@ describe("TokenService", () => { describe("setAccessToken", () => { it("should throw an error if the access token is null", async () => { // Act - const result = tokenService.setAccessToken(null, VaultTimeoutAction.Lock, null); + const result = tokenService.setAccessToken( + null, + VaultTimeoutAction.Lock, + VaultTimeoutStringType.Never, + ); // Assert await expect(result).rejects.toThrow("Access token is required."); }); it("should throw an error if an invalid token is passed in", async () => { // Act - const result = tokenService.setAccessToken("invalidToken", VaultTimeoutAction.Lock, null); + const result = tokenService.setAccessToken( + "invalidToken", + VaultTimeoutAction.Lock, + VaultTimeoutStringType.Never, + ); // Assert await expect(result).rejects.toThrow("JWT must have 3 parts"); }); - it("should not throw an error as long as the token is valid", async () => { + it("should throw an error if the vault timeout is missing", async () => { // Act const result = tokenService.setAccessToken(accessTokenJwt, VaultTimeoutAction.Lock, null); + + // Assert + await expect(result).rejects.toThrow("Vault Timeout is required."); + }); + + it("should throw an error if the vault timeout action is missing", async () => { + // Act + const result = tokenService.setAccessToken( + accessTokenJwt, + null, + VaultTimeoutStringType.Never, + ); + + // Assert + await expect(result).rejects.toThrow("Vault Timeout Action is required."); + }); + + it("should not throw an error as long as the token is valid", async () => { + // Act + const result = tokenService.setAccessToken( + accessTokenJwt, + VaultTimeoutAction.Lock, + VaultTimeoutStringType.Never, + ); // Assert await expect(result).resolves.not.toThrow(); }); @@ -1053,6 +1086,32 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("User id not found. Cannot save refresh token."); }); + it("should throw an error if the vault timeout is missing", async () => { + // Act + const result = (tokenService as any).setRefreshToken( + refreshToken, + VaultTimeoutAction.Lock, + null, + userIdFromAccessToken, + ); + + // Assert + await expect(result).rejects.toThrow("Vault Timeout is required."); + }); + + it("should throw an error if the vault timeout action is missing", async () => { + // Act + const result = (tokenService as any).setRefreshToken( + refreshToken, + null, + VaultTimeoutStringType.Never, + userIdFromAccessToken, + ); + + // Assert + await expect(result).rejects.toThrow("Vault Timeout Action is required."); + }); + describe("Memory storage tests", () => { it("should set the refresh token in memory for the specified user id", async () => { // Act @@ -1382,6 +1441,34 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("User id not found. Cannot save client id."); }); + it("should throw an error if the vault timeout is missing", async () => { + // Arrange + + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + + // Act + const result = tokenService.setClientId(clientId, VaultTimeoutAction.Lock, null); + + // Assert + await expect(result).rejects.toThrow("Vault Timeout is required."); + }); + + it("should throw an error if the vault timeout action is missing", async () => { + // Arrange + + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + + // Act + const result = tokenService.setClientId(clientId, null, VaultTimeoutStringType.Never); + + // Assert + await expect(result).rejects.toThrow("Vault Timeout Action is required."); + }); + describe("Memory storage tests", () => { it("should set the client id in memory when there is an active user in global state", async () => { // Arrange @@ -1618,11 +1705,47 @@ describe("TokenService", () => { it("should throw an error if no user id is provided and there is no active user in global state", async () => { // Act // note: don't await here because we want to test the error - const result = tokenService.setClientSecret(clientSecret, VaultTimeoutAction.Lock, null); + const result = tokenService.setClientSecret( + clientSecret, + VaultTimeoutAction.Lock, + VaultTimeoutStringType.Never, + ); // Assert await expect(result).rejects.toThrow("User id not found. Cannot save client secret."); }); + it("should throw an error if the vault timeout is missing", async () => { + // Arrange + + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + + // Act + const result = tokenService.setClientSecret(clientSecret, VaultTimeoutAction.Lock, null); + + // Assert + await expect(result).rejects.toThrow("Vault Timeout is required."); + }); + + it("should throw an error if the vault timeout action is missing", async () => { + // Arrange + + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + + // Act + const result = tokenService.setClientSecret( + clientSecret, + null, + VaultTimeoutStringType.Never, + ); + + // Assert + await expect(result).rejects.toThrow("Vault Timeout Action is required."); + }); + describe("Memory storage tests", () => { it("should set the client secret in memory when there is an active user in global state", async () => { // Arrange @@ -1991,6 +2114,42 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("Access token is required."); }); + it("should throw an error if the vault timeout is missing", async () => { + // Arrange + const refreshToken = "refreshToken"; + const vaultTimeoutAction = VaultTimeoutAction.Lock; + const vaultTimeout: VaultTimeout = null; + + // Act + const result = tokenService.setTokens( + accessTokenJwt, + vaultTimeoutAction, + vaultTimeout, + refreshToken, + ); + + // Assert + await expect(result).rejects.toThrow("Vault Timeout is required."); + }); + + it("should throw an error if the vault timeout action is missing", async () => { + // Arrange + const refreshToken = "refreshToken"; + const vaultTimeoutAction: VaultTimeoutAction = null; + const vaultTimeout: VaultTimeout = VaultTimeoutStringType.Never; + + // Act + const result = tokenService.setTokens( + accessTokenJwt, + vaultTimeoutAction, + vaultTimeout, + refreshToken, + ); + + // Assert + await expect(result).rejects.toThrow("Vault Timeout Action is required."); + }); + it("should not throw an error if the refresh token is missing and it should just not set it", async () => { // Arrange const refreshToken: string = null; @@ -2270,6 +2429,168 @@ describe("TokenService", () => { }); }); + describe("determineStorageLocation", () => { + it("should throw an error if the vault timeout is null", async () => { + // Arrange + const vaultTimeoutAction: VaultTimeoutAction = VaultTimeoutAction.Lock; + const vaultTimeout: VaultTimeout = null; + // Act + const result = (tokenService as any).determineStorageLocation( + vaultTimeoutAction, + vaultTimeout, + false, + ); + // Assert + await expect(result).rejects.toThrow( + "TokenService - determineStorageLocation: We expect the vault timeout to always exist at this point.", + ); + }); + + it("should throw an error if the vault timeout action is null", async () => { + // Arrange + const vaultTimeoutAction: VaultTimeoutAction = null; + const vaultTimeout: VaultTimeout = 0; + // Act + const result = (tokenService as any).determineStorageLocation( + vaultTimeoutAction, + vaultTimeout, + false, + ); + // Assert + await expect(result).rejects.toThrow( + "TokenService - determineStorageLocation: We expect the vault timeout action to always exist at this point.", + ); + }); + + describe("Secure storage disabled", () => { + beforeEach(() => { + const supportsSecureStorage = false; + tokenService = createTokenService(supportsSecureStorage); + }); + + it.each([ + [VaultTimeoutStringType.OnRestart], + [VaultTimeoutStringType.OnLocked], + [VaultTimeoutStringType.OnSleep], + [VaultTimeoutStringType.OnIdle], + [0], + [30], + [60], + [90], + [120], + ])( + "returns memory when the vault timeout action is logout and the vault timeout is defined %s (not Never)", + async (vaultTimeout: VaultTimeout) => { + // Arrange + const vaultTimeoutAction = VaultTimeoutAction.LogOut; + const useSecureStorage = false; + // Act + const result = await (tokenService as any).determineStorageLocation( + vaultTimeoutAction, + vaultTimeout, + useSecureStorage, + ); + // Assert + expect(result).toEqual(TokenStorageLocation.Memory); + }, + ); + + it("returns disk when the vault timeout action is logout and the vault timeout is never", async () => { + // Arrange + const vaultTimeoutAction = VaultTimeoutAction.LogOut; + const vaultTimeout: VaultTimeout = VaultTimeoutStringType.Never; + const useSecureStorage = false; + // Act + const result = await (tokenService as any).determineStorageLocation( + vaultTimeoutAction, + vaultTimeout, + useSecureStorage, + ); + // Assert + expect(result).toEqual(TokenStorageLocation.Disk); + }); + + it("returns disk when the vault timeout action is lock and the vault timeout is never", async () => { + // Arrange + const vaultTimeoutAction = VaultTimeoutAction.Lock; + const vaultTimeout: VaultTimeout = VaultTimeoutStringType.Never; + const useSecureStorage = false; + // Act + const result = await (tokenService as any).determineStorageLocation( + vaultTimeoutAction, + vaultTimeout, + useSecureStorage, + ); + // Assert + expect(result).toEqual(TokenStorageLocation.Disk); + }); + }); + + describe("Secure storage enabled", () => { + beforeEach(() => { + const supportsSecureStorage = true; + tokenService = createTokenService(supportsSecureStorage); + }); + + it.each([ + [VaultTimeoutStringType.OnRestart], + [VaultTimeoutStringType.OnLocked], + [VaultTimeoutStringType.OnSleep], + [VaultTimeoutStringType.OnIdle], + [0], + [30], + [60], + [90], + [120], + ])( + "returns memory when the vault timeout action is logout and the vault timeout is defined %s (not Never)", + async (vaultTimeout: VaultTimeout) => { + // Arrange + const vaultTimeoutAction = VaultTimeoutAction.LogOut; + const useSecureStorage = true; + // Act + const result = await (tokenService as any).determineStorageLocation( + vaultTimeoutAction, + vaultTimeout, + useSecureStorage, + ); + // Assert + expect(result).toEqual(TokenStorageLocation.Memory); + }, + ); + + it("returns secure storage when the vault timeout action is logout and the vault timeout is never", async () => { + // Arrange + const vaultTimeoutAction = VaultTimeoutAction.LogOut; + const vaultTimeout: VaultTimeout = VaultTimeoutStringType.Never; + const useSecureStorage = true; + // Act + const result = await (tokenService as any).determineStorageLocation( + vaultTimeoutAction, + vaultTimeout, + useSecureStorage, + ); + // Assert + expect(result).toEqual(TokenStorageLocation.SecureStorage); + }); + + it("returns secure storage when the vault timeout action is lock and the vault timeout is never", async () => { + // Arrange + const vaultTimeoutAction = VaultTimeoutAction.Lock; + const vaultTimeout: VaultTimeout = VaultTimeoutStringType.Never; + const useSecureStorage = true; + // Act + const result = await (tokenService as any).determineStorageLocation( + vaultTimeoutAction, + vaultTimeout, + useSecureStorage, + ); + // Assert + expect(result).toEqual(TokenStorageLocation.SecureStorage); + }); + }); + }); + // Helpers function createTokenService(supportsSecureStorage: boolean) { return new TokenService( diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index 56311671ad..203d95429e 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -19,6 +19,7 @@ import { UserKeyDefinition, } from "../../platform/state"; import { UserId } from "../../types/guid"; +import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type"; import { TokenService as TokenServiceAbstraction } from "../abstractions/token.service"; import { ACCOUNT_ACTIVE_ACCOUNT_ID } from "./account.service"; @@ -159,7 +160,7 @@ export class TokenService implements TokenServiceAbstraction { async setTokens( accessToken: string, vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, refreshToken?: string, clientIdClientSecret?: [string, string], ): Promise { @@ -167,6 +168,15 @@ export class TokenService implements TokenServiceAbstraction { throw new Error("Access token is required."); } + // Can't check for falsey b/c 0 is a valid value + if (vaultTimeout == null) { + throw new Error("Vault Timeout is required."); + } + + if (vaultTimeoutAction == null) { + throw new Error("Vault Timeout Action is required."); + } + // get user id the access token const userId: UserId = await this.getUserIdFromAccessToken(accessToken); @@ -272,7 +282,7 @@ export class TokenService implements TokenServiceAbstraction { private async _setAccessToken( accessToken: string, vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, userId: UserId, ): Promise { const storageLocation = await this.determineStorageLocation( @@ -319,7 +329,7 @@ export class TokenService implements TokenServiceAbstraction { async setAccessToken( accessToken: string, vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, ): Promise { if (!accessToken) { throw new Error("Access token is required."); @@ -331,6 +341,15 @@ export class TokenService implements TokenServiceAbstraction { throw new Error("User id not found. Cannot save access token."); } + // Can't check for falsey b/c 0 is a valid value + if (vaultTimeout == null) { + throw new Error("Vault Timeout is required."); + } + + if (vaultTimeoutAction == null) { + throw new Error("Vault Timeout Action is required."); + } + await this._setAccessToken(accessToken, vaultTimeoutAction, vaultTimeout, userId); } @@ -413,7 +432,7 @@ export class TokenService implements TokenServiceAbstraction { private async setRefreshToken( refreshToken: string, vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, userId: UserId, ): Promise { // If we don't have a user id, we can't save the value @@ -421,6 +440,15 @@ export class TokenService implements TokenServiceAbstraction { throw new Error("User id not found. Cannot save refresh token."); } + // Can't check for falsey b/c 0 is a valid value + if (vaultTimeout == null) { + throw new Error("Vault Timeout is required."); + } + + if (vaultTimeoutAction == null) { + throw new Error("Vault Timeout Action is required."); + } + const storageLocation = await this.determineStorageLocation( vaultTimeoutAction, vaultTimeout, @@ -521,7 +549,7 @@ export class TokenService implements TokenServiceAbstraction { async setClientId( clientId: string, vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, userId?: UserId, ): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); @@ -531,6 +559,15 @@ export class TokenService implements TokenServiceAbstraction { throw new Error("User id not found. Cannot save client id."); } + // Can't check for falsey b/c 0 is a valid value + if (vaultTimeout == null) { + throw new Error("Vault Timeout is required."); + } + + if (vaultTimeoutAction == null) { + throw new Error("Vault Timeout Action is required."); + } + const storageLocation = await this.determineStorageLocation( vaultTimeoutAction, vaultTimeout, @@ -589,7 +626,7 @@ export class TokenService implements TokenServiceAbstraction { async setClientSecret( clientSecret: string, vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, userId?: UserId, ): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); @@ -598,6 +635,15 @@ export class TokenService implements TokenServiceAbstraction { throw new Error("User id not found. Cannot save client secret."); } + // Can't check for falsey b/c 0 is a valid value + if (vaultTimeout == null) { + throw new Error("Vault Timeout is required."); + } + + if (vaultTimeoutAction == null) { + throw new Error("Vault Timeout Action is required."); + } + const storageLocation = await this.determineStorageLocation( vaultTimeoutAction, vaultTimeout, @@ -885,10 +931,25 @@ export class TokenService implements TokenServiceAbstraction { private async determineStorageLocation( vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, useSecureStorage: boolean, ): Promise { - if (vaultTimeoutAction === VaultTimeoutAction.LogOut && vaultTimeout != null) { + if (vaultTimeoutAction == null) { + throw new Error( + "TokenService - determineStorageLocation: We expect the vault timeout action to always exist at this point.", + ); + } + + if (vaultTimeout == null) { + throw new Error( + "TokenService - determineStorageLocation: We expect the vault timeout to always exist at this point.", + ); + } + + if ( + vaultTimeoutAction === VaultTimeoutAction.LogOut && + vaultTimeout !== VaultTimeoutStringType.Never + ) { return TokenStorageLocation.Memory; } else { if (useSecureStorage && this.platformSupportsSecureStorage) { diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index f1073ea232..0f678a6bf3 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -118,8 +118,4 @@ export abstract class StateService { getGeneratorOptions: (options?: StorageOptions) => Promise; setGeneratorOptions: (value: GeneratorOptions, options?: StorageOptions) => Promise; getUserId: (options?: StorageOptions) => Promise; - getVaultTimeout: (options?: StorageOptions) => Promise; - setVaultTimeout: (value: number, options?: StorageOptions) => Promise; - getVaultTimeoutAction: (options?: StorageOptions) => Promise; - setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise; } diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 892d608931..8c7d70948c 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -147,8 +147,6 @@ export class AccountSettings { passwordGenerationOptions?: PasswordGeneratorOptions; usernameGenerationOptions?: UsernameGeneratorOptions; generatorOptions?: GeneratorOptions; - vaultTimeout?: number; - vaultTimeoutAction?: string = "lock"; static fromJSON(obj: Jsonify): AccountSettings { if (obj == null) { diff --git a/libs/common/src/platform/models/domain/global-state.ts b/libs/common/src/platform/models/domain/global-state.ts index cd7cf7d174..2d48f09e92 100644 --- a/libs/common/src/platform/models/domain/global-state.ts +++ b/libs/common/src/platform/models/domain/global-state.ts @@ -1,7 +1,5 @@ export class GlobalState { organizationInvitation?: any; - vaultTimeout?: number; - vaultTimeoutAction?: string; enableBrowserIntegration?: boolean; enableBrowserIntegrationFingerprint?: boolean; enableDuckDuckGoBrowserIntegration?: boolean; diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index 5bf8b57e0f..8bb1289419 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/common/src/platform/services/crypto.service.spec.ts @@ -7,9 +7,11 @@ import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-sta import { FakeStateProvider } from "../../../spec/fake-state-provider"; import { KdfConfigService } from "../../auth/abstractions/kdf-config.service"; import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service"; +import { VAULT_TIMEOUT } from "../../services/vault-timeout/vault-timeout-settings.state"; import { CsprngArray } from "../../types/csprng"; import { UserId } from "../../types/guid"; import { UserKey, MasterKey } from "../../types/key"; +import { VaultTimeoutStringType } from "../../types/vault-timeout.type"; import { CryptoFunctionService } from "../abstractions/crypto-function.service"; import { EncryptService } from "../abstractions/encrypt.service"; import { KeyGenerationService } from "../abstractions/key-generation.service"; @@ -220,8 +222,8 @@ describe("cryptoService", () => { }); describe("Auto Key refresh", () => { - it("sets an Auto key if vault timeout is set to null", async () => { - stateService.getVaultTimeout.mockResolvedValue(null); + it("sets an Auto key if vault timeout is set to 'never'", async () => { + await stateProvider.setUserState(VAULT_TIMEOUT, VaultTimeoutStringType.Never, mockUserId); await cryptoService.setUserKey(mockUserKey, mockUserId); @@ -231,7 +233,7 @@ describe("cryptoService", () => { }); it("clears the Auto key if vault timeout is set to anything other than null", async () => { - stateService.getVaultTimeout.mockResolvedValue(10); + await stateProvider.setUserState(VAULT_TIMEOUT, 10, mockUserId); await cryptoService.setUserKey(mockUserKey, mockUserId); diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 2ac8b0f0f1..fed22e06a0 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -11,6 +11,7 @@ import { KdfConfigService } from "../../auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { Utils } from "../../platform/misc/utils"; +import { VAULT_TIMEOUT } from "../../services/vault-timeout/vault-timeout-settings.state"; import { CsprngArray } from "../../types/csprng"; import { OrganizationId, ProviderId, UserId } from "../../types/guid"; import { @@ -22,6 +23,7 @@ import { UserPrivateKey, UserPublicKey, } from "../../types/key"; +import { VaultTimeoutStringType } from "../../types/vault-timeout.type"; import { CryptoFunctionService } from "../abstractions/crypto-function.service"; import { CryptoService as CryptoServiceAbstraction } from "../abstractions/crypto.service"; import { EncryptService } from "../abstractions/encrypt.service"; @@ -773,8 +775,14 @@ export class CryptoService implements CryptoServiceAbstraction { let shouldStoreKey = false; switch (keySuffix) { case KeySuffixOptions.Auto: { - const vaultTimeout = await this.stateService.getVaultTimeout({ userId: userId }); - shouldStoreKey = vaultTimeout == null; + // TODO: Sharing the UserKeyDefinition is temporary to get around a circ dep issue between + // the VaultTimeoutSettingsSvc and this service. + // This should be fixed as part of the PM-7082 - Auto Key Service work. + const vaultTimeout = await firstValueFrom( + this.stateProvider.getUserState$(VAULT_TIMEOUT, userId), + ); + + shouldStoreKey = vaultTimeout == VaultTimeoutStringType.Never; break; } case KeySuffixOptions.Pin: { diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 179286a097..497e6e6703 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -571,49 +571,6 @@ export class StateService< )?.profile?.userId; } - async getVaultTimeout(options?: StorageOptions): Promise { - const accountVaultTimeout = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.settings?.vaultTimeout; - return accountVaultTimeout; - } - - async setVaultTimeout(value: number, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - account.settings.vaultTimeout = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - - async getVaultTimeoutAction(options?: StorageOptions): Promise { - const accountVaultTimeoutAction = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.settings?.vaultTimeoutAction; - return ( - accountVaultTimeoutAction ?? - ( - await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ) - )?.vaultTimeoutAction - ); - } - - async setVaultTimeoutAction(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - account.settings.vaultTimeoutAction = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - protected async getGlobals(options: StorageOptions): Promise { let globals: TGlobalState; if (this.useMemory(options.storageLocation)) { diff --git a/libs/common/src/platform/services/system.service.ts b/libs/common/src/platform/services/system.service.ts index 2047001e36..b25898ab7c 100644 --- a/libs/common/src/platform/services/system.service.ts +++ b/libs/common/src/platform/services/system.service.ts @@ -73,23 +73,25 @@ export class SystemService implements SystemServiceAbstraction { clearInterval(this.reloadInterval); this.reloadInterval = null; - const currentUser = await firstValueFrom( + const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe( map((a) => a?.id), timeout(500), ), ); // Replace current active user if they will be logged out on reload - if (currentUser != null) { + if (activeUserId != null) { const timeoutAction = await firstValueFrom( - this.vaultTimeoutSettingsService.vaultTimeoutAction$().pipe(timeout(500)), + this.vaultTimeoutSettingsService + .getVaultTimeoutActionByUserId$(activeUserId) + .pipe(timeout(500)), // safety feature to avoid this call hanging and stopping process reload from clearing memory ); if (timeoutAction === VaultTimeoutAction.LogOut) { const nextUser = await firstValueFrom( this.accountService.nextUpAccount$.pipe(map((account) => account?.id ?? null)), ); // Can be removed once we migrate password generation history to state providers - await this.stateService.clearDecryptedData(currentUser); + await this.stateService.clearDecryptedData(activeUserId); await this.accountService.switchAccount(nextUser); } } diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index b288b8e19d..6f225f6c2f 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -63,6 +63,13 @@ export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", { export const TOKEN_MEMORY = new StateDefinition("token", "memory"); export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory"); export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk"); +export const VAULT_TIMEOUT_SETTINGS_DISK_LOCAL = new StateDefinition( + "vaultTimeoutSettings", + "disk", + { + web: "disk-local", + }, +); // Autofill diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 84fa7bd077..4620a2ccde 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -1,6 +1,7 @@ import { firstValueFrom } from "rxjs"; import { ApiService as ApiServiceAbstraction } from "../abstractions/api.service"; +import { VaultTimeoutSettingsService } from "../abstractions/vault-timeout/vault-timeout-settings.service"; import { OrganizationConnectionType } from "../admin-console/enums"; import { OrganizationSponsorshipCreateRequest } from "../admin-console/models/request/organization/organization-sponsorship-create.request"; import { OrganizationSponsorshipRedeemRequest } from "../admin-console/models/request/organization/organization-sponsorship-redeem.request"; @@ -116,7 +117,6 @@ import { UserKeyResponse } from "../models/response/user-key.response"; import { AppIdService } from "../platform/abstractions/app-id.service"; import { EnvironmentService } from "../platform/abstractions/environment.service"; import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service"; -import { StateService } from "../platform/abstractions/state.service"; import { Utils } from "../platform/misc/utils"; import { UserId } from "../types/guid"; import { AttachmentRequest } from "../vault/models/request/attachment.request"; @@ -156,7 +156,7 @@ export class ApiService implements ApiServiceAbstraction { private platformUtilsService: PlatformUtilsService, private environmentService: EnvironmentService, private appIdService: AppIdService, - private stateService: StateService, + private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private logoutCallback: (expired: boolean) => Promise, private customUserAgent: string = null, ) { @@ -1750,8 +1750,17 @@ export class ApiService implements ApiServiceAbstraction { const responseJson = await response.json(); const tokenResponse = new IdentityTokenResponse(responseJson); - const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction(); - const vaultTimeout = await this.stateService.getVaultTimeout(); + const newDecodedAccessToken = await this.tokenService.decodeAccessToken( + tokenResponse.accessToken, + ); + const userId = newDecodedAccessToken.sub; + + const vaultTimeoutAction = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId), + ); + const vaultTimeout = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId), + ); await this.tokenService.setTokens( tokenResponse.accessToken, @@ -1783,8 +1792,15 @@ export class ApiService implements ApiServiceAbstraction { throw new Error("Invalid response received when refreshing api token"); } - const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction(); - const vaultTimeout = await this.stateService.getVaultTimeout(); + const newDecodedAccessToken = await this.tokenService.decodeAccessToken(response.accessToken); + const userId = newDecodedAccessToken.sub; + + const vaultTimeoutAction = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId), + ); + const vaultTimeout = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId), + ); await this.tokenService.setAccessToken( response.accessToken, diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts index 776fa46e06..894d550bda 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts @@ -9,14 +9,20 @@ import { import { Utils } from "@bitwarden/common/platform/misc/utils"; import { UserId } from "@bitwarden/common/types/guid"; -import { FakeAccountService, mockAccountServiceWith } from "../../../spec"; +import { FakeAccountService, mockAccountServiceWith, FakeStateProvider } from "../../../spec"; +import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; import { Policy } from "../../admin-console/models/domain/policy"; import { TokenService } from "../../auth/abstractions/token.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { CryptoService } from "../../platform/abstractions/crypto.service"; -import { StateService } from "../../platform/abstractions/state.service"; +import { LogService } from "../../platform/abstractions/log.service"; import { BiometricStateService } from "../../platform/biometrics/biometric-state.service"; +import { + VAULT_TIMEOUT, + VAULT_TIMEOUT_ACTION, +} from "../../services/vault-timeout/vault-timeout-settings.state"; +import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type"; import { VaultTimeoutSettingsService } from "./vault-timeout-settings.service"; @@ -27,13 +33,14 @@ describe("VaultTimeoutSettingsService", () => { let cryptoService: MockProxy; let tokenService: MockProxy; let policyService: MockProxy; - let stateService: MockProxy; const biometricStateService = mock(); - let service: VaultTimeoutSettingsService; + let vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction; let userDecryptionOptionsSubject: BehaviorSubject; const mockUserId = Utils.newGuid() as UserId; + let stateProvider: FakeStateProvider; + let logService: MockProxy; beforeEach(() => { accountService = mockAccountServiceWith(mockUserId); @@ -42,7 +49,6 @@ describe("VaultTimeoutSettingsService", () => { cryptoService = mock(); tokenService = mock(); policyService = mock(); - stateService = mock(); userDecryptionOptionsSubject = new BehaviorSubject(null); userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject; @@ -53,16 +59,13 @@ describe("VaultTimeoutSettingsService", () => { userDecryptionOptionsSubject, ); - service = new VaultTimeoutSettingsService( - accountService, - pinService, - userDecryptionOptionsService, - cryptoService, - tokenService, - policyService, - stateService, - biometricStateService, - ); + accountService = mockAccountServiceWith(mockUserId); + stateProvider = new FakeStateProvider(accountService); + + logService = mock(); + + const defaultVaultTimeout: VaultTimeout = 15; // default web vault timeout + vaultTimeoutSettingsService = createVaultTimeoutSettingsService(defaultVaultTimeout); biometricStateService.biometricUnlockEnabled$ = of(false); }); @@ -73,7 +76,9 @@ describe("VaultTimeoutSettingsService", () => { describe("availableVaultTimeoutActions$", () => { it("always returns LogOut", async () => { - const result = await firstValueFrom(service.availableVaultTimeoutActions$()); + const result = await firstValueFrom( + vaultTimeoutSettingsService.availableVaultTimeoutActions$(), + ); expect(result).toContain(VaultTimeoutAction.LogOut); }); @@ -81,7 +86,9 @@ describe("VaultTimeoutSettingsService", () => { it("contains Lock when the user has a master password", async () => { userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true })); - const result = await firstValueFrom(service.availableVaultTimeoutActions$()); + const result = await firstValueFrom( + vaultTimeoutSettingsService.availableVaultTimeoutActions$(), + ); expect(result).toContain(VaultTimeoutAction.Lock); }); @@ -89,7 +96,9 @@ describe("VaultTimeoutSettingsService", () => { it("contains Lock when the user has either a persistent or ephemeral PIN configured", async () => { pinService.isPinSet.mockResolvedValue(true); - const result = await firstValueFrom(service.availableVaultTimeoutActions$()); + const result = await firstValueFrom( + vaultTimeoutSettingsService.availableVaultTimeoutActions$(), + ); expect(result).toContain(VaultTimeoutAction.Lock); }); @@ -98,7 +107,9 @@ describe("VaultTimeoutSettingsService", () => { biometricStateService.biometricUnlockEnabled$ = of(true); biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(true); - const result = await firstValueFrom(service.availableVaultTimeoutActions$()); + const result = await firstValueFrom( + vaultTimeoutSettingsService.availableVaultTimeoutActions$(), + ); expect(result).toContain(VaultTimeoutAction.Lock); }); @@ -108,13 +119,21 @@ describe("VaultTimeoutSettingsService", () => { pinService.isPinSet.mockResolvedValue(false); biometricStateService.biometricUnlockEnabled$ = of(false); - const result = await firstValueFrom(service.availableVaultTimeoutActions$()); + const result = await firstValueFrom( + vaultTimeoutSettingsService.availableVaultTimeoutActions$(), + ); expect(result).not.toContain(VaultTimeoutAction.Lock); }); }); - describe("vaultTimeoutAction$", () => { + describe("getVaultTimeoutActionByUserId$", () => { + it("should throw an error if no user id is provided", async () => { + expect(() => vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(null)).toThrow( + "User id required. Cannot get vault timeout action.", + ); + }); + describe("given the user has a master password", () => { it.each` policy | userPreference | expected @@ -129,9 +148,12 @@ describe("VaultTimeoutSettingsService", () => { policyService.getAll$.mockReturnValue( of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])), ); - stateService.getVaultTimeoutAction.mockResolvedValue(userPreference); - const result = await firstValueFrom(service.vaultTimeoutAction$()); + await stateProvider.setUserState(VAULT_TIMEOUT_ACTION, userPreference, mockUserId); + + const result = await firstValueFrom( + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(mockUserId), + ); expect(result).toBe(expected); }, @@ -140,19 +162,23 @@ describe("VaultTimeoutSettingsService", () => { describe("given the user does not have a master password", () => { it.each` - unlockMethod | policy | userPreference | expected - ${false} | ${null} | ${null} | ${VaultTimeoutAction.LogOut} - ${false} | ${null} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut} - ${false} | ${VaultTimeoutAction.Lock} | ${null} | ${VaultTimeoutAction.LogOut} - ${true} | ${null} | ${null} | ${VaultTimeoutAction.LogOut} - ${true} | ${null} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.Lock} - ${true} | ${VaultTimeoutAction.Lock} | ${null} | ${VaultTimeoutAction.Lock} - ${true} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut} | ${VaultTimeoutAction.Lock} + hasPinUnlock | hasBiometricUnlock | policy | userPreference | expected + ${false} | ${false} | ${null} | ${null} | ${VaultTimeoutAction.LogOut} + ${false} | ${false} | ${null} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut} + ${false} | ${false} | ${VaultTimeoutAction.Lock} | ${null} | ${VaultTimeoutAction.LogOut} + ${false} | ${true} | ${null} | ${null} | ${VaultTimeoutAction.Lock} + ${false} | ${true} | ${null} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.Lock} + ${false} | ${true} | ${VaultTimeoutAction.Lock} | ${null} | ${VaultTimeoutAction.Lock} + ${false} | ${true} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut} | ${VaultTimeoutAction.Lock} + ${true} | ${false} | ${null} | ${null} | ${VaultTimeoutAction.Lock} + ${true} | ${false} | ${null} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.Lock} + ${true} | ${false} | ${VaultTimeoutAction.Lock} | ${null} | ${VaultTimeoutAction.Lock} + ${true} | ${false} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut} | ${VaultTimeoutAction.Lock} `( - "returns $expected when policy is $policy, has unlock method is $unlockMethod, and user preference is $userPreference", - async ({ unlockMethod, policy, userPreference, expected }) => { - biometricStateService.biometricUnlockEnabled$ = of(unlockMethod); - biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(unlockMethod); + "returns $expected when policy is $policy, has PIN unlock method: $hasPinUnlock or Biometric unlock method: $hasBiometricUnlock, and user preference is $userPreference", + async ({ hasPinUnlock, hasBiometricUnlock, policy, userPreference, expected }) => { + biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(hasBiometricUnlock); + pinService.isPinSet.mockResolvedValue(hasPinUnlock); userDecryptionOptionsSubject.next( new UserDecryptionOptions({ hasMasterPassword: false }), @@ -160,13 +186,160 @@ describe("VaultTimeoutSettingsService", () => { policyService.getAll$.mockReturnValue( of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])), ); - stateService.getVaultTimeoutAction.mockResolvedValue(userPreference); - const result = await firstValueFrom(service.vaultTimeoutAction$()); + await stateProvider.setUserState(VAULT_TIMEOUT_ACTION, userPreference, mockUserId); + + const result = await firstValueFrom( + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(mockUserId), + ); expect(result).toBe(expected); }, ); }); }); + + describe("getVaultTimeoutByUserId$", () => { + it("should throw an error if no user id is provided", async () => { + expect(() => vaultTimeoutSettingsService.getVaultTimeoutByUserId$(null)).toThrow( + "User id required. Cannot get vault timeout.", + ); + }); + + it.each([ + // policy, vaultTimeout, expected + [null, null, 15], // no policy, no vault timeout, falls back to default + [30, 90, 30], // policy overrides vault timeout + [30, 15, 15], // policy doesn't override vault timeout when it's within acceptable range + [90, VaultTimeoutStringType.Never, 90], // policy overrides vault timeout when it's "never" + [null, VaultTimeoutStringType.Never, VaultTimeoutStringType.Never], // no policy, persist "never" vault timeout + [90, 0, 0], // policy doesn't override vault timeout when it's 0 (immediate) + [null, 0, 0], // no policy, persist 0 (immediate) vault timeout + [90, VaultTimeoutStringType.OnRestart, 90], // policy overrides vault timeout when it's "onRestart" + [null, VaultTimeoutStringType.OnRestart, VaultTimeoutStringType.OnRestart], // no policy, persist "onRestart" vault timeout + [90, VaultTimeoutStringType.OnLocked, 90], // policy overrides vault timeout when it's "onLocked" + [null, VaultTimeoutStringType.OnLocked, VaultTimeoutStringType.OnLocked], // no policy, persist "onLocked" vault timeout + [90, VaultTimeoutStringType.OnSleep, 90], // policy overrides vault timeout when it's "onSleep" + [null, VaultTimeoutStringType.OnSleep, VaultTimeoutStringType.OnSleep], // no policy, persist "onSleep" vault timeout + [90, VaultTimeoutStringType.OnIdle, 90], // policy overrides vault timeout when it's "onIdle" + [null, VaultTimeoutStringType.OnIdle, VaultTimeoutStringType.OnIdle], // no policy, persist "onIdle" vault timeout + ])( + "when policy is %s, and vault timeout is %s, returns %s", + async (policy, vaultTimeout, expected) => { + userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true })); + policyService.getAll$.mockReturnValue( + of(policy === null ? [] : ([{ data: { minutes: policy } }] as unknown as Policy[])), + ); + + await stateProvider.setUserState(VAULT_TIMEOUT, vaultTimeout, mockUserId); + + const result = await firstValueFrom( + vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId), + ); + + expect(result).toBe(expected); + }, + ); + }); + + describe("setVaultTimeoutOptions", () => { + const mockAccessToken = "mockAccessToken"; + const mockRefreshToken = "mockRefreshToken"; + const mockClientId = "mockClientId"; + const mockClientSecret = "mockClientSecret"; + + it("should throw an error if no user id is provided", async () => { + // note: don't await here because we want to test the error + const result = vaultTimeoutSettingsService.setVaultTimeoutOptions(null, null, null); + // Assert + await expect(result).rejects.toThrow("User id required. Cannot set vault timeout settings."); + }); + + it("should not throw an error if 0 is provided as the timeout", async () => { + // note: don't await here because we want to test the error + const result = vaultTimeoutSettingsService.setVaultTimeoutOptions( + mockUserId, + 0, + VaultTimeoutAction.Lock, + ); + // Assert + await expect(result).resolves.not.toThrow(); + }); + + it("should throw an error if a null vault timeout is provided", async () => { + // note: don't await here because we want to test the error + const result = vaultTimeoutSettingsService.setVaultTimeoutOptions(mockUserId, null, null); + // Assert + await expect(result).rejects.toThrow("Vault Timeout cannot be null."); + }); + + it("should throw an error if a null vault timout action is provided", async () => { + // note: don't await here because we want to test the error + const result = vaultTimeoutSettingsService.setVaultTimeoutOptions(mockUserId, 30, null); + // Assert + await expect(result).rejects.toThrow("Vault Timeout Action cannot be null."); + }); + + it("should set the vault timeout options for the given user", async () => { + // Arrange + tokenService.getAccessToken.mockResolvedValue(mockAccessToken); + tokenService.getRefreshToken.mockResolvedValue(mockRefreshToken); + tokenService.getClientId.mockResolvedValue(mockClientId); + tokenService.getClientSecret.mockResolvedValue(mockClientSecret); + + const action = VaultTimeoutAction.Lock; + const timeout = 30; + + // Act + await vaultTimeoutSettingsService.setVaultTimeoutOptions(mockUserId, timeout, action); + + // Assert + expect(tokenService.setTokens).toHaveBeenCalledWith( + mockAccessToken, + action, + timeout, + mockRefreshToken, + [mockClientId, mockClientSecret], + ); + + expect( + stateProvider.singleUser.getFake(mockUserId, VAULT_TIMEOUT_ACTION).nextMock, + ).toHaveBeenCalledWith(action); + + expect( + stateProvider.singleUser.getFake(mockUserId, VAULT_TIMEOUT).nextMock, + ).toHaveBeenCalledWith(timeout); + + expect(cryptoService.refreshAdditionalKeys).toHaveBeenCalled(); + }); + + it("should clear the tokens when the timeout is non-null and the action is log out", async () => { + // Arrange + const action = VaultTimeoutAction.LogOut; + const timeout = 30; + + // Act + await vaultTimeoutSettingsService.setVaultTimeoutOptions(mockUserId, timeout, action); + + // Assert + expect(tokenService.clearTokens).toHaveBeenCalled(); + }); + }); + + function createVaultTimeoutSettingsService( + defaultVaultTimeout: VaultTimeout, + ): VaultTimeoutSettingsService { + return new VaultTimeoutSettingsService( + accountService, + pinService, + userDecryptionOptionsService, + cryptoService, + tokenService, + policyService, + biometricStateService, + stateProvider, + logService, + defaultVaultTimeout, + ); + } }); diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts index ca763667ac..282b86fb63 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts @@ -1,4 +1,17 @@ -import { defer, firstValueFrom } from "rxjs"; +import { + EMPTY, + Observable, + catchError, + combineLatest, + defer, + distinctUntilChanged, + firstValueFrom, + from, + map, + shareReplay, + switchMap, + tap, +} from "rxjs"; import { PinServiceAbstraction, @@ -8,13 +21,18 @@ import { import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "../../admin-console/enums"; +import { Policy } from "../../admin-console/models/domain/policy"; import { AccountService } from "../../auth/abstractions/account.service"; import { TokenService } from "../../auth/abstractions/token.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { CryptoService } from "../../platform/abstractions/crypto.service"; -import { StateService } from "../../platform/abstractions/state.service"; +import { LogService } from "../../platform/abstractions/log.service"; import { BiometricStateService } from "../../platform/biometrics/biometric-state.service"; +import { StateProvider } from "../../platform/state"; import { UserId } from "../../types/guid"; +import { VaultTimeout } from "../../types/vault-timeout.type"; + +import { VAULT_TIMEOUT, VAULT_TIMEOUT_ACTION } from "./vault-timeout-settings.state"; export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceAbstraction { constructor( @@ -24,11 +42,29 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA private cryptoService: CryptoService, private tokenService: TokenService, private policyService: PolicyService, - private stateService: StateService, private biometricStateService: BiometricStateService, + private stateProvider: StateProvider, + private logService: LogService, + private defaultVaultTimeout: VaultTimeout, ) {} - async setVaultTimeoutOptions(timeout: number, action: VaultTimeoutAction): Promise { + async setVaultTimeoutOptions( + userId: UserId, + timeout: VaultTimeout, + action: VaultTimeoutAction, + ): Promise { + if (!userId) { + throw new Error("User id required. Cannot set vault timeout settings."); + } + + if (timeout == null) { + throw new Error("Vault Timeout cannot be null."); + } + + if (action == null) { + throw new Error("Vault Timeout Action cannot be null."); + } + // We swap these tokens from being on disk for lock actions, and in memory for logout actions // Get them here to set them to their new location after changing the timeout action and clearing if needed const accessToken = await this.tokenService.getAccessToken(); @@ -36,20 +72,15 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA const clientId = await this.tokenService.getClientId(); const clientSecret = await this.tokenService.getClientSecret(); - await this.stateService.setVaultTimeout(timeout); + await this.setVaultTimeout(userId, timeout); - const currentAction = await this.stateService.getVaultTimeoutAction(); - - if ( - (timeout != null || timeout === 0) && - action === VaultTimeoutAction.LogOut && - action !== currentAction - ) { + if (timeout != null && action === VaultTimeoutAction.LogOut) { // if we have a vault timeout and the action is log out, reset tokens + // as the tokens were stored on disk and now should be stored in memory await this.tokenService.clearTokens(); } - await this.stateService.setVaultTimeoutAction(action); + await this.setVaultTimeoutAction(userId, action); await this.tokenService.setTokens(accessToken, action, timeout, refreshToken, [ clientId, @@ -71,72 +102,164 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA return await biometricUnlockPromise; } - async getVaultTimeout(userId?: UserId): Promise { - const vaultTimeout = await this.stateService.getVaultTimeout({ userId }); - const policies = await firstValueFrom( - this.policyService.getAll$(PolicyType.MaximumVaultTimeout, userId), - ); - - if (policies?.length) { - // Remove negative values, and ensure it's smaller than maximum allowed value according to policy - let timeout = Math.min(vaultTimeout, policies[0].data.minutes); - - if (vaultTimeout == null || timeout < 0) { - timeout = policies[0].data.minutes; - } - - // TODO @jlf0dev: Can we move this somwhere else? Maybe add it to the initialization process? - // ( Apparently I'm the one that reviewed the original PR that added this :) ) - // We really shouldn't need to set the value here, but multiple services relies on this value being correct. - if (vaultTimeout !== timeout) { - await this.stateService.setVaultTimeout(timeout, { userId }); - } - - return timeout; + private async setVaultTimeout(userId: UserId, timeout: VaultTimeout): Promise { + if (!userId) { + throw new Error("User id required. Cannot set vault timeout."); } - return vaultTimeout; + if (timeout == null) { + throw new Error("Vault Timeout cannot be null."); + } + + await this.stateProvider.setUserState(VAULT_TIMEOUT, timeout, userId); } - vaultTimeoutAction$(userId?: UserId) { - return defer(() => this.getVaultTimeoutAction(userId)); + getVaultTimeoutByUserId$(userId: UserId): Observable { + if (!userId) { + throw new Error("User id required. Cannot get vault timeout."); + } + + return combineLatest([ + this.stateProvider.getUserState$(VAULT_TIMEOUT, userId), + this.getMaxVaultTimeoutPolicyByUserId$(userId), + ]).pipe( + switchMap(([currentVaultTimeout, maxVaultTimeoutPolicy]) => { + return from(this.determineVaultTimeout(currentVaultTimeout, maxVaultTimeoutPolicy)).pipe( + tap((vaultTimeout: VaultTimeout) => { + // As a side effect, set the new value determined by determineVaultTimeout into state if it's different from the current + if (vaultTimeout !== currentVaultTimeout) { + return this.stateProvider.setUserState(VAULT_TIMEOUT, vaultTimeout, userId); + } + }), + catchError((error: unknown) => { + // Protect outer observable from canceling on error by catching and returning EMPTY + this.logService.error(`Error getting vault timeout: ${error}`); + return EMPTY; + }), + ); + }), + distinctUntilChanged(), // Avoid having the set side effect trigger a new emission of the same action + shareReplay({ refCount: true, bufferSize: 1 }), + ); } - async getVaultTimeoutAction(userId?: UserId): Promise { - const availableActions = await this.getAvailableVaultTimeoutActions(); - if (availableActions.length === 1) { - return availableActions[0]; + private async determineVaultTimeout( + currentVaultTimeout: VaultTimeout | null, + maxVaultTimeoutPolicy: Policy | null, + ): Promise { + // if current vault timeout is null, apply the client specific default + currentVaultTimeout = currentVaultTimeout ?? this.defaultVaultTimeout; + + // If no policy applies, return the current vault timeout + if (!maxVaultTimeoutPolicy) { + return currentVaultTimeout; } - const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId: userId }); - const policies = await firstValueFrom( - this.policyService.getAll$(PolicyType.MaximumVaultTimeout, userId), + // User is subject to a max vault timeout policy + const maxVaultTimeoutPolicyData = maxVaultTimeoutPolicy.data; + + // If the current vault timeout is not numeric, change it to the policy compliant value + if (typeof currentVaultTimeout === "string") { + return maxVaultTimeoutPolicyData.minutes; + } + + // For numeric vault timeouts, ensure they are smaller than maximum allowed value according to policy + const policyCompliantTimeout = Math.min(currentVaultTimeout, maxVaultTimeoutPolicyData.minutes); + + return policyCompliantTimeout; + } + + private async setVaultTimeoutAction(userId: UserId, action: VaultTimeoutAction): Promise { + if (!userId) { + throw new Error("User id required. Cannot set vault timeout action."); + } + + if (!action) { + throw new Error("Vault Timeout Action cannot be null"); + } + + await this.stateProvider.setUserState(VAULT_TIMEOUT_ACTION, action, userId); + } + + getVaultTimeoutActionByUserId$(userId: UserId): Observable { + if (!userId) { + throw new Error("User id required. Cannot get vault timeout action."); + } + + return combineLatest([ + this.stateProvider.getUserState$(VAULT_TIMEOUT_ACTION, userId), + this.getMaxVaultTimeoutPolicyByUserId$(userId), + ]).pipe( + switchMap(([currentVaultTimeoutAction, maxVaultTimeoutPolicy]) => { + return from( + this.determineVaultTimeoutAction( + userId, + currentVaultTimeoutAction, + maxVaultTimeoutPolicy, + ), + ).pipe( + tap((vaultTimeoutAction: VaultTimeoutAction) => { + // As a side effect, set the new value determined by determineVaultTimeout into state if it's different from the current + // We want to avoid having a null timeout action always so we set it to the default if it is null + // and if the user becomes subject to a policy that requires a specific action, we set it to that + if (vaultTimeoutAction !== currentVaultTimeoutAction) { + return this.stateProvider.setUserState( + VAULT_TIMEOUT_ACTION, + vaultTimeoutAction, + userId, + ); + } + }), + catchError((error: unknown) => { + // Protect outer observable from canceling on error by catching and returning EMPTY + this.logService.error(`Error getting vault timeout: ${error}`); + return EMPTY; + }), + ); + }), + distinctUntilChanged(), // Avoid having the set side effect trigger a new emission of the same action + shareReplay({ refCount: true, bufferSize: 1 }), ); + } - if (policies?.length) { - const action = policies[0].data.action; - // We really shouldn't need to set the value here, but multiple services relies on this value being correct. - if (action && vaultTimeoutAction !== action) { - await this.stateService.setVaultTimeoutAction(action, { userId: userId }); - } - if (action && availableActions.includes(action)) { - return action; - } + private async determineVaultTimeoutAction( + userId: string, + currentVaultTimeoutAction: VaultTimeoutAction | null, + maxVaultTimeoutPolicy: Policy | null, + ): Promise { + const availableVaultTimeoutActions = await this.getAvailableVaultTimeoutActions(userId); + if (availableVaultTimeoutActions.length === 1) { + return availableVaultTimeoutActions[0]; } - if (vaultTimeoutAction == null) { - // Depends on whether or not the user has a master password - const defaultValue = (await this.userHasMasterPassword(userId)) - ? VaultTimeoutAction.Lock - : VaultTimeoutAction.LogOut; - // We really shouldn't need to set the value here, but multiple services relies on this value being correct. - await this.stateService.setVaultTimeoutAction(defaultValue, { userId: userId }); - return defaultValue; + if ( + maxVaultTimeoutPolicy?.data?.action && + availableVaultTimeoutActions.includes(maxVaultTimeoutPolicy.data.action) + ) { + // return policy defined vault timeout action + return maxVaultTimeoutPolicy.data.action; } - return vaultTimeoutAction === VaultTimeoutAction.LogOut - ? VaultTimeoutAction.LogOut - : VaultTimeoutAction.Lock; + // No policy applies from here on + // If the current vault timeout is null and lock is an option, set it as the default + if ( + currentVaultTimeoutAction == null && + availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock) + ) { + return VaultTimeoutAction.Lock; + } + + return currentVaultTimeoutAction; + } + + private getMaxVaultTimeoutPolicyByUserId$(userId: UserId): Observable { + if (!userId) { + throw new Error("User id required. Cannot get max vault timeout policy."); + } + + return this.policyService + .getAll$(PolicyType.MaximumVaultTimeout, userId) + .pipe(map((policies) => policies[0] ?? null)); } private async getAvailableVaultTimeoutActions(userId?: string): Promise { @@ -166,10 +289,9 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), ); - if (decryptionOptions?.hasMasterPassword != undefined) { - return decryptionOptions.hasMasterPassword; - } + return !!decryptionOptions?.hasMasterPassword; + } else { + return await firstValueFrom(this.userDecryptionOptionsService.hasMasterPassword$); } - return await firstValueFrom(this.userDecryptionOptionsService.hasMasterPassword$); } } diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.state.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.state.spec.ts new file mode 100644 index 0000000000..42a82e67ee --- /dev/null +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.state.spec.ts @@ -0,0 +1,36 @@ +import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { UserKeyDefinition } from "../../platform/state"; +import { VaultTimeout } from "../../types/vault-timeout.type"; + +import { VAULT_TIMEOUT, VAULT_TIMEOUT_ACTION } from "./vault-timeout-settings.state"; + +describe.each([ + [VAULT_TIMEOUT_ACTION, VaultTimeoutAction.Lock], + [VAULT_TIMEOUT, 5], +])( + "deserializes state key definitions", + ( + keyDefinition: UserKeyDefinition | UserKeyDefinition, + state: VaultTimeoutAction | VaultTimeout | boolean, + ) => { + function getTypeDescription(value: any): string { + if (Array.isArray(value)) { + return "array"; + } else if (value === null) { + return "null"; + } + + // Fallback for primitive types + return typeof value; + } + + function testDeserialization(keyDefinition: UserKeyDefinition, state: T) { + const deserialized = keyDefinition.deserializer(JSON.parse(JSON.stringify(state))); + expect(deserialized).toEqual(state); + } + + it(`should deserialize state for KeyDefinition<${getTypeDescription(state)}>: "${keyDefinition.key}"`, () => { + testDeserialization(keyDefinition, state); + }); + }, +); diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.state.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.state.ts new file mode 100644 index 0000000000..46097d6a4c --- /dev/null +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.state.ts @@ -0,0 +1,27 @@ +import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { UserKeyDefinition, VAULT_TIMEOUT_SETTINGS_DISK_LOCAL } from "../../platform/state"; +import { VaultTimeout } from "../../types/vault-timeout.type"; + +/** + * Settings use disk storage and local storage on web so settings can persist after logout + * in order for us to know if the user's chose to never lock their vault or not. + * When the user has never lock selected, we have to set the user key in memory + * from the user auto unlock key stored on disk on client bootstrap. + */ +export const VAULT_TIMEOUT_ACTION = new UserKeyDefinition( + VAULT_TIMEOUT_SETTINGS_DISK_LOCAL, + "vaultTimeoutAction", + { + deserializer: (vaultTimeoutAction) => vaultTimeoutAction, + clearOn: [], // persisted on logout + }, +); + +export const VAULT_TIMEOUT = new UserKeyDefinition( + VAULT_TIMEOUT_SETTINGS_DISK_LOCAL, + "vaultTimeout", + { + deserializer: (vaultTimeout) => vaultTimeout, + clearOn: [], // persisted on logout + }, +); diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts index 14b26fa541..51fca6e666 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts @@ -15,6 +15,7 @@ import { StateService } from "../../platform/abstractions/state.service"; import { Utils } from "../../platform/misc/utils"; import { StateEventRunnerService } from "../../platform/state"; import { UserId } from "../../types/guid"; +import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type"; import { CipherService } from "../../vault/abstractions/cipher.service"; import { CollectionService } from "../../vault/abstractions/collection.service"; import { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction"; @@ -63,7 +64,9 @@ describe("VaultTimeoutService", () => { vaultTimeoutActionSubject = new BehaviorSubject(VaultTimeoutAction.Lock); - vaultTimeoutSettingsService.vaultTimeoutAction$.mockReturnValue(vaultTimeoutActionSubject); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + vaultTimeoutActionSubject, + ); availableVaultTimeoutActionsSubject = new BehaviorSubject([]); @@ -93,7 +96,7 @@ describe("VaultTimeoutService", () => { authStatus?: AuthenticationStatus; isAuthenticated?: boolean; lastActive?: number; - vaultTimeout?: number; + vaultTimeout?: VaultTimeout; timeoutAction?: VaultTimeoutAction; availableTimeoutActions?: VaultTimeoutAction[]; } @@ -121,8 +124,8 @@ describe("VaultTimeoutService", () => { return Promise.resolve(accounts[options.userId ?? globalSetups?.userId]?.isAuthenticated); }); - vaultTimeoutSettingsService.getVaultTimeout.mockImplementation((userId) => { - return Promise.resolve(accounts[userId]?.vaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockImplementation((userId) => { + return new BehaviorSubject(accounts[userId]?.vaultTimeout); }); stateService.getUserId.mockResolvedValue(globalSetups?.userId); @@ -161,7 +164,7 @@ describe("VaultTimeoutService", () => { platformUtilsService.isViewOpen.mockResolvedValue(globalSetups?.isViewOpen ?? false); - vaultTimeoutSettingsService.vaultTimeoutAction$.mockImplementation((userId) => { + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockImplementation((userId) => { return new BehaviorSubject(accounts[userId]?.timeoutAction); }); @@ -212,18 +215,18 @@ describe("VaultTimeoutService", () => { ); it.each([ - null, // never - -1, // onRestart - -2, // onLocked - -3, // onSleep - -4, // onIdle + VaultTimeoutStringType.Never, + VaultTimeoutStringType.OnRestart, + VaultTimeoutStringType.OnLocked, + VaultTimeoutStringType.OnSleep, + VaultTimeoutStringType.OnIdle, ])( "does not log out or lock a user who has %s as their vault timeout", async (vaultTimeout) => { setupAccounts({ 1: { authStatus: AuthenticationStatus.Unlocked, - vaultTimeout: vaultTimeout, + vaultTimeout: vaultTimeout as VaultTimeout, isAuthenticated: true, }, }); diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.ts index a75fb6d4c4..5d07c0ebd3 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -170,8 +170,11 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { return false; } - const vaultTimeout = await this.vaultTimeoutSettingsService.getVaultTimeout(userId); - if (vaultTimeout == null || vaultTimeout < 0) { + const vaultTimeout = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId), + ); + + if (typeof vaultTimeout === "string") { return false; } @@ -186,7 +189,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { private async executeTimeoutAction(userId: UserId): Promise { const timeoutAction = await firstValueFrom( - this.vaultTimeoutSettingsService.vaultTimeoutAction$(userId), + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId), ); timeoutAction === VaultTimeoutAction.LogOut ? await this.logOut(userId) diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index fea4f28b03..ed438cda88 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -59,13 +59,14 @@ import { KdfConfigMigrator } from "./migrations/59-move-kdf-config-to-state-prov import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { KnownAccountsMigrator } from "./migrations/60-known-accounts"; import { PinStateMigrator } from "./migrations/61-move-pin-state-to-providers"; +import { VaultTimeoutSettingsServiceStateProviderMigrator } from "./migrations/62-migrate-vault-timeout-settings-svc-to-state-provider"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global"; import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 61; +export const CURRENT_VERSION = 62; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -128,7 +129,8 @@ export function createMigrationBuilder() { .with(RemoveRefreshTokenMigratedFlagMigrator, 57, 58) .with(KdfConfigMigrator, 58, 59) .with(KnownAccountsMigrator, 59, 60) - .with(PinStateMigrator, 60, CURRENT_VERSION); + .with(PinStateMigrator, 60, 61) + .with(VaultTimeoutSettingsServiceStateProviderMigrator, 61, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migration-helper.spec.ts b/libs/common/src/state-migrations/migration-helper.spec.ts index 21c5c72a18..9f6fa74747 100644 --- a/libs/common/src/state-migrations/migration-helper.spec.ts +++ b/libs/common/src/state-migrations/migration-helper.spec.ts @@ -242,6 +242,7 @@ export function mockMigrationHelper( mockHelper.remove.mockImplementation((key) => helper.remove(key)); mockHelper.type = helper.type; + mockHelper.clientType = helper.clientType; return mockHelper; } diff --git a/libs/common/src/state-migrations/migrations/62-migrate-vault-timeout-settings-svc-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/62-migrate-vault-timeout-settings-svc-to-state-provider.spec.ts new file mode 100644 index 0000000000..1a736c1623 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/62-migrate-vault-timeout-settings-svc-to-state-provider.spec.ts @@ -0,0 +1,669 @@ +import { MockProxy, any } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { + ClientType, + VAULT_TIMEOUT, + VAULT_TIMEOUT_ACTION, + VaultTimeoutSettingsServiceStateProviderMigrator, +} from "./62-migrate-vault-timeout-settings-svc-to-state-provider"; + +// Represents data in state service pre-migration +function preMigrationJson() { + return { + global: { + vaultTimeout: 30, + vaultTimeoutAction: "lock", + otherStuff: "otherStuff", + }, + + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "user2@email.com", + name: "User 2", + emailVerified: true, + }, + // create the same structure for user3, user4, user5, user6, user7 in the global_account_accounts + user3: { + email: "user3@email.com", + name: "User 3", + emailVerified: true, + }, + user4: { + email: "user4@email.com", + name: "User 4", + emailVerified: true, + }, + user5: { + email: "user5@email.com", + name: "User 5", + emailVerified: true, + }, + user6: { + email: "user6@email.com", + name: "User 6", + emailVerified: true, + }, + user7: { + email: "user7@email.com", + name: "User 7", + emailVerified: true, + }, + }, + + user1: { + settings: { + vaultTimeout: 30, + vaultTimeoutAction: "lock", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user2: { + settings: { + vaultTimeout: null as any, + vaultTimeoutAction: "logOut", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user3: { + settings: { + vaultTimeout: -1, // onRestart + vaultTimeoutAction: "lock", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user4: { + settings: { + vaultTimeout: -2, // onLocked + vaultTimeoutAction: "logOut", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user5: { + settings: { + vaultTimeout: -3, // onSleep + vaultTimeoutAction: "lock", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user6: { + settings: { + vaultTimeout: -4, // onIdle + vaultTimeoutAction: "logOut", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user7: { + settings: { + // no vault timeout data to migrate + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + }; +} + +function rollbackJSON(cli: boolean = false) { + const rollbackJson: any = { + // User specific state provider data + // use pattern user_{userId}_{stateDefinitionName}_{keyDefinitionKey} for user data + + // User1 migrated data + user_user1_vaultTimeoutSettings_vaultTimeout: 30, + user_user1_vaultTimeoutSettings_vaultTimeoutAction: "lock", + + // User2 migrated data + user_user2_vaultTimeoutSettings_vaultTimeout: "never", + user_user2_vaultTimeoutSettings_vaultTimeoutAction: "logOut", + + // User3 migrated data + user_user3_vaultTimeoutSettings_vaultTimeout: "onRestart", + user_user3_vaultTimeoutSettings_vaultTimeoutAction: "lock", + + // User4 migrated data + user_user4_vaultTimeoutSettings_vaultTimeout: "onLocked", + user_user4_vaultTimeoutSettings_vaultTimeoutAction: "logOut", + + // User5 migrated data + user_user5_vaultTimeoutSettings_vaultTimeout: "onSleep", + user_user5_vaultTimeoutSettings_vaultTimeoutAction: "lock", + + // User6 migrated data + user_user6_vaultTimeoutSettings_vaultTimeout: "onIdle", + user_user6_vaultTimeoutSettings_vaultTimeoutAction: "logOut", + + // User7 migrated data + // user_user7_vaultTimeoutSettings_vaultTimeout: null as any, + // user_user7_vaultTimeoutSettings_vaultTimeoutAction: null as any, + + // Global state provider data + // use pattern global_{stateDefinitionName}_{keyDefinitionKey} for global data + // Not migrating global data + + global: { + // no longer has vault timeout data + otherStuff: "otherStuff", + }, + + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "user2@email.com", + name: "User 2", + emailVerified: true, + }, + // create the same structure for user3, user4, user5, user6, user7 in the global_account_accounts + user3: { + email: "user3@email.com", + name: "User 3", + emailVerified: true, + }, + user4: { + email: "user4@email.com", + name: "User 4", + emailVerified: true, + }, + user5: { + email: "user5@email.com", + name: "User 5", + emailVerified: true, + }, + user6: { + email: "user6@email.com", + name: "User 6", + emailVerified: true, + }, + user7: { + email: "user7@email.com", + name: "User 7", + emailVerified: true, + }, + }, + + user1: { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user2: { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user3: { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user4: { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user5: { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user6: { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user7: { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + }; + + if (cli) { + rollbackJson.user_user7_vaultTimeoutSettings_vaultTimeout = "never"; + } + + return rollbackJson; +} + +describe("VaultTimeoutSettingsServiceStateProviderMigrator", () => { + let helper: MockProxy; + let sut: VaultTimeoutSettingsServiceStateProviderMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(preMigrationJson(), 61); + sut = new VaultTimeoutSettingsServiceStateProviderMigrator(61, 62); + }); + + it("should remove state service data from all accounts that have it", async () => { + await sut.migrate(helper); + + // Global data + expect(helper.set).toHaveBeenCalledWith("global", { + // no longer has vault timeout data + otherStuff: "otherStuff", + }); + + // User data + expect(helper.set).toHaveBeenCalledWith("user1", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user2", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user3", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user4", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user5", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user6", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledTimes(7); // 6 users + 1 global + expect(helper.set).not.toHaveBeenCalledWith("user7", any()); + }); + + it("should migrate data to state providers for defined accounts that have the data", async () => { + await sut.migrate(helper); + + expect(helper.setToUser).toHaveBeenCalledWith("user1", VAULT_TIMEOUT, 30); + expect(helper.setToUser).toHaveBeenCalledWith("user1", VAULT_TIMEOUT_ACTION, "lock"); + + expect(helper.setToUser).toHaveBeenCalledWith("user2", VAULT_TIMEOUT, "never"); + expect(helper.setToUser).toHaveBeenCalledWith("user2", VAULT_TIMEOUT_ACTION, "logOut"); + + expect(helper.setToUser).toHaveBeenCalledWith("user3", VAULT_TIMEOUT, "onRestart"); + expect(helper.setToUser).toHaveBeenCalledWith("user3", VAULT_TIMEOUT_ACTION, "lock"); + + expect(helper.setToUser).toHaveBeenCalledWith("user4", VAULT_TIMEOUT, "onLocked"); + expect(helper.setToUser).toHaveBeenCalledWith("user4", VAULT_TIMEOUT_ACTION, "logOut"); + + expect(helper.setToUser).toHaveBeenCalledWith("user5", VAULT_TIMEOUT, "onSleep"); + expect(helper.setToUser).toHaveBeenCalledWith("user5", VAULT_TIMEOUT_ACTION, "lock"); + + expect(helper.setToUser).toHaveBeenCalledWith("user6", VAULT_TIMEOUT, "onIdle"); + expect(helper.setToUser).toHaveBeenCalledWith("user6", VAULT_TIMEOUT_ACTION, "logOut"); + + // Expect that we didn't migrate anything to user 7 or 8 + expect(helper.setToUser).not.toHaveBeenCalledWith("user7", VAULT_TIMEOUT, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user7", VAULT_TIMEOUT_ACTION, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user8", VAULT_TIMEOUT, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user8", VAULT_TIMEOUT_ACTION, any()); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 62); + sut = new VaultTimeoutSettingsServiceStateProviderMigrator(61, 62); + }); + + it("should null out newly migrated entries in state provider framework", async () => { + await sut.rollback(helper); + + expect(helper.setToUser).toHaveBeenCalledWith("user1", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user1", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user2", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user2", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user3", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user3", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user4", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user4", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user5", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user5", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user6", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user6", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user7", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user7", VAULT_TIMEOUT_ACTION, null); + }); + + it("should add back data to all accounts that had migrated data (only user 1)", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledWith("user1", { + settings: { + vaultTimeout: 30, + vaultTimeoutAction: "lock", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user2", { + settings: { + vaultTimeout: null, + vaultTimeoutAction: "logOut", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user3", { + settings: { + vaultTimeout: -1, // onRestart + vaultTimeoutAction: "lock", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user4", { + settings: { + vaultTimeout: -2, // onLocked + vaultTimeoutAction: "logOut", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user5", { + settings: { + vaultTimeout: -3, // onSleep + vaultTimeoutAction: "lock", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user6", { + settings: { + vaultTimeout: -4, // onIdle + vaultTimeoutAction: "logOut", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + }); + + it("should not add back the global vault timeout data", async () => { + await sut.rollback(helper); + + expect(helper.set).not.toHaveBeenCalledWith("global", any()); + }); + + it("should not add data back if data wasn't migrated or acct doesn't exist", async () => { + await sut.rollback(helper); + + // no data to add back for user7 (acct exists but no migrated data) and user8 (no acct) + expect(helper.set).not.toHaveBeenCalledWith("user7", any()); + expect(helper.set).not.toHaveBeenCalledWith("user8", any()); + }); + }); +}); + +describe("VaultTimeoutSettingsServiceStateProviderMigrator - CLI", () => { + let helper: MockProxy; + let sut: VaultTimeoutSettingsServiceStateProviderMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(preMigrationJson(), 61, "general", ClientType.Cli); + sut = new VaultTimeoutSettingsServiceStateProviderMigrator(61, 62); + }); + + it("should remove state service data from all accounts that have it", async () => { + await sut.migrate(helper); + + // Global data + expect(helper.set).toHaveBeenCalledWith("global", { + // no longer has vault timeout data + otherStuff: "otherStuff", + }); + + // User data + expect(helper.set).toHaveBeenCalledWith("user1", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user2", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user3", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user4", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user5", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user6", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user7", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledTimes(8); // 7 users + 1 global + expect(helper.set).not.toHaveBeenCalledWith("user8", any()); + }); + + it("should migrate data to state providers for defined accounts that have the data with an exception for the vault timeout", async () => { + await sut.migrate(helper); + + expect(helper.setToUser).toHaveBeenCalledWith("user1", VAULT_TIMEOUT, 30); + expect(helper.setToUser).toHaveBeenCalledWith("user1", VAULT_TIMEOUT_ACTION, "lock"); + + expect(helper.setToUser).toHaveBeenCalledWith("user2", VAULT_TIMEOUT, "never"); + expect(helper.setToUser).toHaveBeenCalledWith("user2", VAULT_TIMEOUT_ACTION, "logOut"); + + expect(helper.setToUser).toHaveBeenCalledWith("user3", VAULT_TIMEOUT, "onRestart"); + expect(helper.setToUser).toHaveBeenCalledWith("user3", VAULT_TIMEOUT_ACTION, "lock"); + + expect(helper.setToUser).toHaveBeenCalledWith("user4", VAULT_TIMEOUT, "onLocked"); + expect(helper.setToUser).toHaveBeenCalledWith("user4", VAULT_TIMEOUT_ACTION, "logOut"); + + expect(helper.setToUser).toHaveBeenCalledWith("user5", VAULT_TIMEOUT, "onSleep"); + expect(helper.setToUser).toHaveBeenCalledWith("user5", VAULT_TIMEOUT_ACTION, "lock"); + + expect(helper.setToUser).toHaveBeenCalledWith("user6", VAULT_TIMEOUT, "onIdle"); + expect(helper.setToUser).toHaveBeenCalledWith("user6", VAULT_TIMEOUT_ACTION, "logOut"); + + // User7 has an undefined vault timeout, but we should still migrate it to "never" + // b/c the CLI doesn't have a vault timeout + expect(helper.setToUser).toHaveBeenCalledWith("user7", VAULT_TIMEOUT, "never"); + // Note: we don't have to worry about not migrating the vault timeout action b/c each client + // has a default value for the vault timeout action when it is retrieved via the vault timeout settings svc. + expect(helper.setToUser).not.toHaveBeenCalledWith("user7", VAULT_TIMEOUT_ACTION, any()); + + // Expect that we didn't migrate anything to user 8 b/c it doesn't exist + expect(helper.setToUser).not.toHaveBeenCalledWith("user8", VAULT_TIMEOUT, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user8", VAULT_TIMEOUT_ACTION, any()); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(true), 62, "general", ClientType.Cli); + sut = new VaultTimeoutSettingsServiceStateProviderMigrator(61, 62); + }); + + it("should null out newly migrated entries in state provider framework", async () => { + await sut.rollback(helper); + + expect(helper.setToUser).toHaveBeenCalledWith("user1", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user1", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user2", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user2", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user3", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user3", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user4", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user4", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user5", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user5", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user6", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user6", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user7", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user7", VAULT_TIMEOUT_ACTION, null); + }); + + it("should add back data to all accounts that had migrated data (only user 1)", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledWith("user1", { + settings: { + vaultTimeout: 30, + vaultTimeoutAction: "lock", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user2", { + settings: { + vaultTimeout: null, + vaultTimeoutAction: "logOut", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user3", { + settings: { + vaultTimeout: -1, // onRestart + vaultTimeoutAction: "lock", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user4", { + settings: { + vaultTimeout: -2, // onLocked + vaultTimeoutAction: "logOut", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user5", { + settings: { + vaultTimeout: -3, // onSleep + vaultTimeoutAction: "lock", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user6", { + settings: { + vaultTimeout: -4, // onIdle + vaultTimeoutAction: "logOut", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user7", { + settings: { + vaultTimeout: null, + // vaultTimeoutAction: null, // not migrated + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + }); + + it("should not add back the global vault timeout data", async () => { + await sut.rollback(helper); + + expect(helper.set).not.toHaveBeenCalledWith("global", any()); + }); + + it("should not add data back if data wasn't migrated or acct doesn't exist", async () => { + await sut.rollback(helper); + + // no data to add back for user8 (no acct) + expect(helper.set).not.toHaveBeenCalledWith("user8", any()); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/62-migrate-vault-timeout-settings-svc-to-state-provider.ts b/libs/common/src/state-migrations/migrations/62-migrate-vault-timeout-settings-svc-to-state-provider.ts new file mode 100644 index 0000000000..ee9ee4c9ea --- /dev/null +++ b/libs/common/src/state-migrations/migrations/62-migrate-vault-timeout-settings-svc-to-state-provider.ts @@ -0,0 +1,174 @@ +import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper"; +import { Migrator } from "../migrator"; + +// Types to represent data as it is stored in JSON +type ExpectedAccountType = { + settings?: { + vaultTimeout?: number; + vaultTimeoutAction?: string; + }; +}; + +type ExpectedGlobalType = { + vaultTimeout?: number; + vaultTimeoutAction?: string; +}; + +const VAULT_TIMEOUT_SETTINGS_STATE_DEF_LIKE: StateDefinitionLike = { + name: "vaultTimeoutSettings", +}; + +export const VAULT_TIMEOUT: KeyDefinitionLike = { + key: "vaultTimeout", // matches KeyDefinition.key + stateDefinition: VAULT_TIMEOUT_SETTINGS_STATE_DEF_LIKE, +}; + +export const VAULT_TIMEOUT_ACTION: KeyDefinitionLike = { + key: "vaultTimeoutAction", // matches KeyDefinition.key + stateDefinition: VAULT_TIMEOUT_SETTINGS_STATE_DEF_LIKE, +}; + +// Migrations are supposed to be frozen so we have to copy the type here. +export type VaultTimeout = + | number // 0 for immediately; otherwise positive numbers + | "never" // null + | "onRestart" // -1 + | "onLocked" // -2 + | "onSleep" // -3 + | "onIdle"; // -4 + +// Define mapping of old values to new values for migration purposes +const vaultTimeoutTypeMigrateRecord: Record = { + null: "never", + "-1": "onRestart", + "-2": "onLocked", + "-3": "onSleep", + "-4": "onIdle", +}; + +// define mapping of new values to old values for rollback purposes +const vaultTimeoutTypeRollbackRecord: Record = { + never: null, + onRestart: -1, + onLocked: -2, + onSleep: -3, + onIdle: -4, +}; + +export enum ClientType { + Web = "web", + Browser = "browser", + Desktop = "desktop", + Cli = "cli", +} + +export class VaultTimeoutSettingsServiceStateProviderMigrator extends Migrator<61, 62> { + async migrate(helper: MigrationHelper): Promise { + const globalData = await helper.get("global"); + + const accounts = await helper.getAccounts(); + async function migrateAccount( + userId: string, + account: ExpectedAccountType | undefined, + ): Promise { + let updatedAccount = false; + + // Migrate vault timeout + let existingVaultTimeout = account?.settings?.vaultTimeout; + + if (helper.clientType === ClientType.Cli && existingVaultTimeout === undefined) { + // The CLI does not set a vault timeout by default so we need to set it to null + // so that the migration can migrate null to "never" as the CLI does not have a vault timeout. + existingVaultTimeout = null; + } + + if (existingVaultTimeout !== undefined) { + // check undefined so that we allow null values (previously meant never timeout) + // Only migrate data that exists + + if (existingVaultTimeout === null || existingVaultTimeout < 0) { + // Map null or negative values to new string values + const newVaultTimeout = vaultTimeoutTypeMigrateRecord[existingVaultTimeout]; + await helper.setToUser(userId, VAULT_TIMEOUT, newVaultTimeout); + } else { + // Persist positive numbers as is + await helper.setToUser(userId, VAULT_TIMEOUT, existingVaultTimeout); + } + + delete account?.settings?.vaultTimeout; + updatedAccount = true; + } + + // Migrate vault timeout action + const existingVaultTimeoutAction = account?.settings?.vaultTimeoutAction; + + if (existingVaultTimeoutAction != null) { + // Only migrate data that exists + await helper.setToUser(userId, VAULT_TIMEOUT_ACTION, existingVaultTimeoutAction); + + delete account?.settings?.vaultTimeoutAction; + updatedAccount = true; + } + + // Note: we are explicitly not worrying about mapping over the global fallback vault timeout / action + // into the new state provider framework. It was originally a fallback but hasn't been used for years + // so this migration will clean up the global properties fully. + + if (updatedAccount) { + // Save the migrated account only if it was updated + await helper.set(userId, account); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + + // Delete global data + delete globalData?.vaultTimeout; + delete globalData?.vaultTimeoutAction; + await helper.set("global", globalData); + } + + async rollback(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise { + let updatedLegacyAccount = false; + + // Rollback vault timeout + const migratedVaultTimeout = await helper.getFromUser(userId, VAULT_TIMEOUT); + + if (account?.settings && migratedVaultTimeout != null) { + if (typeof migratedVaultTimeout === "string") { + // Map new string values back to old values + account.settings.vaultTimeout = vaultTimeoutTypeRollbackRecord[migratedVaultTimeout]; + } else { + // persist numbers as is + account.settings.vaultTimeout = migratedVaultTimeout; + } + + updatedLegacyAccount = true; + } + + await helper.setToUser(userId, VAULT_TIMEOUT, null); + + // Rollback vault timeout action + const migratedVaultTimeoutAction = await helper.getFromUser( + userId, + VAULT_TIMEOUT_ACTION, + ); + + if (account?.settings && migratedVaultTimeoutAction != null) { + account.settings.vaultTimeoutAction = migratedVaultTimeoutAction; + updatedLegacyAccount = true; + } + + await helper.setToUser(userId, VAULT_TIMEOUT_ACTION, null); + + if (updatedLegacyAccount) { + await helper.set(userId, account); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + } +} diff --git a/libs/common/src/types/vault-timeout.type.ts b/libs/common/src/types/vault-timeout.type.ts new file mode 100644 index 0000000000..e5a2e7f182 --- /dev/null +++ b/libs/common/src/types/vault-timeout.type.ts @@ -0,0 +1,17 @@ +// Note: the below comments are just for documenting what they used to be. +export const VaultTimeoutStringType = { + Never: "never", // null + OnRestart: "onRestart", // -1 + OnLocked: "onLocked", // -2 + OnSleep: "onSleep", // -3 + OnIdle: "onIdle", // -4 +} as const; + +export type VaultTimeout = + | number // 0 or positive numbers only + | (typeof VaultTimeoutStringType)[keyof typeof VaultTimeoutStringType]; + +export interface VaultTimeoutOption { + name: string; + value: VaultTimeout; +}