Auth/PM-5501 - VaultTimeoutSettingsService State Provider Migration (#8604)

* PM-5501 - VaultTimeoutSettingsSvc - refactor var names in getVaultTimeoutAction

* PM-5501 - Add state definitions and key definitions + test deserialization of key defs.

* PM-5501 - Add state provider dep to VaultTimeoutSettingsSvc

* PM-5501 - Refactor getVaultTimeout

* PM-5501 - VaultTimeoutSettingsService - Build getMaxVaultTimeoutPolicyByUserId helper

* PM-5501 - (1) Update state definitions (2) convert KeyDefs to UserKeyDefs (2) Remove everBeenUnlocked as we won't need it

* PM-5501 - VaultTimeoutSettingsSvc - POC for getVaultTimeoutActionByUserId$ method + new private determineVaultTimeoutAction helper.

* PM-5501 - VaultTimeoutSettingsSvc - build set and observable get methods for vault timeout settings

* PM-5501 - Update web references to use new vault timeout setting service methods

* PM-5501 - VaultTimeoutSettingsSvc - write up abstraction js docs

* PM-5501 - VaultTimeoutSettingsSvc abstraction - finish tweaks

* PM-5501 - VaultTimeoutSettingsSvc - add catchError blocks to observables to protect outer observables and prevent cancellation in case of error.

* PM-5501 - Remove vault timeout settings from state service implementation.

* PM-5501 - VaultTimeoutSettingsServiceStateProviderMigrator first draft

* PM-5501 - WIP - replace some state service calls with calls to vault timeout settings svc.

* PM-5501 - Replace state service calls in login strategies to get vault timeout settings data with VaultTimeoutSettingsService calls.

* PM-5501 - Fix login strategy tests

* PM-5501 - Update login strategy tests to pass

* PM-5501 - CryptoSvc - share VaultTimeout user key def to allow crypto svc access to the vault timeout without creating a circular dep.

* PM-5501 - Fix dependency injections.

* PM-5501 - ApiSvc - replace state svc with vault timeout settings svc.

* PM-5501 - VaultTimeoutSettingsServiceStateProviderMigrator more cleanup

* PM-5501 - Test VaultTimeoutSettingsServiceStateProviderMigrator

* PM-5501 - VaultTimeoutSettingsSvc tests updated

* PM-5501 - Update all setVaultTimeoutOptions references

* PM-5501 - VaultTimeoutSettingsSvc - Update setVaultTimeoutOptions to remove unnecessary logic and clean up clearTokens condition.

* PM-5501 - Fix vault timeout service tests

* PM-5501 - Update VaultTimeoutSettings state tests to pass

* PM-5501 - Desktop - system svc - fix build by replacing use of removed method.

* PM-5501 - Fix CLI by properly configuring super class deps in NodeApiService

* PM-5501 - Actually finish getitng deps fixed to get CLI to build

* PM-5501 - VaultTimeoutSettingsSvc.determineVaultTimeoutAction - pass userId to getAvailableVaultTimeoutActions to prevent hang waiting for an active user.

* PM-5501 - VaultTimeoutSettingSvc test - enhance getVaultTimeoutActionByUserId$ to also test PIN scenarios as an unlock method

* PM-5501 - bump migration version

* PM-5501 - Refactor migration to ensure the migration persists null vault timeout values.

* PM-5501 - Bump migration version

* PM-5501 - Fix web build issues introduced by merging main.

* PM-5501 - Bump migration version

* PM-5501 - PreferencesComponent - revert dep change from InternalPolicyService to standard PolicyService abstraction

* PM-5501 - Address all PR feedback from Jake

Co-authored-by: Jake Fink <jfink@bitwarden.com>

* PM-5501 - VaultTimeoutSettingsSvc tests - add tests for setVaultTimeoutOptions

* PM-5501 - VaultTimeoutSettingsSvc - setVaultTimeoutOptions - Update tests to use platform's desired syntax.

* PM-5501 - Fix tests

* PM-5501 - Create new VaultTimeout type

* PM-5501 - Create new DEFAULT_VAULT_TIMEOUT to allow each client to inject their default timeout into the VaultTimeoutSettingService

* PM-5501 - Migrate client default vault timeout to new injection token

* PM-5501 - Update VaultTimeoutSettingsSvc to use VaultTimeout type and apply default vault timeout if it is null.

* PM-5501 - Update vaultTimeout: number to be vaultTimeout: VaultTimeout everywhere I could find it.

* PM-5501 - More changes based on changing vaultTimeout from number to VaultTimeout type.

* PM-5501 - VaultTimeoutSvc - Update shouldLock logic which previously checked for null (never) or any negative values (any strings except never) with a simple string type check.

* PM-5501 - More cleanup of vaultTimeout type change - replacing null checks with "never" checks

* PM-5501 - VaultTimeoutSettingsSvc - refactor determineVaultTimeout to properly treat string and numeric vault timeouts.

* PM-5501 - Update vault timeout settings service tests to reflect new VaultTimeout type.

* PM-5501 - VaultTimeoutSettingsService - add more test cases for getVaultTimeoutByUserId

* PM-5501 - (1) Remove "immediately" as 0 is numerically meaningful and can be used with Math.min (2) Add VaultTimeoutOption interface for use in all places we show the user a list of vault timeout options.

* PM-5501 - VaultTimeoutSettingSvc - update tests to use 0 as immediately.

* PM-5501 - VaultTimeoutInputComp - Add new types and update applyVaultTimeoutPolicy logic appropriately.

* PM-5501 - Add new types to all preferences and setting components across clients.

* PM-5501 - Fix bug on web where navigating to the preferences page throws an error b/c the validatorChange function isn't defined.

* PM-5501 - WIP on updating vault timeout setting migration and rollback + testing it.

* PM-5501 - Update VaultTimeoutSettingsSvc state provider migration and tests to map existing possible values into new VaultTImeout type.

* PM-5501 - Fix vault timeout settings state tests by changing number to new VaultTimeout type.

* PM-5501 - Fix crypto svc auto key refresh test to use "never" instead of null.

* PM-5501 - Add clarifying comment to vaulttimeout type

* PM-5501 - Desktop app comp - replace systemTimeoutOptions with vault timeout type.

* PM-5501 - Update vault timeout service tests to use VaultTimeout type.

* PM-5501 - VaultTimeoutSettingsSvc - (1) Fix bug where vault timeout action didn't have a default like it did before (2) Fix bug in userHasMasterPassword where it would incorrectly return the active user stream for a given user id as a fallback. There is no guarantee the given user would match the active user so the paths are mutually exclusive.

* PM-5501 - Login Strategy fix - Move retrieval of vault timeout settings and setting of the tokens until after account init and user decryption options set as those opts are needed to properly determine the user's available vault timeout actions.

* PM-5501 - Fix vault timeout settings svc tests

* PM-5501 - VaultTimeoutSettingSvc - move default logic to determine methods + refactor default vault timeout action to properly default to lock in scenarios the user has lock available.

* Update libs/angular/src/components/settings/vault-timeout-input.component.ts

Co-authored-by: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com>

* PM-5501 - Per PR feedback, cleanup commented out vault timeout options

* PM-5501 - Fix vault timeout input comp lint issues

* PM-5501 - Per PR feedback from Cesar, update VaultTimeout type to use const so we can avoid any magic string usage. Awesome.

Co-authored-by: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com>

* PM-5501 - CLI - use "never" as default vault timeout instead of null.

* PM-5501 - Fix broken tests

* PM-5501 - Bump migration version

* PM-5501 - Fix build errors after merging main.

* PM-5501 - Update mockMigrationHelper to pass along client type so tests will respect it.

* PM-5501 - Update VaultTimeoutSettingsServiceStateProviderMigrator and tests to use new CLI client type to convert undefined values to never so that CLI users don't lose their session upon running this migration.

* PM-5501 - Bump migration version

* PM-5501 - Fix migration tests to use new authenticated user format

* PM-5501 Update rollback tests

* PM-5501 - Adjust migration based on feedback.

* PM-5501 - Per Jake's find, fix missed -2

Co-authored-by: Jake Fink <jfink@bitwarden.com>

* PM-5501 - Add user id to needsStorageReseed.

Co-authored-by: Jake Fink <jfink@bitwarden.com>

* PM-5501 - Per PR feedback, setVaultTimeoutOptions shouldn't accept null for vault timeout anymore.

* PM-5501 - Per PR feedback, add null checks for set methods for setting vault timeout or vault timeout action.

* PM-5501 - Per PR feedback, add more context as to why we need vault timeout settings to persist after logout.

* PM-5501 - Per PR feedback, fix userHasMasterPassword

* PM-5501 - VaultTimeoutSettingsService - fix userHasMasterPassword check by checking for null decryption options.

* PM-5501 - Remove state service from vault timeout settings service (WOOO)

* PM-5501 - Bump migration version

* PM-5501 - Account Security comp - refactor to consider ease of debugging.

* PM-5501 - (1) Add checks for null vault timeout and vault timeout actions (2) Add tests for new scenarios.

* PM-5501 - VaultTimeoutSettingsSvc - setVaultTimeoutOptions - fix bug where nullish check would throw incorrectly if immediately (0) was picked as the timeout.

* PM-5501 - Per PR feedback, clean up remaining token service methods which accept null for timeout and add tests. .

* PM-5501 - Fix nit

---------

Co-authored-by: Jake Fink <jfink@bitwarden.com>
Co-authored-by: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com>
This commit is contained in:
Jared Snider 2024-05-13 15:56:04 -04:00 committed by GitHub
parent dd41bcb52e
commit 473c5311fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
59 changed files with 2306 additions and 390 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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<boolean> {
const currentVaultTimeout = await this.stateService.getVaultTimeout();
return currentVaultTimeout == null ? false : true;
private async needsStorageReseed(userId: UserId): Promise<boolean> {
const currentVaultTimeout = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId),
);
return currentVaultTimeout == VaultTimeoutStringType.Never ? false : true;
}
async collectPageDetailsForContentScript(tab: any, sender: string, frameId: number = null) {

View File

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

View File

@ -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>): 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<Account>) {
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),

View File

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

View File

@ -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<void>>,
useFactory: (initService: InitService) => initService.init(),

View File

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

View File

@ -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<void>,
customUserAgent: string = null,
) {
@ -30,7 +30,7 @@ export class NodeApiService extends ApiService {
platformUtilsService,
environmentService,
appIdService,
stateService,
vaultTimeoutSettingsService,
logoutCallback,
customUserAgent,
);

View File

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

View File

@ -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<void> {
private async checkForSystemTimeout(timeout: VaultTimeout): Promise<void> {
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];
}

View File

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

View File

@ -4,7 +4,6 @@ import {
} from "@bitwarden/common/platform/models/domain/account";
export class AccountSettings extends BaseAccountSettings {
vaultTimeout = -1; // On Restart
dismissedBiometricRequirePasswordOnStartCallout?: boolean;
}

View File

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

View File

@ -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<Account>) {
super(init);
Object.assign(this.settings, {
...new AccountSettings(),
...this.settings,
});
}
}

View File

@ -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<void>();
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,
);

View File

@ -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<boolean>;
private onChange: (vaultTimeout: number) => void;
private onChange: (vaultTimeout: VaultTimeout) => void;
private validatorChange: () => void;
private destroy$ = new Subject<void>();
@ -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();
}
}
}

View File

@ -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<boolean>("LOG_MAC_FAILURE
export const SYSTEM_THEME_OBSERVABLE = new SafeInjectionToken<Observable<ThemeType>>(
"SYSTEM_THEME_OBSERVABLE",
);
export const DEFAULT_VAULT_TIMEOUT = new SafeInjectionToken<VaultTimeout>("DEFAULT_VAULT_TIMEOUT");
export const INTRAPROCESS_MESSAGING_SUBJECT = new SafeInjectionToken<Subject<Message<object>>>(
"INTRAPROCESS_MESSAGING_SUBJECT",
);

View File

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

View File

@ -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<InternalUserDecryptionOptionsServiceAbstraction>;
let deviceTrustService: MockProxy<DeviceTrustServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
let kdfConfigService: MockProxy<KdfConfigService>;
const mockUserId = Utils.newGuid() as UserId;
@ -79,6 +83,7 @@ describe("AuthRequestLoginStrategy", () => {
userDecryptionOptions = mock<InternalUserDecryptionOptionsServiceAbstraction>();
deviceTrustService = mock<DeviceTrustServiceAbstraction>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
kdfConfigService = mock<KdfConfigService>();
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<VaultTimeoutAction>(
mockVaultTimeoutAction,
);
vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue(
mockVaultTimeoutActionBSub.asObservable(),
);
const mockVaultTimeout = 1000;
const mockVaultTimeoutBSub = new BehaviorSubject<number>(mockVaultTimeout);
vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue(
mockVaultTimeoutBSub.asObservable(),
);
});
it("sets keys after a successful authentication when masterKey and masterKeyHash provided in login credentials", async () => {

View File

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

View File

@ -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<PolicyService>;
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
let kdfConfigService: MockProxy<KdfConfigService>;
let passwordLoginStrategy: PasswordLoginStrategy;
@ -139,6 +142,8 @@ describe("LoginStrategy", () => {
passwordStrengthService = mock<PasswordStrengthService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
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<VaultTimeoutAction>(
mockVaultTimeoutAction,
);
vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue(
mockVaultTimeoutActionBSub.asObservable(),
);
const mockVaultTimeout = 1000;
const mockVaultTimeoutBSub = new BehaviorSubject<number>(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<VaultTimeoutAction>(
mockVaultTimeoutAction,
);
vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue(
mockVaultTimeoutActionBSub.asObservable(),
);
const mockVaultTimeout = 1000;
stateService.getVaultTimeoutAction.mockResolvedValue(mockVaultTimeoutAction);
stateService.getVaultTimeout.mockResolvedValue(mockVaultTimeout);
const mockVaultTimeoutBSub = new BehaviorSubject<number>(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<VaultTimeoutAction>(
mockVaultTimeoutAction,
);
vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue(
mockVaultTimeoutActionBSub.asObservable(),
);
const mockVaultTimeout = 1000;
stateService.getVaultTimeoutAction.mockResolvedValue(mockVaultTimeoutAction);
stateService.getVaultTimeout.mockResolvedValue(mockVaultTimeout);
const mockVaultTimeoutBSub = new BehaviorSubject<number>(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<VaultTimeoutAction>(
mockVaultTimeoutAction,
);
vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue(
mockVaultTimeoutActionBSub.asObservable(),
);
const mockVaultTimeout = 1000;
const mockVaultTimeoutBSub = new BehaviorSubject<number>(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,
);

View File

@ -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<UserId> {
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

View File

@ -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<PolicyService>;
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
let kdfConfigService: MockProxy<KdfConfigService>;
let passwordLoginStrategy: PasswordLoginStrategy;
@ -96,6 +100,7 @@ describe("PasswordLoginStrategy", () => {
policyService = mock<PolicyService>();
passwordStrengthService = mock<PasswordStrengthService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
kdfConfigService = mock<KdfConfigService>();
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<VaultTimeoutAction>(
mockVaultTimeoutAction,
);
vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue(
mockVaultTimeoutActionBSub.asObservable(),
);
const mockVaultTimeout = 1000;
const mockVaultTimeoutBSub = new BehaviorSubject<number>(mockVaultTimeout);
vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue(
mockVaultTimeoutBSub.asObservable(),
);
});
it("sends master password credentials to the server", async () => {

View File

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

View File

@ -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<AuthRequestServiceAbstraction>;
let i18nService: MockProxy<I18nService>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
let kdfConfigService: MockProxy<KdfConfigService>;
let ssoLoginStrategy: SsoLoginStrategy;
@ -88,6 +92,7 @@ describe("SsoLoginStrategy", () => {
authRequestService = mock<AuthRequestServiceAbstraction>();
i18nService = mock<I18nService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
kdfConfigService = mock<KdfConfigService>();
tokenService.getTwoFactorToken.mockResolvedValue(null);
@ -96,6 +101,21 @@ describe("SsoLoginStrategy", () => {
sub: userId,
});
const mockVaultTimeoutAction = VaultTimeoutAction.Lock;
const mockVaultTimeoutActionBSub = new BehaviorSubject<VaultTimeoutAction>(
mockVaultTimeoutAction,
);
vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue(
mockVaultTimeoutActionBSub.asObservable(),
);
const mockVaultTimeout = 1000;
const mockVaultTimeoutBSub = new BehaviorSubject<number>(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);

View File

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

View File

@ -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<KeyConnectorService>;
let environmentService: MockProxy<EnvironmentService>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
let kdfConfigService: MockProxy<KdfConfigService>;
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<KeyConnectorService>();
environmentService = mock<EnvironmentService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
kdfConfigService = mock<KdfConfigService>();
appIdService.getAppId.mockResolvedValue(deviceId);
@ -103,10 +109,23 @@ describe("UserApiLoginStrategy", () => {
environmentService,
keyConnectorService,
billingAccountProfileStateService,
vaultTimeoutSettingsService,
kdfConfigService,
);
credentials = new UserApiLoginCredentials(apiClientId, apiClientSecret);
const mockVaultTimeoutActionBSub = new BehaviorSubject<VaultTimeoutAction>(
mockVaultTimeoutAction,
);
vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue(
mockVaultTimeoutActionBSub.asObservable(),
);
const mockVaultTimeoutBSub = new BehaviorSubject<number>(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(

View File

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

View File

@ -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<TwoFactorService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
let kdfConfigService: MockProxy<KdfConfigService>;
let webAuthnLoginStrategy!: WebAuthnLoginStrategy;
@ -85,6 +89,7 @@ describe("WebAuthnLoginStrategy", () => {
twoFactorService = mock<TwoFactorService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
kdfConfigService = mock<KdfConfigService>();
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<VaultTimeoutAction>(
mockVaultTimeoutAction,
);
vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue(
mockVaultTimeoutActionBSub.asObservable(),
);
const mockVaultTimeout = 1000;
const mockVaultTimeoutBSub = new BehaviorSubject<number>(mockVaultTimeout);
vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue(
mockVaultTimeoutBSub.asObservable(),
);
});
afterAll(() => {

View File

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

View File

@ -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<AuthRequestServiceAbstraction>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
let kdfConfigService: MockProxy<KdfConfigService>;
let stateProvider: FakeGlobalStateProvider;
@ -97,6 +101,7 @@ describe("LoginStrategyService", () => {
userDecryptionOptionsService = mock<UserDecryptionOptionsService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
stateProvider = new FakeGlobalStateProvider();
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
kdfConfigService = mock<KdfConfigService>();
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<VaultTimeoutAction>(
mockVaultTimeoutAction,
);
vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue(
mockVaultTimeoutActionBSub.asObservable(),
);
const mockVaultTimeout = 1000;
const mockVaultTimeoutBSub = new BehaviorSubject<number>(mockVaultTimeout);
vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue(
mockVaultTimeoutBSub.asObservable(),
);
});
it("should return an AuthResult on successful login", async () => {

View File

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

View File

@ -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<void>;
@ -23,19 +26,23 @@ export abstract class VaultTimeoutSettingsService {
availableVaultTimeoutActions$: (userId?: string) => Observable<VaultTimeoutAction[]>;
/**
* 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<number>;
getVaultTimeoutActionByUserId$: (userId: string) => Observable<VaultTimeoutAction>;
/**
* 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<VaultTimeoutAction>;
getVaultTimeoutByUserId$: (userId: string) => Observable<VaultTimeout>;
/**
* Has the user enabled unlock with Biometric.

View File

@ -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<void>;
@ -51,7 +52,7 @@ export abstract class TokenService {
setAccessToken: (
accessToken: string,
vaultTimeoutAction: VaultTimeoutAction,
vaultTimeout: number | null,
vaultTimeout: VaultTimeout,
) => Promise<void>;
// 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<void>;
@ -110,7 +111,7 @@ export abstract class TokenService {
setClientSecret: (
clientSecret: string,
vaultTimeoutAction: VaultTimeoutAction,
vaultTimeout: number | null,
vaultTimeout: VaultTimeout,
userId?: UserId,
) => Promise<void>;

View File

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

View File

@ -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<void> {
@ -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<void> {
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<void> {
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<void> {
// 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<void> {
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<void> {
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<TokenStorageLocation> {
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) {

View File

@ -118,8 +118,4 @@ export abstract class StateService<T extends Account = Account> {
getGeneratorOptions: (options?: StorageOptions) => Promise<GeneratorOptions>;
setGeneratorOptions: (value: GeneratorOptions, options?: StorageOptions) => Promise<void>;
getUserId: (options?: StorageOptions) => Promise<string>;
getVaultTimeout: (options?: StorageOptions) => Promise<number>;
setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>;
getVaultTimeoutAction: (options?: StorageOptions) => Promise<string>;
setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise<void>;
}

View File

@ -147,8 +147,6 @@ export class AccountSettings {
passwordGenerationOptions?: PasswordGeneratorOptions;
usernameGenerationOptions?: UsernameGeneratorOptions;
generatorOptions?: GeneratorOptions;
vaultTimeout?: number;
vaultTimeoutAction?: string = "lock";
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {
if (obj == null) {

View File

@ -1,7 +1,5 @@
export class GlobalState {
organizationInvitation?: any;
vaultTimeout?: number;
vaultTimeoutAction?: string;
enableBrowserIntegration?: boolean;
enableBrowserIntegrationFingerprint?: boolean;
enableDuckDuckGoBrowserIntegration?: boolean;

View File

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

View File

@ -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: {

View File

@ -571,49 +571,6 @@ export class StateService<
)?.profile?.userId;
}
async getVaultTimeout(options?: StorageOptions): Promise<number> {
const accountVaultTimeout = (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
)?.settings?.vaultTimeout;
return accountVaultTimeout;
}
async setVaultTimeout(value: number, options?: StorageOptions): Promise<void> {
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<string> {
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<void> {
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<TGlobalState> {
let globals: TGlobalState;
if (this.useMemory(options.storageLocation)) {

View File

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

View File

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

View File

@ -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<void>,
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,

View File

@ -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<CryptoService>;
let tokenService: MockProxy<TokenService>;
let policyService: MockProxy<PolicyService>;
let stateService: MockProxy<StateService>;
const biometricStateService = mock<BiometricStateService>();
let service: VaultTimeoutSettingsService;
let vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction;
let userDecryptionOptionsSubject: BehaviorSubject<UserDecryptionOptions>;
const mockUserId = Utils.newGuid() as UserId;
let stateProvider: FakeStateProvider;
let logService: MockProxy<LogService>;
beforeEach(() => {
accountService = mockAccountServiceWith(mockUserId);
@ -42,7 +49,6 @@ describe("VaultTimeoutSettingsService", () => {
cryptoService = mock<CryptoService>();
tokenService = mock<TokenService>();
policyService = mock<PolicyService>();
stateService = mock<StateService>();
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<LogService>();
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,
);
}
});

View File

@ -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<void> {
async setVaultTimeoutOptions(
userId: UserId,
timeout: VaultTimeout,
action: VaultTimeoutAction,
): Promise<void> {
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<number> {
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<void> {
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<VaultTimeout> {
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<VaultTimeoutAction> {
const availableActions = await this.getAvailableVaultTimeoutActions();
if (availableActions.length === 1) {
return availableActions[0];
private async determineVaultTimeout(
currentVaultTimeout: VaultTimeout | null,
maxVaultTimeoutPolicy: Policy | null,
): Promise<VaultTimeout | null> {
// 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<void> {
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<VaultTimeoutAction> {
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<VaultTimeoutAction> {
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<Policy | null> {
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<VaultTimeoutAction[]> {
@ -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$);
}
}

View File

@ -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<VaultTimeoutAction> | UserKeyDefinition<VaultTimeout>,
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<T>(keyDefinition: UserKeyDefinition<T>, 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);
});
},
);

View File

@ -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<VaultTimeoutAction>(
VAULT_TIMEOUT_SETTINGS_DISK_LOCAL,
"vaultTimeoutAction",
{
deserializer: (vaultTimeoutAction) => vaultTimeoutAction,
clearOn: [], // persisted on logout
},
);
export const VAULT_TIMEOUT = new UserKeyDefinition<VaultTimeout>(
VAULT_TIMEOUT_SETTINGS_DISK_LOCAL,
"vaultTimeout",
{
deserializer: (vaultTimeout) => vaultTimeout,
clearOn: [], // persisted on logout
},
);

View File

@ -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<VaultTimeoutAction[]>([]);
@ -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<VaultTimeout>(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<VaultTimeoutAction>(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,
},
});

View File

@ -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<void> {
const timeoutAction = await firstValueFrom(
this.vaultTimeoutSettingsService.vaultTimeoutAction$(userId),
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId),
);
timeoutAction === VaultTimeoutAction.LogOut
? await this.logOut(userId)

View File

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

View File

@ -242,6 +242,7 @@ export function mockMigrationHelper(
mockHelper.remove.mockImplementation((key) => helper.remove(key));
mockHelper.type = helper.type;
mockHelper.clientType = helper.clientType;
return mockHelper;
}

View File

@ -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<MigrationHelper>;
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<MigrationHelper>;
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());
});
});
});

View File

@ -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<any, VaultTimeout> = {
null: "never",
"-1": "onRestart",
"-2": "onLocked",
"-3": "onSleep",
"-4": "onIdle",
};
// define mapping of new values to old values for rollback purposes
const vaultTimeoutTypeRollbackRecord: Record<VaultTimeout, any> = {
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<void> {
const globalData = await helper.get<ExpectedGlobalType>("global");
const accounts = await helper.getAccounts<ExpectedAccountType>();
async function migrateAccount(
userId: string,
account: ExpectedAccountType | undefined,
): Promise<void> {
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<void> {
const accounts = await helper.getAccounts<ExpectedAccountType>();
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
let updatedLegacyAccount = false;
// Rollback vault timeout
const migratedVaultTimeout = await helper.getFromUser<VaultTimeout>(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<string>(
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))]);
}
}

View File

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