mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 15:23:33 +00:00
* 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
388 lines
17 KiB
TypeScript
388 lines
17 KiB
TypeScript
// FIXME: Update this file to be type safe and remove this and next line
|
|
// @ts-strict-ignore
|
|
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, FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
|
|
import { PolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
|
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
|
import { TokenService } from "../../../auth/services/token.service";
|
|
import { LogService } from "../../../platform/abstractions/log.service";
|
|
import { Utils } from "../../../platform/misc/utils";
|
|
import { UserId } from "../../../types/guid";
|
|
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../abstractions/vault-timeout-settings.service";
|
|
import { VaultTimeoutAction } from "../enums/vault-timeout-action.enum";
|
|
import { VaultTimeout, VaultTimeoutStringType } from "../types/vault-timeout.type";
|
|
|
|
import { VaultTimeoutSettingsService } from "./vault-timeout-settings.service";
|
|
import { VAULT_TIMEOUT, VAULT_TIMEOUT_ACTION } from "./vault-timeout-settings.state";
|
|
|
|
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,
|
|
);
|
|
}
|
|
});
|