1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 17:53:39 +00:00

[PM-12606] Move Vault Timeout and Vault Timeout Settings to KM (#13405)

* move vault timeout and vault timeout settings to km

* move browser vault timeout service to km

* fix cli import

* fix imports

* fix some relative imports

* use relative imports within common

* fix imports

* fix new imports

* Fix new imports

* fix spec imports
This commit is contained in:
Jake Fink
2025-02-28 08:55:03 -06:00
committed by GitHub
parent 0ee2e0bf93
commit 43f5423e78
64 changed files with 273 additions and 203 deletions

View File

@@ -11,7 +11,6 @@ import {
import { LogoutReason } from "@bitwarden/auth/common";
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";
@@ -105,7 +104,8 @@ import { PlanResponse } from "../billing/models/response/plan.response";
import { SubscriptionResponse } from "../billing/models/response/subscription.response";
import { TaxInfoResponse } from "../billing/models/response/tax-info.response";
import { DeviceType } from "../enums";
import { VaultTimeoutAction } from "../enums/vault-timeout-action.enum";
import { VaultTimeoutSettingsService } from "../key-management/vault-timeout";
import { VaultTimeoutAction } from "../key-management/vault-timeout/enums/vault-timeout-action.enum";
import { CollectionBulkDeleteRequest } from "../models/request/collection-bulk-delete.request";
import { DeleteRecoverRequest } from "../models/request/delete-recover.request";
import { EventRequest } from "../models/request/event.request";

View File

@@ -1,388 +0,0 @@
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom, map, of } from "rxjs";
import {
PinServiceAbstraction,
FakeUserDecryptionOptions as UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { BiometricStateService, KeyService } from "@bitwarden/key-management";
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 { LogService } from "../../platform/abstractions/log.service";
import { Utils } from "../../platform/misc/utils";
import {
VAULT_TIMEOUT,
VAULT_TIMEOUT_ACTION,
} from "../../services/vault-timeout/vault-timeout-settings.state";
import { UserId } from "../../types/guid";
import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type";
import { VaultTimeoutSettingsService } from "./vault-timeout-settings.service";
describe("VaultTimeoutSettingsService", () => {
let accountService: FakeAccountService;
let pinService: MockProxy<PinServiceAbstraction>;
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let keyService: MockProxy<KeyService>;
let tokenService: MockProxy<TokenService>;
let policyService: MockProxy<PolicyService>;
const biometricStateService = mock<BiometricStateService>();
let vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction;
let userDecryptionOptionsSubject: BehaviorSubject<UserDecryptionOptions>;
const mockUserId = Utils.newGuid() as UserId;
let stateProvider: FakeStateProvider;
let logService: MockProxy<LogService>;
beforeEach(() => {
accountService = mockAccountServiceWith(mockUserId);
pinService = mock<PinServiceAbstraction>();
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
keyService = mock<KeyService>();
tokenService = mock<TokenService>();
policyService = mock<PolicyService>();
userDecryptionOptionsSubject = new BehaviorSubject(null);
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
userDecryptionOptionsService.hasMasterPassword$ = userDecryptionOptionsSubject.pipe(
map((options) => options?.hasMasterPassword ?? false),
);
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
userDecryptionOptionsSubject,
);
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);
});
afterEach(() => {
jest.resetAllMocks();
});
describe("availableVaultTimeoutActions$", () => {
it("always returns LogOut", async () => {
const result = await firstValueFrom(
vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
);
expect(result).toContain(VaultTimeoutAction.LogOut);
});
it("contains Lock when the user has a master password", async () => {
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
const result = await firstValueFrom(
vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
);
expect(result).toContain(VaultTimeoutAction.Lock);
});
it("contains Lock when the user has either a persistent or ephemeral PIN configured", async () => {
pinService.isPinSet.mockResolvedValue(true);
const result = await firstValueFrom(
vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
);
expect(result).toContain(VaultTimeoutAction.Lock);
});
it("contains Lock when the user has biometrics configured", async () => {
biometricStateService.biometricUnlockEnabled$ = of(true);
biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(true);
const result = await firstValueFrom(
vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
);
expect(result).toContain(VaultTimeoutAction.Lock);
});
it("not contains Lock when the user does not have a master password, PIN, or biometrics", async () => {
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: false }));
pinService.isPinSet.mockResolvedValue(false);
biometricStateService.biometricUnlockEnabled$ = of(false);
const result = await firstValueFrom(
vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
);
expect(result).not.toContain(VaultTimeoutAction.Lock);
});
});
describe("canLock", () => {
it("returns true if the user can lock", async () => {
jest
.spyOn(vaultTimeoutSettingsService, "availableVaultTimeoutActions$")
.mockReturnValue(of([VaultTimeoutAction.Lock]));
const result = await vaultTimeoutSettingsService.canLock("userId" as UserId);
expect(result).toBe(true);
});
it("returns false if the user only has the log out vault timeout action", async () => {
jest
.spyOn(vaultTimeoutSettingsService, "availableVaultTimeoutActions$")
.mockReturnValue(of([VaultTimeoutAction.LogOut]));
const result = await vaultTimeoutSettingsService.canLock("userId" as UserId);
expect(result).toBe(false);
});
it("returns false if the user has no vault timeout actions", async () => {
jest
.spyOn(vaultTimeoutSettingsService, "availableVaultTimeoutActions$")
.mockReturnValue(of([]));
const result = await vaultTimeoutSettingsService.canLock("userId" as UserId);
expect(result).toBe(false);
});
});
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
${null} | ${null} | ${VaultTimeoutAction.Lock}
${null} | ${VaultTimeoutAction.LogOut} | ${VaultTimeoutAction.LogOut}
${VaultTimeoutAction.LogOut} | ${null} | ${VaultTimeoutAction.LogOut}
${VaultTimeoutAction.LogOut} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut}
`(
"returns $expected when policy is $policy, and user preference is $userPreference",
async ({ policy, userPreference, expected }) => {
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
policyService.getAll$.mockReturnValue(
of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])),
);
await stateProvider.setUserState(VAULT_TIMEOUT_ACTION, userPreference, mockUserId);
const result = await firstValueFrom(
vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(mockUserId),
);
expect(result).toBe(expected);
},
);
});
describe("given the user does not have a master password", () => {
it.each`
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 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 }),
);
policyService.getAll$.mockReturnValue(
of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])),
);
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(keyService.refreshAdditionalKeys).toHaveBeenCalled();
});
it("should clear the tokens when the timeout is not never 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();
});
it("should not clear the tokens when the timeout is never and the action is log out", async () => {
// Arrange
const action = VaultTimeoutAction.LogOut;
const timeout = VaultTimeoutStringType.Never;
// Act
await vaultTimeoutSettingsService.setVaultTimeoutOptions(mockUserId, timeout, action);
// Assert
expect(tokenService.clearTokens).not.toHaveBeenCalled();
});
});
function createVaultTimeoutSettingsService(
defaultVaultTimeout: VaultTimeout,
): VaultTimeoutSettingsService {
return new VaultTimeoutSettingsService(
accountService,
pinService,
userDecryptionOptionsService,
keyService,
tokenService,
policyService,
biometricStateService,
stateProvider,
logService,
defaultVaultTimeout,
);
}
});

View File

@@ -1,305 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
EMPTY,
Observable,
catchError,
combineLatest,
defer,
distinctUntilChanged,
firstValueFrom,
from,
map,
shareReplay,
switchMap,
tap,
} from "rxjs";
import {
PinServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { BiometricStateService, KeyService } from "@bitwarden/key-management";
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 { LogService } from "../../platform/abstractions/log.service";
import { StateProvider } from "../../platform/state";
import { UserId } from "../../types/guid";
import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type";
import { VAULT_TIMEOUT, VAULT_TIMEOUT_ACTION } from "./vault-timeout-settings.state";
export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceAbstraction {
constructor(
private accountService: AccountService,
private pinService: PinServiceAbstraction,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private keyService: KeyService,
private tokenService: TokenService,
private policyService: PolicyService,
private biometricStateService: BiometricStateService,
private stateProvider: StateProvider,
private logService: LogService,
private defaultVaultTimeout: VaultTimeout,
) {}
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();
const refreshToken = await this.tokenService.getRefreshToken();
const clientId = await this.tokenService.getClientId();
const clientSecret = await this.tokenService.getClientSecret();
await this.setVaultTimeout(userId, timeout);
if (timeout != VaultTimeoutStringType.Never && 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.setVaultTimeoutAction(userId, action);
await this.tokenService.setTokens(accessToken, action, timeout, refreshToken, [
clientId,
clientSecret,
]);
await this.keyService.refreshAdditionalKeys();
}
availableVaultTimeoutActions$(userId?: string): Observable<VaultTimeoutAction[]> {
return defer(() => this.getAvailableVaultTimeoutActions(userId));
}
async canLock(userId: UserId): Promise<boolean> {
const availableVaultTimeoutActions: VaultTimeoutAction[] = await firstValueFrom(
this.availableVaultTimeoutActions$(userId),
);
return availableVaultTimeoutActions?.includes(VaultTimeoutAction.Lock) || false;
}
async isBiometricLockSet(userId?: string): Promise<boolean> {
const biometricUnlockPromise =
userId == null
? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)
: this.biometricStateService.getBiometricUnlockEnabled(userId as UserId);
return await biometricUnlockPromise;
}
private async setVaultTimeout(userId: UserId, timeout: VaultTimeout): Promise<void> {
if (!userId) {
throw new Error("User id required. Cannot set vault timeout.");
}
if (timeout == null) {
throw new Error("Vault Timeout cannot be null.");
}
await this.stateProvider.setUserState(VAULT_TIMEOUT, timeout, 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 }),
);
}
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;
}
// 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 }),
);
}
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 (
maxVaultTimeoutPolicy?.data?.action &&
availableVaultTimeoutActions.includes(maxVaultTimeoutPolicy.data.action)
) {
// return policy defined vault timeout action
return maxVaultTimeoutPolicy.data.action;
}
// 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[]> {
userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id;
const availableActions = [VaultTimeoutAction.LogOut];
const canLock =
(await this.userHasMasterPassword(userId)) ||
(await this.pinService.isPinSet(userId as UserId)) ||
(await this.isBiometricLockSet(userId));
if (canLock) {
availableActions.push(VaultTimeoutAction.Lock);
}
return availableActions;
}
async clear(userId?: string): Promise<void> {
await this.keyService.clearPinKeys(userId);
}
private async userHasMasterPassword(userId: string): Promise<boolean> {
if (userId) {
const decryptionOptions = await firstValueFrom(
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
);
return !!decryptionOptions?.hasMasterPassword;
} else {
return await firstValueFrom(this.userDecryptionOptionsService.hasMasterPassword$);
}
}
}

View File

@@ -1,36 +0,0 @@
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

@@ -1,27 +0,0 @@
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

@@ -1,454 +0,0 @@
import { MockProxy, any, mock } from "jest-mock-extended";
import { BehaviorSubject, from, of } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { LogoutReason } from "@bitwarden/auth/common";
import { BiometricsService } from "@bitwarden/key-management";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { SearchService } from "../../abstractions/search.service";
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { AccountInfo } from "../../auth/abstractions/account.service";
import { AuthService } from "../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { TaskSchedulerService } from "../../platform/scheduling";
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 { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction";
import { VaultTimeoutService } from "./vault-timeout.service";
describe("VaultTimeoutService", () => {
let accountService: FakeAccountService;
let masterPasswordService: FakeMasterPasswordService;
let cipherService: MockProxy<CipherService>;
let folderService: MockProxy<FolderService>;
let collectionService: MockProxy<CollectionService>;
let platformUtilsService: MockProxy<PlatformUtilsService>;
let messagingService: MockProxy<MessagingService>;
let searchService: MockProxy<SearchService>;
let stateService: MockProxy<StateService>;
let authService: MockProxy<AuthService>;
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
let stateEventRunnerService: MockProxy<StateEventRunnerService>;
let taskSchedulerService: MockProxy<TaskSchedulerService>;
let logService: MockProxy<LogService>;
let biometricsService: MockProxy<BiometricsService>;
let lockedCallback: jest.Mock<Promise<void>, [userId: string]>;
let loggedOutCallback: jest.Mock<Promise<void>, [logoutReason: LogoutReason, userId?: string]>;
let vaultTimeoutActionSubject: BehaviorSubject<VaultTimeoutAction>;
let availableVaultTimeoutActionsSubject: BehaviorSubject<VaultTimeoutAction[]>;
let vaultTimeoutService: VaultTimeoutService;
const userId = Utils.newGuid() as UserId;
beforeEach(() => {
accountService = mockAccountServiceWith(userId);
masterPasswordService = new FakeMasterPasswordService();
cipherService = mock();
folderService = mock();
collectionService = mock();
platformUtilsService = mock();
messagingService = mock();
searchService = mock();
stateService = mock();
authService = mock();
vaultTimeoutSettingsService = mock();
stateEventRunnerService = mock();
taskSchedulerService = mock<TaskSchedulerService>();
logService = mock<LogService>();
biometricsService = mock<BiometricsService>();
lockedCallback = jest.fn();
loggedOutCallback = jest.fn();
vaultTimeoutActionSubject = new BehaviorSubject(VaultTimeoutAction.Lock);
vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue(
vaultTimeoutActionSubject,
);
availableVaultTimeoutActionsSubject = new BehaviorSubject<VaultTimeoutAction[]>([]);
vaultTimeoutService = new VaultTimeoutService(
accountService,
masterPasswordService,
cipherService,
folderService,
collectionService,
platformUtilsService,
messagingService,
searchService,
stateService,
authService,
vaultTimeoutSettingsService,
stateEventRunnerService,
taskSchedulerService,
logService,
biometricsService,
lockedCallback,
loggedOutCallback,
);
});
// Helper for setting up mocks for multiple users
const setupAccounts = (
accounts: Record<
string,
{
authStatus?: AuthenticationStatus;
isAuthenticated?: boolean;
lastActive?: number;
vaultTimeout?: VaultTimeout;
timeoutAction?: VaultTimeoutAction;
availableTimeoutActions?: VaultTimeoutAction[];
}
>,
globalSetups?: {
userId?: string;
isViewOpen?: boolean;
},
) => {
// Both are available by default and the specific test can change this per test
availableVaultTimeoutActionsSubject.next([VaultTimeoutAction.Lock, VaultTimeoutAction.LogOut]);
authService.authStatusFor$.mockImplementation((userId) => {
return from([
accounts[userId]?.authStatus ?? AuthenticationStatus.LoggedOut,
AuthenticationStatus.Locked,
]);
});
authService.getAuthStatus.mockImplementation((userId) => {
return Promise.resolve(accounts[userId]?.authStatus);
});
stateService.getIsAuthenticated.mockImplementation((options) => {
// Just like actual state service, if no userId is given fallback to active userId
return Promise.resolve(accounts[options.userId ?? globalSetups?.userId]?.isAuthenticated);
});
vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockImplementation((userId) => {
return new BehaviorSubject<VaultTimeout>(accounts[userId]?.vaultTimeout);
});
// Set desired user active and known users on accounts service : note the only thing that matters here is that the ID are set
if (globalSetups?.userId) {
accountService.activeAccountSubject.next({
id: globalSetups.userId as UserId,
email: null,
emailVerified: false,
name: null,
});
}
accountService.accounts$ = of(
Object.entries(accounts).reduce(
(agg, [id]) => {
agg[id] = {
email: "",
emailVerified: true,
name: "",
};
return agg;
},
{} as Record<string, AccountInfo>,
),
);
accountService.accountActivity$ = of(
Object.entries(accounts).reduce(
(agg, [id, info]) => {
agg[id] = info.lastActive ? new Date(info.lastActive) : null;
return agg;
},
{} as Record<string, Date>,
),
);
platformUtilsService.isViewOpen.mockResolvedValue(globalSetups?.isViewOpen ?? false);
vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockImplementation((userId) => {
return new BehaviorSubject<VaultTimeoutAction>(accounts[userId]?.timeoutAction);
});
vaultTimeoutSettingsService.availableVaultTimeoutActions$.mockImplementation((userId) => {
return new BehaviorSubject<VaultTimeoutAction[]>(
// Default to both options if it wasn't supplied at all
accounts[userId]?.availableTimeoutActions ?? [
VaultTimeoutAction.Lock,
VaultTimeoutAction.LogOut,
],
);
});
};
const expectUserToHaveLocked = (userId: string) => {
// This does NOT assert all the things that the lock process does
expect(stateService.getIsAuthenticated).toHaveBeenCalledWith({ userId: userId });
expect(vaultTimeoutSettingsService.availableVaultTimeoutActions$).toHaveBeenCalledWith(userId);
expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(null, { userId: userId });
expect(masterPasswordService.mock.clearMasterKey).toHaveBeenCalledWith(userId);
expect(cipherService.clearCache).toHaveBeenCalledWith(userId);
expect(lockedCallback).toHaveBeenCalledWith(userId);
};
const expectUserToHaveLoggedOut = (userId: string) => {
expect(loggedOutCallback).toHaveBeenCalledWith("vaultTimeout", userId);
};
const expectNoAction = (userId: string) => {
expect(lockedCallback).not.toHaveBeenCalledWith(userId);
expect(loggedOutCallback).not.toHaveBeenCalledWith(any(), userId);
};
describe("checkVaultTimeout", () => {
it.each([AuthenticationStatus.Locked, AuthenticationStatus.LoggedOut])(
"should not try to log out or lock any user that has authStatus === %s.",
async (authStatus) => {
platformUtilsService.isViewOpen.mockResolvedValue(false);
setupAccounts({
1: {
authStatus: authStatus,
isAuthenticated: true,
},
});
expectNoAction("1");
},
);
it.each([
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 as VaultTimeout,
isAuthenticated: true,
},
});
await vaultTimeoutService.checkVaultTimeout();
expectNoAction("1");
},
);
it.each([undefined, null])(
"should not log out or lock a user who has %s lastActive value",
async (lastActive) => {
setupAccounts({
1: {
authStatus: AuthenticationStatus.Unlocked,
vaultTimeout: 1, // One minute
lastActive: lastActive,
},
});
await vaultTimeoutService.checkVaultTimeout();
expectNoAction("1");
},
);
it("should lock an account that isn't active and has immediate as their timeout when view is not open", async () => {
// Arrange
setupAccounts(
{
1: {
authStatus: AuthenticationStatus.Unlocked,
isAuthenticated: true,
vaultTimeout: 0, // Immediately
lastActive: new Date().getTime() - 10 * 1000, // Last active 10 seconds ago
},
2: {
authStatus: AuthenticationStatus.Unlocked,
isAuthenticated: true,
vaultTimeout: 1, // One minute
lastActive: new Date().getTime() - 10 * 1000, // Last active 10 seconds ago
},
},
{
isViewOpen: false,
},
);
// Act
await vaultTimeoutService.checkVaultTimeout();
// Assert
expectUserToHaveLocked("1");
expectNoAction("2");
});
it("should run action on an account that hasn't been active for greater than 1 minute and has a vault timeout for 1 minutes", async () => {
setupAccounts(
{
1: {
authStatus: AuthenticationStatus.Unlocked,
isAuthenticated: true,
vaultTimeout: 1, // One minute
lastActive: new Date().getTime() - 10 * 1000,
},
2: {
authStatus: AuthenticationStatus.Unlocked,
isAuthenticated: true,
vaultTimeout: 1, // One minute
lastActive: new Date().getTime() - 61 * 1000, // Last active 61 seconds ago
},
3: {
authStatus: AuthenticationStatus.Unlocked,
isAuthenticated: true,
vaultTimeout: 1, // One minute
lastActive: new Date().getTime() - 120 * 1000, // Last active 2 minutes ago
timeoutAction: VaultTimeoutAction.LogOut,
availableTimeoutActions: [VaultTimeoutAction.Lock, VaultTimeoutAction.LogOut],
},
4: {
authStatus: AuthenticationStatus.Unlocked,
isAuthenticated: true,
vaultTimeout: 1, // One minute
lastActive: new Date().getTime() - 100 * 1000, // Last active 100 seconds ago
timeoutAction: VaultTimeoutAction.Lock,
availableTimeoutActions: [VaultTimeoutAction.LogOut],
},
},
{ userId: "2", isViewOpen: false }, // Treat user 2 as the active user
);
await vaultTimeoutService.checkVaultTimeout();
expectNoAction("1");
expectUserToHaveLocked("2");
// Active users should have additional steps ran
expect(searchService.clearIndex).toHaveBeenCalled();
expect(folderService.clearDecryptedFolderState).toHaveBeenCalled();
expectUserToHaveLoggedOut("3"); // They have chosen logout as their action and it's available, log them out
expectUserToHaveLoggedOut("4"); // They may have had lock as their chosen action but it's not available to them so logout
});
it("should lock an account if they haven't been active passed their vault timeout even if a view is open when they are not the active user.", async () => {
setupAccounts(
{
1: {
// Neither of these setup values ever get called
authStatus: AuthenticationStatus.Unlocked,
isAuthenticated: true,
lastActive: new Date().getTime() - 80 * 1000, // Last active 80 seconds ago
vaultTimeout: 1, // Vault timeout of 1 minute
},
},
{ userId: "2", isViewOpen: true },
);
await vaultTimeoutService.checkVaultTimeout();
expectUserToHaveLocked("1");
});
it("should not lock an account that is active and we know that a view is open, even if they haven't been active passed their timeout", async () => {
setupAccounts(
{
1: {
authStatus: AuthenticationStatus.Unlocked,
isAuthenticated: true,
lastActive: new Date().getTime() - 80 * 1000, // Last active 80 seconds ago
vaultTimeout: 1, // Vault timeout of 1 minute
},
},
{ userId: "1", isViewOpen: true }, // They are the currently active user
);
await vaultTimeoutService.checkVaultTimeout();
expectNoAction("1");
});
});
describe("lock", () => {
const setupLock = () => {
setupAccounts(
{
user1: {
authStatus: AuthenticationStatus.Unlocked,
isAuthenticated: true,
},
user2: {
authStatus: AuthenticationStatus.Unlocked,
isAuthenticated: true,
},
},
{
userId: "user1",
},
);
};
it("should call state event runner with currently active user if no user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock();
expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", "user1");
});
it("should call locked callback if no user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock();
// Currently these pass `undefined` (or what they were given) as the userId back
// but we could change this to give the user that was locked (active) to these methods
// so they don't have to get it their own way, but that is a behavioral change that needs
// to be tested.
expect(lockedCallback).toHaveBeenCalledWith(undefined);
});
it("should call state event runner with user passed into lock", async () => {
setupLock();
const user2 = "user2" as UserId;
await vaultTimeoutService.lock(user2);
expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", user2);
});
it("should call messaging service locked message with user passed into lock", async () => {
setupLock();
const user2 = "user2" as UserId;
await vaultTimeoutService.lock(user2);
expect(messagingService.send).toHaveBeenCalledWith("locked", { userId: user2 });
});
it("should call locked callback with user passed into lock", async () => {
setupLock();
const user2 = "user2" as UserId;
await vaultTimeoutService.lock(user2);
expect(lockedCallback).toHaveBeenCalledWith(user2);
});
});
});

View File

@@ -1,219 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { combineLatest, concatMap, filter, firstValueFrom, map, timeout } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { LogoutReason } from "@bitwarden/auth/common";
import { BiometricsService } from "@bitwarden/key-management";
import { SearchService } from "../../abstractions/search.service";
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout.service";
import { AccountService } from "../../auth/abstractions/account.service";
import { AuthService } from "../../auth/abstractions/auth.service";
import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction";
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { TaskSchedulerService, ScheduledTaskNames } from "../../platform/scheduling";
import { StateEventRunnerService } from "../../platform/state";
import { UserId } from "../../types/guid";
import { CipherService } from "../../vault/abstractions/cipher.service";
import { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction";
export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
private inited = false;
constructor(
private accountService: AccountService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
private cipherService: CipherService,
private folderService: FolderService,
private collectionService: CollectionService,
protected platformUtilsService: PlatformUtilsService,
private messagingService: MessagingService,
private searchService: SearchService,
private stateService: StateService,
private authService: AuthService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private stateEventRunnerService: StateEventRunnerService,
private taskSchedulerService: TaskSchedulerService,
protected logService: LogService,
private biometricService: BiometricsService,
private lockedCallback: (userId?: string) => Promise<void> = null,
private loggedOutCallback: (
logoutReason: LogoutReason,
userId?: string,
) => Promise<void> = null,
) {
this.taskSchedulerService.registerTaskHandler(
ScheduledTaskNames.vaultTimeoutCheckInterval,
() => this.checkVaultTimeout(),
);
}
async init(checkOnInterval: boolean) {
if (this.inited) {
return;
}
this.inited = true;
if (checkOnInterval) {
this.startCheck();
}
}
startCheck() {
this.checkVaultTimeout().catch((error) => this.logService.error(error));
this.taskSchedulerService.setInterval(
ScheduledTaskNames.vaultTimeoutCheckInterval,
10 * 1000, // check every 10 seconds
);
}
async checkVaultTimeout(): Promise<void> {
// Get whether or not the view is open a single time so it can be compared for each user
const isViewOpen = await this.platformUtilsService.isViewOpen();
await firstValueFrom(
combineLatest([
this.accountService.activeAccount$,
this.accountService.accountActivity$,
]).pipe(
concatMap(async ([activeAccount, accountActivity]) => {
const activeUserId = activeAccount?.id;
for (const userIdString in accountActivity) {
const userId = userIdString as UserId;
if (
userId != null &&
(await this.shouldLock(userId, accountActivity[userId], activeUserId, isViewOpen))
) {
await this.executeTimeoutAction(userId);
}
}
}),
),
);
}
async lock(userId?: UserId): Promise<void> {
await this.biometricService.setShouldAutopromptNow(false);
const authed = await this.stateService.getIsAuthenticated({ userId: userId });
if (!authed) {
return;
}
const availableActions = await firstValueFrom(
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(userId),
);
const supportsLock = availableActions.includes(VaultTimeoutAction.Lock);
if (!supportsLock) {
await this.logOut(userId);
}
const currentUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const lockingUserId = userId ?? currentUserId;
// HACK: Start listening for the transition of the locking user from something to the locked state.
// This is very much a hack to ensure that the authentication status to retrievable right after
// it does its work. Particularly the `lockedCallback` and `"locked"` message. Instead
// lockedCallback should be deprecated and people should subscribe and react to `authStatusFor$` themselves.
const lockPromise = firstValueFrom(
this.authService.authStatusFor$(lockingUserId).pipe(
filter((authStatus) => authStatus === AuthenticationStatus.Locked),
timeout({
first: 5_000,
with: () => {
throw new Error("The lock process did not complete in a reasonable amount of time.");
},
}),
),
);
if (userId == null || userId === currentUserId) {
await this.searchService.clearIndex();
await this.collectionService.clearActiveUserCache();
}
await this.folderService.clearDecryptedFolderState(lockingUserId);
await this.masterPasswordService.clearMasterKey(lockingUserId);
await this.stateService.setUserKeyAutoUnlock(null, { userId: lockingUserId });
await this.stateService.setCryptoMasterKeyAuto(null, { userId: lockingUserId });
await this.cipherService.clearCache(lockingUserId);
await this.stateEventRunnerService.handleEvent("lock", lockingUserId);
// HACK: Sit here and wait for the the auth status to transition to `Locked`
// to ensure the message and lockedCallback will get the correct status
// if/when they call it.
await lockPromise;
this.messagingService.send("locked", { userId: lockingUserId });
if (this.lockedCallback != null) {
await this.lockedCallback(userId);
}
}
async logOut(userId?: string): Promise<void> {
if (this.loggedOutCallback != null) {
await this.loggedOutCallback("vaultTimeout", userId);
}
}
private async shouldLock(
userId: string,
lastActive: Date,
activeUserId: string,
isViewOpen: boolean,
): Promise<boolean> {
if (isViewOpen && userId === activeUserId) {
// We know a view is open and this is the currently active user
// which means they are likely looking at their vault
// and they should not lock.
return false;
}
const authStatus = await this.authService.getAuthStatus(userId);
if (
authStatus === AuthenticationStatus.Locked ||
authStatus === AuthenticationStatus.LoggedOut
) {
return false;
}
const vaultTimeout = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId),
);
if (typeof vaultTimeout === "string") {
return false;
}
if (lastActive == null) {
return false;
}
const vaultTimeoutSeconds = vaultTimeout * 60;
const diffSeconds = (new Date().getTime() - lastActive.getTime()) / 1000;
return diffSeconds >= vaultTimeoutSeconds;
}
private async executeTimeoutAction(userId: UserId): Promise<void> {
const timeoutAction = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId),
);
timeoutAction === VaultTimeoutAction.LogOut
? await this.logOut(userId)
: await this.lock(userId);
}
}