1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 13:53:34 +00:00
Files
browser/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.ts
Brandon Treston 0fd01ed7ee [PM-18566] Wire up vNextPolicyService for Clients (#13678)
* wire up vNext impl

* wire up vNextPolicyService for browser

* wire up vNextPolicyService for desktop

* wire up vNextPolicyService for cli

* fix test

* fix missed caller

* cleanup

* fix missing property assignment

* fix QA bug for PM-19205

* fix QA bug for PM-19206

* fix QA bug for pm-19228

* cleanup
2025-03-25 11:30:47 -04:00

306 lines
11 KiB
TypeScript

// 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,
shareReplay,
switchMap,
tap,
} from "rxjs";
import {
PinServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { BiometricStateService, KeyService } from "@bitwarden/key-management";
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 { getFirstPolicy } from "../../../admin-console/services/policy/default-policy.service";
import { AccountService } from "../../../auth/abstractions/account.service";
import { TokenService } from "../../../auth/abstractions/token.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { StateProvider } from "../../../platform/state";
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 { 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
.policiesByType$(PolicyType.MaximumVaultTimeout, userId)
.pipe(getFirstPolicy);
}
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$);
}
}
}