From bba2812fdd1b75b8fd8894568f89926a1fd5467f Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Wed, 8 Sep 2021 22:02:19 +0200 Subject: [PATCH] Vault Timeout Policy (#474) --- .../settings/vault-timeout-input.component.ts | 138 ++++++++++++++++++ .../src/abstractions/platformUtils.service.ts | 4 - .../src/abstractions/vaultTimeout.service.ts | 1 + common/src/enums/policyType.ts | 1 + common/src/services/vaultTimeout.service.ts | 34 ++++- .../services/electronPlatformUtils.service.ts | 4 - .../cli/services/cliPlatformUtils.service.ts | 4 - 7 files changed, 167 insertions(+), 19 deletions(-) create mode 100644 angular/src/components/settings/vault-timeout-input.component.ts diff --git a/angular/src/components/settings/vault-timeout-input.component.ts b/angular/src/components/settings/vault-timeout-input.component.ts new file mode 100644 index 0000000000..749e75953a --- /dev/null +++ b/angular/src/components/settings/vault-timeout-input.component.ts @@ -0,0 +1,138 @@ +import { + Directive, + Input, + OnInit, +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormBuilder, + ValidationErrors, + Validator +} from '@angular/forms'; + +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { PolicyService } from 'jslib-common/abstractions/policy.service'; + +import { PolicyType } from 'jslib-common/enums/policyType'; +import { Policy } from 'jslib-common/models/domain/policy'; + +@Directive() +export class VaultTimeoutInputComponent implements ControlValueAccessor, Validator, OnInit { + + get showCustom() { + return this.form.get('vaultTimeout').value === VaultTimeoutInputComponent.CUSTOM_VALUE; + } + + static CUSTOM_VALUE = -100; + + form = this.fb.group({ + vaultTimeout: [null], + custom: this.fb.group({ + hours: [null], + minutes: [null], + }), + }); + + @Input() vaultTimeouts: { name: string; value: number; }[]; + vaultTimeoutPolicy: Policy; + vaultTimeoutPolicyHours: number; + vaultTimeoutPolicyMinutes: number; + + private onChange: (vaultTimeout: number) => void; + private validatorChange: () => void; + + constructor(private fb: FormBuilder, private policyService: PolicyService, private i18nService: I18nService) { + } + + async ngOnInit() { + if (await this.policyService.policyAppliesToUser(PolicyType.MaximumVaultTimeout)) { + const vaultTimeoutPolicy = await this.policyService.getAll(PolicyType.MaximumVaultTimeout); + + this.vaultTimeoutPolicy = vaultTimeoutPolicy[0]; + this.vaultTimeoutPolicyHours = Math.floor(this.vaultTimeoutPolicy.data.minutes / 60); + this.vaultTimeoutPolicyMinutes = this.vaultTimeoutPolicy.data.minutes % 60; + + this.vaultTimeouts = this.vaultTimeouts.filter(t => + t.value <= this.vaultTimeoutPolicy.data.minutes && + (t.value > 0 || t.value === VaultTimeoutInputComponent.CUSTOM_VALUE) && + t.value != null + ); + this.validatorChange(); + } + + this.form.valueChanges.subscribe(async value => { + this.onChange(this.getVaultTimeout(value)); + }); + + // Assign the previous value to the custom fields + this.form.get('vaultTimeout').valueChanges.subscribe(value => { + if (value !== VaultTimeoutInputComponent.CUSTOM_VALUE) { + return; + } + + const current = Math.max(this.form.value.vaultTimeout, 0); + this.form.patchValue({ + custom: { + hours: Math.floor(current / 60), + minutes: current % 60, + }, + }); + }); + } + + ngOnChanges() { + this.vaultTimeouts.push({ name: this.i18nService.t('custom'), value: VaultTimeoutInputComponent.CUSTOM_VALUE }); + } + + getVaultTimeout(value: any) { + if (value.vaultTimeout !== VaultTimeoutInputComponent.CUSTOM_VALUE) { + return value.vaultTimeout; + } + + return value.custom.hours * 60 + value.custom.minutes; + } + + writeValue(value: number): void { + if (value == null) { + return; + } + + if (this.vaultTimeouts.every(p => p.value !== value)) { + this.form.setValue({ + vaultTimeout: VaultTimeoutInputComponent.CUSTOM_VALUE, + custom: { + hours: Math.floor(value / 60), + minutes: value % 60, + }, + }); + return; + } + + this.form.patchValue({ + vaultTimeout: value, + }); + } + + registerOnChange(onChange: any): void { + this.onChange = onChange; + } + + // tslint:disable-next-line + registerOnTouched(onTouched: any): void {} + + // tslint:disable-next-line + setDisabledState?(isDisabled: boolean): void { } + + validate(control: AbstractControl): ValidationErrors { + if (this.vaultTimeoutPolicy && this.vaultTimeoutPolicy?.data?.minutes < control.value) { + return { policyError: true }; + } + + return null; + } + + registerOnValidatorChange(fn: () => void): void { + this.validatorChange = fn; + } +} diff --git a/common/src/abstractions/platformUtils.service.ts b/common/src/abstractions/platformUtils.service.ts index 8828d3af7b..5e0f311a12 100644 --- a/common/src/abstractions/platformUtils.service.ts +++ b/common/src/abstractions/platformUtils.service.ts @@ -13,10 +13,6 @@ export abstract class PlatformUtilsService { isIE: () => boolean; isMacAppStore: () => boolean; isViewOpen: () => Promise; - /** - * @deprecated This only ever returns null. Pull from your platform's storage using ConstantsService.vaultTimeoutKey - */ - lockTimeout: () => number; launchUri: (uri: string, options?: any) => void; saveFile: (win: Window, blobData: any, blobOptions: any, fileName: string) => void; getApplicationVersion: () => Promise; diff --git a/common/src/abstractions/vaultTimeout.service.ts b/common/src/abstractions/vaultTimeout.service.ts index d709d363b3..a5944c4813 100644 --- a/common/src/abstractions/vaultTimeout.service.ts +++ b/common/src/abstractions/vaultTimeout.service.ts @@ -9,6 +9,7 @@ export abstract class VaultTimeoutService { lock: (allowSoftLock?: boolean) => Promise; logOut: () => Promise; setVaultTimeoutOptions: (vaultTimeout: number, vaultTimeoutAction: string) => Promise; + getVaultTimeout: () => Promise; isPinLockSet: () => Promise<[boolean, boolean]>; isBiometricLockSet: () => Promise; clear: () => Promise; diff --git a/common/src/enums/policyType.ts b/common/src/enums/policyType.ts index 8d5020988a..9af8cebd9b 100644 --- a/common/src/enums/policyType.ts +++ b/common/src/enums/policyType.ts @@ -8,4 +8,5 @@ export enum PolicyType { DisableSend = 6, // Disables the ability to create and edit Bitwarden Sends SendOptions = 7, // Sets restrictions or defaults for Bitwarden Sends ResetPassword = 8, // Allows orgs to use reset password : also can enable auto-enrollment during invite flow + MaximumVaultTimeout = 9, // Sets the maximum allowed vault timeout } diff --git a/common/src/services/vaultTimeout.service.ts b/common/src/services/vaultTimeout.service.ts index b08ed1d205..907f382995 100644 --- a/common/src/services/vaultTimeout.service.ts +++ b/common/src/services/vaultTimeout.service.ts @@ -6,12 +6,14 @@ import { CryptoService } from '../abstractions/crypto.service'; import { FolderService } from '../abstractions/folder.service'; import { MessagingService } from '../abstractions/messaging.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; +import { PolicyService } from '../abstractions/policy.service'; import { SearchService } from '../abstractions/search.service'; import { StorageService } from '../abstractions/storage.service'; import { TokenService } from '../abstractions/token.service'; import { UserService } from '../abstractions/user.service'; import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from '../abstractions/vaultTimeout.service'; +import { PolicyType } from '../enums/policyType'; import { EncString } from '../models/domain/encString'; export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { @@ -25,7 +27,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { private collectionService: CollectionService, private cryptoService: CryptoService, protected platformUtilsService: PlatformUtilsService, private storageService: StorageService, private messagingService: MessagingService, private searchService: SearchService, - private userService: UserService, private tokenService: TokenService, + private userService: UserService, private tokenService: TokenService, private policyService: PolicyService, private lockedCallback: () => Promise = null, private loggedOutCallback: () => Promise = null) { } @@ -71,12 +73,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { return; } - // This has the potential to be removed. Evaluate after all platforms complete with auto-logout - let vaultTimeout = this.platformUtilsService.lockTimeout(); - if (vaultTimeout == null) { - vaultTimeout = await this.storageService.get(ConstantsService.vaultTimeoutKey); - } - + const vaultTimeout = await this.getVaultTimeout(); if (vaultTimeout == null || vaultTimeout < 0) { return; } @@ -141,6 +138,29 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { return await this.storageService.get(ConstantsService.biometricUnlockKey); } + async getVaultTimeout(): Promise { + const vaultTimeout = await this.storageService.get(ConstantsService.vaultTimeoutKey); + + if (await this.policyService.policyAppliesToUser(PolicyType.MaximumVaultTimeout)) { + const policy = await this.policyService.getAll(PolicyType.MaximumVaultTimeout); + // Remove negative values, and ensure it's smaller than maximum allowed value according to policy + let timeout = Math.min(vaultTimeout, policy[0].data.minutes); + + if (vaultTimeout == null || timeout < 0) { + timeout = policy[0].data.minutes; + } + + // We really shouldn't need to set the value here, but multiple services relies on this value being correct. + if (vaultTimeout !== timeout) { + await this.storageService.save(ConstantsService.vaultTimeoutKey, timeout); + } + + return timeout; + } + + return vaultTimeout; + } + clear(): Promise { this.everBeenUnlocked = false; this.pinProtectedKey = null; diff --git a/electron/src/services/electronPlatformUtils.service.ts b/electron/src/services/electronPlatformUtils.service.ts index 87f719defd..07f9660396 100644 --- a/electron/src/services/electronPlatformUtils.service.ts +++ b/electron/src/services/electronPlatformUtils.service.ts @@ -88,10 +88,6 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { return Promise.resolve(false); } - lockTimeout(): number { - return null; - } - launchUri(uri: string, options?: any): void { shell.openExternal(uri); } diff --git a/node/src/cli/services/cliPlatformUtils.service.ts b/node/src/cli/services/cliPlatformUtils.service.ts index fe2af22c4e..0b81b2e854 100644 --- a/node/src/cli/services/cliPlatformUtils.service.ts +++ b/node/src/cli/services/cliPlatformUtils.service.ts @@ -76,10 +76,6 @@ export class CliPlatformUtilsService implements PlatformUtilsService { return Promise.resolve(false); } - lockTimeout(): number { - return null; - } - launchUri(uri: string, options?: any): void { if (process.platform === 'linux') { child_process.spawnSync('xdg-open', [uri]);