From fbbaf10488bb6452969d56397563dac2181a4a9b Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Fri, 14 Apr 2023 19:11:33 -0400 Subject: [PATCH] [AC-1045] add action to vault timeout policy (#4782) --- apps/browser/src/_locales/en/messages.json | 28 +- .../browser/src/background/idle.background.ts | 3 +- .../popup/settings/settings.component.html | 36 +- .../src/popup/settings/settings.component.ts | 183 +++++--- .../vault-timeout-input.component.html | 6 +- .../src/app/accounts/settings.component.html | 102 ++--- .../src/app/accounts/settings.component.ts | 412 +++++++++++------- .../vault-timeout-input.component.html | 6 +- apps/desktop/src/locales/en/messages.json | 28 +- .../app/settings/preferences.component.html | 37 +- .../src/app/settings/preferences.component.ts | 138 ++++-- .../vault-timeout-input.component.html | 6 +- apps/web/src/locales/en/messages.json | 28 +- .../maximum-vault-timeout.component.html | 12 + .../maximum-vault-timeout.component.ts | 27 +- .../settings/vault-timeout-input.component.ts | 75 ++-- .../vaultTimeoutSettings.service.ts | 8 +- .../policy/policy.service.abstraction.ts | 1 + .../services/policy/policy.service.ts | 22 + .../src/enums/vault-timeout-action.enum.ts | 4 + libs/common/src/services/state.service.ts | 8 +- .../vaultTimeout/vaultTimeout.service.ts | 7 +- .../vaultTimeoutSettings.service.ts | 32 +- 23 files changed, 801 insertions(+), 408 deletions(-) create mode 100644 libs/common/src/enums/vault-timeout-action.enum.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 70adbdceb4..8c232846bc 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1863,7 +1863,7 @@ "message": "Minutes" }, "vaultTimeoutPolicyInEffect": { - "message": "Your organization policies are affecting your vault timeout. Maximum allowed Vault Timeout is $HOURS$ hour(s) and $MINUTES$ minute(s)", + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", "placeholders": { "hours": { "content": "$1", @@ -1875,6 +1875,32 @@ } } }, + "vaultTimeoutPolicyWithActionInEffect": { + "message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + }, + "action": { + "content": "$3", + "example": "Lock" + } + } + }, + "vaultTimeoutActionPolicyInEffect": { + "message": "Your organization policies have set your vault timeout action to $ACTION$.", + "placeholders": { + "action": { + "content": "$1", + "example": "Lock" + } + } + }, "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, diff --git a/apps/browser/src/background/idle.background.ts b/apps/browser/src/background/idle.background.ts index 1a8c5ae5c5..0037340f03 100644 --- a/apps/browser/src/background/idle.background.ts +++ b/apps/browser/src/background/idle.background.ts @@ -1,5 +1,6 @@ import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service"; +import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { BrowserStateService } from "../services/abstractions/browser-state.service"; @@ -45,7 +46,7 @@ export default class IdleBackground { if (timeout === -2) { // On System Lock vault timeout option const action = await this.stateService.getVaultTimeoutAction(); - if (action === "logOut") { + if (action === VaultTimeoutAction.LogOut) { await this.vaultTimeoutService.logOut(); } else { await this.vaultTimeoutService.lock(); diff --git a/apps/browser/src/popup/settings/settings.component.html b/apps/browser/src/popup/settings/settings.component.html index 539e971b5f..987127d3d8 100644 --- a/apps/browser/src/popup/settings/settings.component.html +++ b/apps/browser/src/popup/settings/settings.component.html @@ -7,7 +7,7 @@
-
+

{{ "manage" | i18n }}

@@ -48,9 +48,23 @@

{{ "security" | i18n }}

+ + + {{ + "vaultTimeoutPolicyWithActionInEffect" + | i18n : policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n) + }} + + + {{ "vaultTimeoutPolicyInEffect" | i18n : policy.timeout.hours : policy.timeout.minutes }} + + + {{ "vaultTimeoutActionPolicyInEffect" | i18n : (policy.action | i18n) }} + + @@ -60,15 +74,16 @@ #vaultTimeoutActionSelect id="vaultTimeoutAction" name="VaultTimeoutActions" - [ngModel]="vaultTimeoutAction" - (ngModelChange)="saveVaultTimeoutAction($event)" + formControlName="vaultTimeoutAction" > - +
- +
@@ -76,21 +91,20 @@ id="biometric" type="checkbox" (change)="updateBiometric()" - [(ngModel)]="biometric" + formControlName="biometric" />
+ + + {{ + "vaultTimeoutPolicyWithActionInEffect" + | i18n + : policy.timeout.hours + : policy.timeout.minutes + : (policy.action | i18n) + }} + + + {{ + "vaultTimeoutPolicyInEffect" + | i18n : policy.timeout.hours : policy.timeout.minutes + }} + + + {{ "vaultTimeoutActionPolicyInEffect" | i18n : (policy.action | i18n) }} + +
@@ -41,12 +61,10 @@ @@ -58,12 +76,10 @@ @@ -75,13 +91,7 @@
@@ -92,22 +102,20 @@ {{ biometricText | i18n }}
-
+
@@ -379,8 +374,7 @@ {{ "enableDuckDuckGoBrowserIntegration" | i18n }} @@ -394,9 +388,8 @@ diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 2151efff04..1c515ed51d 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit } from "@angular/core"; -import { UntypedFormControl } from "@angular/forms"; -import { debounceTime } from "rxjs/operators"; +import { FormBuilder } from "@angular/forms"; +import { Observable, Subject } from "rxjs"; +import { concatMap, debounceTime, filter, map, takeUntil, tap } from "rxjs/operators"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction"; @@ -10,7 +11,10 @@ import { MessagingService } from "@bitwarden/common/abstractions/messaging.servi import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { DeviceType, ThemeType, StorageLocation } from "@bitwarden/common/enums"; +import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { Utils } from "@bitwarden/common/misc/utils"; import { flagEnabled } from "../../flags"; @@ -23,36 +27,20 @@ import { SetPinComponent } from "../components/set-pin.component"; }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil export class SettingsComponent implements OnInit { - vaultTimeoutAction: string; - pin: boolean = null; - enableFavicons = false; - enableBrowserIntegration = false; - enableDuckDuckGoBrowserIntegration = false; - enableBrowserIntegrationFingerprint = false; - enableMinToTray = false; - enableCloseToTray = false; - enableTray = false; + // For use in template + protected readonly VaultTimeoutAction = VaultTimeoutAction; + showMinToTray = false; - startToTray = false; - minimizeOnCopyToClipboard = false; - locale: string; - vaultTimeouts: any[]; + vaultTimeoutOptions: any[]; localeOptions: any[]; - theme: ThemeType; themeOptions: any[]; - clearClipboard: number; clearClipboardOptions: any[]; supportsBiometric: boolean; - biometric: boolean; biometricText: string; - autoPromptBiometrics: boolean; autoPromptBiometricsText: string; - alwaysShowDock: boolean; showAlwaysShowDock = false; - openAtLogin: boolean; requireEnableTray = false; showDuckDuckGoIntegrationOption = false; - approveLoginRequests = false; enableTrayText: string; enableTrayDescText: string; @@ -63,17 +51,52 @@ export class SettingsComponent implements OnInit { startToTrayText: string; startToTrayDescText: string; - vaultTimeout: UntypedFormControl = new UntypedFormControl(null); - showSecurity = true; showAccountPreferences = true; showAppPreferences = true; currentUserEmail: string; + vaultTimeoutPolicyCallout: Observable<{ + timeout: { hours: number; minutes: number }; + action: "lock" | "logOut"; + }>; previousVaultTimeout: number = null; + form = this.formBuilder.group({ + // Security + vaultTimeout: [null as number | null], + vaultTimeoutAction: [VaultTimeoutAction.Lock], + pin: [null as boolean | null], + biometric: false, + autoPromptBiometrics: false, + approveLoginRequests: false, + // Account Preferences + clearClipboard: [null as number | null], + minimizeOnCopyToClipboard: false, + enableFavicons: false, + // App Settings + enableTray: false, + enableMinToTray: false, + enableCloseToTray: false, + startToTray: false, + openAtLogin: false, + alwaysShowDock: false, + enableBrowserIntegration: false, + enableBrowserIntegrationFingerprint: this.formBuilder.control({ + value: false, + disabled: true, + }), + enableDuckDuckGoBrowserIntegration: false, + theme: [null as ThemeType | null], + locale: [null as string | null], + }); + + private destroy$ = new Subject(); + constructor( + private policyService: PolicyService, + private formBuilder: FormBuilder, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, @@ -107,108 +130,158 @@ export class SettingsComponent implements OnInit { // DuckDuckGo browser is only for macos initially this.showDuckDuckGoIntegrationOption = flagEnabled("showDDGSetting") && isMac; - this.vaultTimeouts = [ + this.vaultTimeoutOptions = [ // { name: i18nService.t('immediately'), value: 0 }, - { name: i18nService.t("oneMinute"), value: 1 }, - { name: i18nService.t("fiveMinutes"), value: 5 }, - { name: i18nService.t("fifteenMinutes"), value: 15 }, - { name: i18nService.t("thirtyMinutes"), value: 30 }, - { name: i18nService.t("oneHour"), value: 60 }, - { name: i18nService.t("fourHours"), value: 240 }, - { name: i18nService.t("onIdle"), value: -4 }, - { name: i18nService.t("onSleep"), value: -3 }, + { 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 }, ]; if (this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop) { - this.vaultTimeouts.push({ name: i18nService.t("onLocked"), value: -2 }); + this.vaultTimeoutOptions.push({ name: this.i18nService.t("onLocked"), value: -2 }); } - this.vaultTimeouts = this.vaultTimeouts.concat([ - { name: i18nService.t("onRestart"), value: -1 }, - { name: i18nService.t("never"), value: null }, + this.vaultTimeoutOptions = this.vaultTimeoutOptions.concat([ + { name: this.i18nService.t("onRestart"), value: -1 }, + { name: this.i18nService.t("never"), value: null }, ]); const localeOptions: any[] = []; - i18nService.supportedTranslationLocales.forEach((locale) => { + this.i18nService.supportedTranslationLocales.forEach((locale) => { let name = locale; - if (i18nService.localeNames.has(locale)) { - name += " - " + i18nService.localeNames.get(locale); + if (this.i18nService.localeNames.has(locale)) { + name += " - " + this.i18nService.localeNames.get(locale); } localeOptions.push({ name: name, value: locale }); }); - localeOptions.sort(Utils.getSortFunction(i18nService, "name")); - localeOptions.splice(0, 0, { name: i18nService.t("default"), value: null }); + localeOptions.sort(Utils.getSortFunction(this.i18nService, "name")); + localeOptions.splice(0, 0, { name: this.i18nService.t("default"), value: null }); this.localeOptions = localeOptions; this.themeOptions = [ - { name: i18nService.t("default"), value: ThemeType.System }, - { name: i18nService.t("light"), value: ThemeType.Light }, - { name: i18nService.t("dark"), value: ThemeType.Dark }, + { name: this.i18nService.t("default"), value: ThemeType.System }, + { name: this.i18nService.t("light"), value: ThemeType.Light }, + { name: this.i18nService.t("dark"), value: ThemeType.Dark }, { name: "Nord", value: ThemeType.Nord }, ]; this.clearClipboardOptions = [ - { name: i18nService.t("never"), value: null }, - { name: i18nService.t("tenSeconds"), value: 10 }, - { name: i18nService.t("twentySeconds"), value: 20 }, - { name: i18nService.t("thirtySeconds"), value: 30 }, - { name: i18nService.t("oneMinute"), value: 60 }, - { name: i18nService.t("twoMinutes"), value: 120 }, - { name: i18nService.t("fiveMinutes"), value: 300 }, + { name: this.i18nService.t("never"), value: null }, + { name: this.i18nService.t("tenSeconds"), value: 10 }, + { name: this.i18nService.t("twentySeconds"), value: 20 }, + { name: this.i18nService.t("thirtySeconds"), value: 30 }, + { name: this.i18nService.t("oneMinute"), value: 60 }, + { name: this.i18nService.t("twoMinutes"), value: 120 }, + { name: this.i18nService.t("fiveMinutes"), value: 300 }, ]; } async ngOnInit() { - // App preferences - this.showMinToTray = this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop; - this.enableMinToTray = await this.stateService.getEnableMinimizeToTray(); - this.enableCloseToTray = await this.stateService.getEnableCloseToTray(); - this.enableTray = await this.stateService.getEnableTray(); - this.startToTray = await this.stateService.getEnableStartToTray(); - - this.alwaysShowDock = await this.stateService.getAlwaysShowDock(); - this.showAlwaysShowDock = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop; - this.openAtLogin = await this.stateService.getOpenAtLogin(); - - this.locale = (await this.stateService.getLocale()) ?? null; - this.theme = await this.stateService.getTheme(); - if ((await this.stateService.getUserId()) == null) { return; } this.currentUserEmail = await this.stateService.getEmail(); - // Security - this.vaultTimeout.setValue(await this.stateService.getVaultTimeout()); - this.vaultTimeoutAction = await this.stateService.getVaultTimeoutAction(); - this.previousVaultTimeout = this.vaultTimeout.value; - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - this.vaultTimeout.valueChanges.pipe(debounceTime(500)).subscribe(() => { - this.saveVaultTimeoutOptions(); - }); + // Load timeout policy + this.vaultTimeoutPolicyCallout = this.policyService.get$(PolicyType.MaximumVaultTimeout).pipe( + filter((policy) => policy != null), + map((policy) => { + let timeout; + if (policy.data?.minutes) { + timeout = { + hours: Math.floor(policy.data?.minutes / 60), + minutes: policy.data?.minutes % 60, + }; + } + return { timeout: timeout, action: policy.data?.action }; + }), + tap((policy) => { + if (policy.action) { + this.form.controls.vaultTimeoutAction.disable({ emitEvent: false }); + } else { + this.form.controls.vaultTimeoutAction.enable({ emitEvent: false }); + } + }) + ); + // Load initial values const pinSet = await this.vaultTimeoutSettingsService.isPinLockSet(); - this.pin = pinSet[0] || pinSet[1]; - this.approveLoginRequests = await this.stateService.getApproveLoginRequests(); + const initialValues = { + vaultTimeout: await this.vaultTimeoutSettingsService.getVaultTimeout(), + vaultTimeoutAction: await this.vaultTimeoutSettingsService.getVaultTimeoutAction(), + pin: pinSet[0] || pinSet[1], + biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(), + autoPromptBiometrics: !(await this.stateService.getNoAutoPromptBiometrics()), + approveLoginRequests: (await this.stateService.getApproveLoginRequests()) ?? false, + clearClipboard: await this.stateService.getClearClipboard(), + minimizeOnCopyToClipboard: await this.stateService.getMinimizeOnCopyToClipboard(), + enableFavicons: !(await this.stateService.getDisableFavicon()), + enableTray: await this.stateService.getEnableTray(), + enableMinToTray: await this.stateService.getEnableMinimizeToTray(), + enableCloseToTray: await this.stateService.getEnableCloseToTray(), + startToTray: await this.stateService.getEnableStartToTray(), + openAtLogin: await this.stateService.getOpenAtLogin(), + alwaysShowDock: await this.stateService.getAlwaysShowDock(), + enableBrowserIntegration: await this.stateService.getEnableBrowserIntegration(), + enableBrowserIntegrationFingerprint: + await this.stateService.getEnableBrowserIntegrationFingerprint(), + enableDuckDuckGoBrowserIntegration: + await this.stateService.getEnableDuckDuckGoBrowserIntegration(), + theme: await this.stateService.getTheme(), + locale: (await this.stateService.getLocale()) ?? null, + }; + this.form.setValue(initialValues, { emitEvent: false }); - // Account preferences - this.enableFavicons = !(await this.stateService.getDisableFavicon()); - this.enableBrowserIntegration = await this.stateService.getEnableBrowserIntegration(); - this.enableDuckDuckGoBrowserIntegration = - await this.stateService.getEnableDuckDuckGoBrowserIntegration(); - this.enableBrowserIntegrationFingerprint = - await this.stateService.getEnableBrowserIntegrationFingerprint(); - this.clearClipboard = await this.stateService.getClearClipboard(); - this.minimizeOnCopyToClipboard = await this.stateService.getMinimizeOnCopyToClipboard(); + if (this.form.value.enableBrowserIntegration) { + this.form.controls.enableBrowserIntegrationFingerprint.enable(); + } + + // Non-form values + this.showMinToTray = this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop; + this.showAlwaysShowDock = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop; this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); - this.biometric = await this.vaultTimeoutSettingsService.isBiometricLockSet(); this.biometricText = await this.stateService.getBiometricText(); - this.autoPromptBiometrics = !(await this.stateService.getNoAutoPromptBiometrics()); this.autoPromptBiometricsText = await this.stateService.getNoAutoPromptBiometricsText(); + this.previousVaultTimeout = this.form.value.vaultTimeout; + + // Form events + this.form.controls.vaultTimeout.valueChanges + .pipe( + debounceTime(500), + concatMap(async (value) => { + await this.saveVaultTimeout(value); + }), + takeUntil(this.destroy$) + ) + .subscribe(); + + this.form.controls.vaultTimeoutAction.valueChanges + .pipe( + concatMap(async (action) => { + await this.saveVaultTimeoutAction(action); + }), + takeUntil(this.destroy$) + ) + .subscribe(); + + this.form.controls.enableBrowserIntegration.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe((enabled) => { + if (enabled) { + this.form.controls.enableBrowserIntegrationFingerprint.enable(); + } else { + this.form.controls.enableBrowserIntegrationFingerprint.disable(); + } + }); } - async saveVaultTimeoutOptions() { - if (this.vaultTimeout.value == null) { + async saveVaultTimeout(newValue: number) { + if (newValue == null) { const confirmed = await this.platformUtilsService.showDialog( this.i18nService.t("neverLockWarning"), "", @@ -217,31 +290,17 @@ export class SettingsComponent implements OnInit { "warning" ); if (!confirmed) { - this.vaultTimeout.setValue(this.previousVaultTimeout); - return; - } - } - - if (this.vaultTimeoutAction === "logOut") { - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t("vaultTimeoutLogOutConfirmation"), - this.i18nService.t("vaultTimeoutLogOutConfirmationTitle"), - this.i18nService.t("yes"), - this.i18nService.t("cancel"), - "warning" - ); - if (!confirmed) { - this.vaultTimeoutAction = "lock"; + this.form.controls.vaultTimeout.setValue(this.previousVaultTimeout); return; } } // Avoid saving 0 since it's useless as a timeout value. - if (this.vaultTimeout.value === 0) { + if (this.form.value.vaultTimeout === 0) { return; } - if (!this.vaultTimeout.valid) { + if (!this.form.controls.vaultTimeout.valid) { this.platformUtilsService.showToast( "error", null, @@ -250,39 +309,71 @@ export class SettingsComponent implements OnInit { return; } - this.previousVaultTimeout = this.vaultTimeout.value; + this.previousVaultTimeout = this.form.value.vaultTimeout; await this.vaultTimeoutSettingsService.setVaultTimeoutOptions( - this.vaultTimeout.value, - this.vaultTimeoutAction + newValue, + this.form.value.vaultTimeoutAction + ); + } + + async saveVaultTimeoutAction(newValue: VaultTimeoutAction) { + if (newValue === "logOut") { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("vaultTimeoutLogOutConfirmation"), + this.i18nService.t("vaultTimeoutLogOutConfirmationTitle"), + this.i18nService.t("yes"), + this.i18nService.t("cancel"), + "warning" + ); + if (!confirmed) { + this.form.controls.vaultTimeoutAction.patchValue(VaultTimeoutAction.Lock, { + emitEvent: false, + }); + return; + } + } + + if (this.form.controls.vaultTimeout.hasError("policyError")) { + this.platformUtilsService.showToast( + "error", + null, + this.i18nService.t("vaultTimeoutTooLarge") + ); + return; + } + + await this.vaultTimeoutSettingsService.setVaultTimeoutOptions( + this.form.value.vaultTimeout, + newValue ); } async updatePin() { - if (this.pin) { + if (this.form.value.pin) { const ref = this.modalService.open(SetPinComponent, { allowMultipleModals: true }); if (ref == null) { - this.pin = false; + this.form.controls.pin.setValue(false); return; } - this.pin = await ref.onClosedPromise(); + this.form.controls.pin.setValue(await ref.onClosedPromise()); } - if (!this.pin) { + if (!this.form.value.pin) { await this.cryptoService.clearPinProtectedKey(); await this.vaultTimeoutSettingsService.clear(); } } - async updateBiometric(newValue: boolean) { + async updateBiometric() { // NOTE: A bug in angular causes [ngModel] to not reflect the backing field value // causing the checkbox to remain checked even if authentication fails. // The bug should resolve itself once the angular issue is resolved. // See: https://github.com/angular/angular/issues/13063 - if (!newValue || !this.supportsBiometric) { - this.biometric = false; + if (!this.form.value.biometric || !this.supportsBiometric) { + this.form.controls.biometric.setValue(false); await this.stateService.setBiometricUnlock(null); await this.cryptoService.toggleKey(); return; @@ -291,17 +382,17 @@ export class SettingsComponent implements OnInit { const authResult = await this.platformUtilsService.authenticateBiometric(); if (!authResult) { - this.biometric = false; + this.form.controls.biometric.setValue(false); return; } - this.biometric = true; + this.form.controls.biometric.setValue(true); await this.stateService.setBiometricUnlock(true); await this.cryptoService.toggleKey(); } async updateAutoPromptBiometrics() { - if (this.autoPromptBiometrics) { + if (this.form.value.autoPromptBiometrics) { await this.stateService.setNoAutoPromptBiometrics(null); } else { await this.stateService.setNoAutoPromptBiometrics(true); @@ -309,31 +400,31 @@ export class SettingsComponent implements OnInit { } async saveFavicons() { - await this.stateService.setDisableFavicon(!this.enableFavicons); - await this.stateService.setDisableFavicon(!this.enableFavicons, { + await this.stateService.setDisableFavicon(!this.form.value.enableFavicons); + await this.stateService.setDisableFavicon(!this.form.value.enableFavicons, { storageLocation: StorageLocation.Disk, }); this.messagingService.send("refreshCiphers"); } async saveMinToTray() { - await this.stateService.setEnableMinimizeToTray(this.enableMinToTray); + await this.stateService.setEnableMinimizeToTray(this.form.value.enableMinToTray); } async saveCloseToTray() { if (this.requireEnableTray) { - this.enableTray = true; - await this.stateService.setEnableTray(this.enableTray); + this.form.controls.enableTray.setValue(true); + await this.stateService.setEnableTray(this.form.value.enableTray); } - await this.stateService.setEnableCloseToTray(this.enableCloseToTray); + await this.stateService.setEnableCloseToTray(this.form.value.enableCloseToTray); } async saveTray() { if ( this.requireEnableTray && - !this.enableTray && - (this.startToTray || this.enableCloseToTray) + !this.form.value.enableTray && + (this.form.value.startToTray || this.form.value.enableCloseToTray) ) { const confirm = await this.platformUtilsService.showDialog( this.i18nService.t("confirmTrayDesc"), @@ -344,53 +435,55 @@ export class SettingsComponent implements OnInit { ); if (confirm) { - this.startToTray = false; - await this.stateService.setEnableStartToTray(this.startToTray); - this.enableCloseToTray = false; - await this.stateService.setEnableCloseToTray(this.enableCloseToTray); + this.form.controls.startToTray.setValue(false, { emitEvent: false }); + await this.stateService.setEnableStartToTray(this.form.value.startToTray); + this.form.controls.enableCloseToTray.setValue(false, { emitEvent: false }); + await this.stateService.setEnableCloseToTray(this.form.value.enableCloseToTray); } else { - this.enableTray = true; + this.form.controls.enableTray.setValue(true); } return; } - await this.stateService.setEnableTray(this.enableTray); - this.messagingService.send(this.enableTray ? "showTray" : "removeTray"); + await this.stateService.setEnableTray(this.form.value.enableTray); + this.messagingService.send(this.form.value.enableTray ? "showTray" : "removeTray"); } async saveStartToTray() { if (this.requireEnableTray) { - this.enableTray = true; - await this.stateService.setEnableTray(this.enableTray); + this.form.controls.enableTray.setValue(true); + await this.stateService.setEnableTray(this.form.value.enableTray); } - await this.stateService.setEnableStartToTray(this.startToTray); + await this.stateService.setEnableStartToTray(this.form.value.startToTray); } async saveLocale() { - await this.stateService.setLocale(this.locale); + await this.stateService.setLocale(this.form.value.locale); } async saveTheme() { - await this.themingService.updateConfiguredTheme(this.theme); + await this.themingService.updateConfiguredTheme(this.form.value.theme); } async saveMinOnCopyToClipboard() { - await this.stateService.setMinimizeOnCopyToClipboard(this.minimizeOnCopyToClipboard); + await this.stateService.setMinimizeOnCopyToClipboard(this.form.value.minimizeOnCopyToClipboard); } async saveClearClipboard() { - await this.stateService.setClearClipboard(this.clearClipboard); + await this.stateService.setClearClipboard(this.form.value.clearClipboard); } async saveAlwaysShowDock() { - await this.stateService.setAlwaysShowDock(this.alwaysShowDock); + await this.stateService.setAlwaysShowDock(this.form.value.alwaysShowDock); } async saveOpenAtLogin() { - this.stateService.setOpenAtLogin(this.openAtLogin); - this.messagingService.send(this.openAtLogin ? "addOpenAtLogin" : "removeOpenAtLogin"); + this.stateService.setOpenAtLogin(this.form.value.openAtLogin); + this.messagingService.send( + this.form.value.openAtLogin ? "addOpenAtLogin" : "removeOpenAtLogin" + ); } async saveBrowserIntegration() { @@ -403,7 +496,7 @@ export class SettingsComponent implements OnInit { "warning" ); - this.enableBrowserIntegration = false; + this.form.controls.enableBrowserIntegration.setValue(false); return; } else if (isWindowsStore()) { await this.platformUtilsService.showDialog( @@ -414,7 +507,7 @@ export class SettingsComponent implements OnInit { "warning" ); - this.enableBrowserIntegration = false; + this.form.controls.enableBrowserIntegration.setValue(false); return; } else if (process.platform == "linux") { await this.platformUtilsService.showDialog( @@ -425,32 +518,34 @@ export class SettingsComponent implements OnInit { "warning" ); - this.enableBrowserIntegration = false; + this.form.controls.enableBrowserIntegration.setValue(false); return; } - await this.stateService.setEnableBrowserIntegration(this.enableBrowserIntegration); + await this.stateService.setEnableBrowserIntegration(this.form.value.enableBrowserIntegration); this.messagingService.send( - this.enableBrowserIntegration ? "enableBrowserIntegration" : "disableBrowserIntegration" + this.form.value.enableBrowserIntegration + ? "enableBrowserIntegration" + : "disableBrowserIntegration" ); - if (!this.enableBrowserIntegration) { - this.enableBrowserIntegrationFingerprint = false; + if (!this.form.value.enableBrowserIntegration) { + this.form.controls.enableBrowserIntegrationFingerprint.setValue(false); this.saveBrowserIntegrationFingerprint(); } } async saveDdgBrowserIntegration() { await this.stateService.setEnableDuckDuckGoBrowserIntegration( - this.enableDuckDuckGoBrowserIntegration + this.form.value.enableDuckDuckGoBrowserIntegration ); - if (!this.enableBrowserIntegration) { + if (!this.form.value.enableBrowserIntegration) { await this.stateService.setDuckDuckGoSharedKey(null); } this.messagingService.send( - this.enableDuckDuckGoBrowserIntegration + this.form.value.enableDuckDuckGoBrowserIntegration ? "enableDuckDuckGoBrowserIntegration" : "disableDuckDuckGoBrowserIntegration" ); @@ -458,11 +553,16 @@ export class SettingsComponent implements OnInit { async saveBrowserIntegrationFingerprint() { await this.stateService.setEnableBrowserIntegrationFingerprint( - this.enableBrowserIntegrationFingerprint + this.form.value.enableBrowserIntegrationFingerprint ); } async updateApproveLoginRequests() { - await this.stateService.setApproveLoginRequests(this.approveLoginRequests); + await this.stateService.setApproveLoginRequests(this.form.value.approveLoginRequests); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); } } diff --git a/apps/desktop/src/app/accounts/vault-timeout-input.component.html b/apps/desktop/src/app/accounts/vault-timeout-input.component.html index 051efe600b..2bda819859 100644 --- a/apps/desktop/src/app/accounts/vault-timeout-input.component.html +++ b/apps/desktop/src/app/accounts/vault-timeout-input.component.html @@ -1,7 +1,3 @@ - - {{ "vaultTimeoutPolicyInEffect" | i18n : vaultTimeoutPolicyHours : vaultTimeoutPolicyMinutes }} - -
@@ -12,7 +8,7 @@ formControlName="vaultTimeout" class="form-control" > - + {{ "vaultTimeoutDesc" | i18n }}
diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 68eb26c473..d43d0f6687 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1850,7 +1850,7 @@ "message": "Minutes" }, "vaultTimeoutPolicyInEffect": { - "message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s)", + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", "placeholders": { "hours": { "content": "$1", @@ -1862,6 +1862,32 @@ } } }, + "vaultTimeoutPolicyWithActionInEffect": { + "message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + }, + "action": { + "content": "$3", + "example": "Lock" + } + } + }, + "vaultTimeoutActionPolicyInEffect": { + "message": "Your organization policies have set your vault timeout action to $ACTION$.", + "placeholders": { + "action": { + "content": "$1", + "example": "Lock" + } + } + }, "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, diff --git a/apps/web/src/app/settings/preferences.component.html b/apps/web/src/app/settings/preferences.component.html index fc2ede374c..f8a81103cb 100644 --- a/apps/web/src/app/settings/preferences.component.html +++ b/apps/web/src/app/settings/preferences.component.html @@ -2,12 +2,26 @@

{{ "preferences" | i18n }}

{{ "preferencesDesc" | i18n }}

-
+
+ + + {{ + "vaultTimeoutPolicyWithActionInEffect" + | i18n : policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n) + }} + + + {{ "vaultTimeoutPolicyInEffect" | i18n : policy.timeout.hours : policy.timeout.minutes }} + + + {{ "vaultTimeoutActionPolicyInEffect" | i18n : (policy.action | i18n) }} + + @@ -21,8 +35,8 @@ type="radio" name="vaultTimeoutAction" id="vaultTimeoutActionLock" - value="lock" - [(ngModel)]="vaultTimeoutAction" + value="{{ VaultTimeoutAction.Lock }}" + formControlName="vaultTimeoutAction" />
- {{ "languageDesc" | i18n }} @@ -74,7 +87,7 @@ type="checkbox" id="enableFavicons" name="enableFavicons" - [(ngModel)]="enableFavicons" + formControlName="enableFavicons" />
diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/maximum-vault-timeout.component.ts b/bitwarden_license/bit-web/src/app/admin-console/policies/maximum-vault-timeout.component.ts index 7d7ea9bdff..cffed9b585 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/maximum-vault-timeout.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/policies/maximum-vault-timeout.component.ts @@ -1,9 +1,10 @@ import { Component } from "@angular/core"; -import { UntypedFormBuilder } from "@angular/forms"; +import { FormBuilder, FormControl } from "@angular/forms"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request"; +import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { BasePolicy, BasePolicyComponent, @@ -21,25 +22,30 @@ export class MaximumVaultTimeoutPolicy extends BasePolicy { templateUrl: "maximum-vault-timeout.component.html", }) export class MaximumVaultTimeoutPolicyComponent extends BasePolicyComponent { + vaultTimeoutActionOptions: { name: string; value: string }[]; data = this.formBuilder.group({ - hours: [null], - minutes: [null], + hours: new FormControl(null), + minutes: new FormControl(null), + action: new FormControl(null), }); - constructor(private formBuilder: UntypedFormBuilder, private i18nService: I18nService) { + constructor(private formBuilder: FormBuilder, private i18nService: I18nService) { super(); + this.vaultTimeoutActionOptions = [ + { name: i18nService.t("userPreference"), value: null }, + { name: i18nService.t(VaultTimeoutAction.Lock), value: VaultTimeoutAction.Lock }, + { name: i18nService.t(VaultTimeoutAction.LogOut), value: VaultTimeoutAction.LogOut }, + ]; } loadData() { const minutes = this.policyResponse.data?.minutes; - - if (minutes == null) { - return; - } + const action = this.policyResponse.data?.action; this.data.patchValue({ - hours: Math.floor(minutes / 60), - minutes: minutes % 60, + hours: minutes ? Math.floor(minutes / 60) : null, + minutes: minutes ? minutes % 60 : null, + action: action, }); } @@ -50,6 +56,7 @@ export class MaximumVaultTimeoutPolicyComponent extends BasePolicyComponent { return { minutes: this.data.value.hours * 60 + this.data.value.minutes, + action: this.data.value.action, }; } diff --git a/libs/angular/src/components/settings/vault-timeout-input.component.ts b/libs/angular/src/components/settings/vault-timeout-input.component.ts index e0d4a1ef89..8ac3074a4c 100644 --- a/libs/angular/src/components/settings/vault-timeout-input.component.ts +++ b/libs/angular/src/components/settings/vault-timeout-input.component.ts @@ -1,4 +1,4 @@ -import { Directive, Input, OnDestroy, OnInit } from "@angular/core"; +import { Directive, Input, OnChanges, OnDestroy, OnInit } from "@angular/core"; import { AbstractControl, ControlValueAccessor, @@ -6,7 +6,7 @@ import { ValidationErrors, Validator, } from "@angular/forms"; -import { combineLatestWith, Subject, takeUntil } from "rxjs"; +import { filter, Subject, takeUntil } from "rxjs"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -15,7 +15,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; @Directive() export class VaultTimeoutInputComponent - implements ControlValueAccessor, Validator, OnInit, OnDestroy + implements ControlValueAccessor, Validator, OnInit, OnDestroy, OnChanges { get showCustom() { return this.form.get("vaultTimeout").value === VaultTimeoutInputComponent.CUSTOM_VALUE; @@ -38,7 +38,7 @@ export class VaultTimeoutInputComponent }), }); - @Input() vaultTimeouts: { name: string; value: number }[]; + @Input() vaultTimeoutOptions: { name: string; value: number }[]; vaultTimeoutPolicy: Policy; vaultTimeoutPolicyHours: number; vaultTimeoutPolicyMinutes: number; @@ -55,38 +55,37 @@ export class VaultTimeoutInputComponent async ngOnInit() { this.policyService - .policyAppliesToActiveUser$(PolicyType.MaximumVaultTimeout) - .pipe(combineLatestWith(this.policyService.policies$), takeUntil(this.destroy$)) - .subscribe(([policyAppliesToActiveUser, policies]) => { - if (policyAppliesToActiveUser) { - const vaultTimeoutPolicy = policies.find( - (policy) => policy.type === PolicyType.MaximumVaultTimeout && policy.enabled - ); - - this.vaultTimeoutPolicy = vaultTimeoutPolicy; - this.applyVaultTimeoutPolicy(); - } + .get$(PolicyType.MaximumVaultTimeout) + .pipe( + filter((policy) => policy != null), + takeUntil(this.destroy$) + ) + .subscribe((policy) => { + this.vaultTimeoutPolicy = policy; + this.applyVaultTimeoutPolicy(); }); - // eslint-disable-next-line rxjs/no-async-subscribe - this.form.valueChanges.subscribe(async (value) => { - this.onChange(this.getVaultTimeout(value)); + this.form.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => { + if (this.onChange) { + 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, - }, + this.form.controls.vaultTimeout.valueChanges + .pipe( + filter((value) => value !== VaultTimeoutInputComponent.CUSTOM_VALUE), + takeUntil(this.destroy$) + ) + .subscribe((_) => { + const current = Math.max(this.form.value.vaultTimeout, 0); + this.form.patchValue({ + custom: { + hours: Math.floor(current / 60), + minutes: current % 60, + }, + }); }); - }); } ngOnDestroy() { @@ -95,10 +94,14 @@ export class VaultTimeoutInputComponent } ngOnChanges() { - this.vaultTimeouts.push({ - name: this.i18nService.t("custom"), - value: VaultTimeoutInputComponent.CUSTOM_VALUE, - }); + if ( + !this.vaultTimeoutOptions.find((p) => p.value === VaultTimeoutInputComponent.CUSTOM_VALUE) + ) { + this.vaultTimeoutOptions.push({ + name: this.i18nService.t("custom"), + value: VaultTimeoutInputComponent.CUSTOM_VALUE, + }); + } } getVaultTimeout(value: any) { @@ -114,7 +117,7 @@ export class VaultTimeoutInputComponent return; } - if (this.vaultTimeouts.every((p) => p.value !== value)) { + if (this.vaultTimeoutOptions.every((p) => p.value !== value)) { this.form.setValue({ vaultTimeout: VaultTimeoutInputComponent.CUSTOM_VALUE, custom: { @@ -166,7 +169,7 @@ export class VaultTimeoutInputComponent this.vaultTimeoutPolicyHours = Math.floor(this.vaultTimeoutPolicy.data.minutes / 60); this.vaultTimeoutPolicyMinutes = this.vaultTimeoutPolicy.data.minutes % 60; - this.vaultTimeouts = this.vaultTimeouts.filter( + this.vaultTimeoutOptions = this.vaultTimeoutOptions.filter( (t) => t.value <= this.vaultTimeoutPolicy.data.minutes && (t.value > 0 || t.value === VaultTimeoutInputComponent.CUSTOM_VALUE) && diff --git a/libs/common/src/abstractions/vaultTimeout/vaultTimeoutSettings.service.ts b/libs/common/src/abstractions/vaultTimeout/vaultTimeoutSettings.service.ts index 7e819df6b9..03f89b0476 100644 --- a/libs/common/src/abstractions/vaultTimeout/vaultTimeoutSettings.service.ts +++ b/libs/common/src/abstractions/vaultTimeout/vaultTimeoutSettings.service.ts @@ -1,6 +1,12 @@ +import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; + export abstract class VaultTimeoutSettingsService { - setVaultTimeoutOptions: (vaultTimeout: number, vaultTimeoutAction: string) => Promise; + setVaultTimeoutOptions: ( + vaultTimeout: number, + vaultTimeoutAction: VaultTimeoutAction + ) => Promise; getVaultTimeout: (userId?: string) => Promise; + getVaultTimeoutAction: (userId?: string) => Promise; isPinLockSet: () => Promise<[boolean, boolean]>; isBiometricLockSet: () => Promise; clear: (userId?: string) => Promise; diff --git a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts index a619091a7f..e43b124dcd 100644 --- a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts @@ -10,6 +10,7 @@ import { PolicyResponse } from "../../models/response/policy.response"; export abstract class PolicyService { policies$: Observable; + get$: (policyType: PolicyType, policyFilter?: (policy: Policy) => boolean) => Observable; masterPasswordPolicyOptions$: (policies?: Policy[]) => Observable; policyAppliesToActiveUser$: ( policyType: PolicyType, diff --git a/libs/common/src/admin-console/services/policy/policy.service.ts b/libs/common/src/admin-console/services/policy/policy.service.ts index 34389d8817..8fa0d72298 100644 --- a/libs/common/src/admin-console/services/policy/policy.service.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.ts @@ -42,6 +42,28 @@ export class PolicyService implements InternalPolicyServiceAbstraction { .subscribe(); } + /** + * Returns the first policy found that applies to the active user + * @param policyType Policy type to search for + * @param policyFilter Additional filter to apply to the policy + */ + get$(policyType: PolicyType, policyFilter?: (policy: Policy) => boolean): Observable { + return this.policies$.pipe( + concatMap(async (policies) => { + const userId = await this.stateService.getUserId(); + const appliesToCurrentUser = await this.checkPoliciesThatApplyToUser( + policies, + policyType, + policyFilter, + userId + ); + if (appliesToCurrentUser) { + return policies.find((policy) => policy.type === policyType && policy.enabled); + } + }) + ); + } + /** * @deprecated Do not call this, use the policies$ observable collection */ diff --git a/libs/common/src/enums/vault-timeout-action.enum.ts b/libs/common/src/enums/vault-timeout-action.enum.ts new file mode 100644 index 0000000000..239a749019 --- /dev/null +++ b/libs/common/src/enums/vault-timeout-action.enum.ts @@ -0,0 +1,4 @@ +export enum VaultTimeoutAction { + Lock = "lock", + LogOut = "logOut", +} diff --git a/libs/common/src/services/state.service.ts b/libs/common/src/services/state.service.ts index 20663d2d81..049eabaa3c 100644 --- a/libs/common/src/services/state.service.ts +++ b/libs/common/src/services/state.service.ts @@ -18,6 +18,7 @@ import { CollectionView } from "../admin-console/models/view/collection.view"; import { EnvironmentUrls } from "../auth/models/domain/environment-urls"; import { KdfConfig } from "../auth/models/domain/kdf-config"; import { HtmlStorageLocation, KdfType, StorageLocation, ThemeType, UriMatchType } from "../enums"; +import { VaultTimeoutAction } from "../enums/vault-timeout-action.enum"; import { StateFactory } from "../factories/stateFactory"; import { Utils } from "../misc/utils"; import { EventData } from "../models/data/event.data"; @@ -2571,7 +2572,10 @@ export class StateService< await this.storageService.remove(keys.tempAccountSettings); } account.settings.environmentUrls = environmentUrls; - if (account.settings.vaultTimeoutAction === "logOut" && account.settings.vaultTimeout != null) { + if ( + account.settings.vaultTimeoutAction === VaultTimeoutAction.LogOut && + account.settings.vaultTimeout != null + ) { account.tokens.accessToken = null; account.tokens.refreshToken = null; account.profile.apiKeyClientId = null; @@ -2831,7 +2835,7 @@ export class StateService< const timeoutAction = await this.getVaultTimeoutAction({ userId: options?.userId }); const timeout = await this.getVaultTimeout({ userId: options?.userId }); const defaultOptions = - timeoutAction === "logOut" && timeout != null + timeoutAction === VaultTimeoutAction.LogOut && timeout != null ? await this.defaultInMemoryOptions() : await this.defaultOnDiskOptions(); return this.reconcileOptions(options, defaultOptions); diff --git a/libs/common/src/services/vaultTimeout/vaultTimeout.service.ts b/libs/common/src/services/vaultTimeout/vaultTimeout.service.ts index f1744cf9c7..313cd38566 100644 --- a/libs/common/src/services/vaultTimeout/vaultTimeout.service.ts +++ b/libs/common/src/services/vaultTimeout/vaultTimeout.service.ts @@ -11,6 +11,7 @@ import { CollectionService } from "../../admin-console/abstractions/collection.s import { AuthService } from "../../auth/abstractions/auth.service"; import { KeyConnectorService } from "../../auth/abstractions/key-connector.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; +import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { CipherService } from "../../vault/abstractions/cipher.service"; import { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction"; @@ -132,7 +133,9 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { } private async executeTimeoutAction(userId: string): Promise { - const timeoutAction = await this.stateService.getVaultTimeoutAction({ userId: userId }); - timeoutAction === "logOut" ? await this.logOut(userId) : await this.lock(userId); + const timeoutAction = await this.vaultTimeoutSettingsService.getVaultTimeoutAction(userId); + timeoutAction === VaultTimeoutAction.LogOut + ? await this.logOut(userId) + : await this.lock(userId); } } diff --git a/libs/common/src/services/vaultTimeout/vaultTimeoutSettings.service.ts b/libs/common/src/services/vaultTimeout/vaultTimeoutSettings.service.ts index 83187c4ba5..09bd311f93 100644 --- a/libs/common/src/services/vaultTimeout/vaultTimeoutSettings.service.ts +++ b/libs/common/src/services/vaultTimeout/vaultTimeoutSettings.service.ts @@ -4,6 +4,7 @@ import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "../../admin-console/enums"; import { TokenService } from "../../auth/abstractions/token.service"; +import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceAbstraction { constructor( @@ -13,7 +14,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA private stateService: StateService ) {} - async setVaultTimeoutOptions(timeout: number, action: string): Promise { + async setVaultTimeoutOptions(timeout: number, action: VaultTimeoutAction): Promise { await this.stateService.setVaultTimeout(timeout); // We swap these tokens from being on disk for lock actions, and in memory for logout actions @@ -24,7 +25,11 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA const clientSecret = await this.tokenService.getClientSecret(); const currentAction = await this.stateService.getVaultTimeoutAction(); - if ((timeout != null || timeout === 0) && action === "logOut" && action !== currentAction) { + if ( + (timeout != null || timeout === 0) && + action === VaultTimeoutAction.LogOut && + action !== currentAction + ) { // if we have a vault timeout and the action is log out, reset tokens await this.tokenService.clearToken(); } @@ -74,6 +79,29 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA return vaultTimeout; } + async getVaultTimeoutAction(userId?: string): Promise { + let vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId: userId }); + + if ( + await this.policyService.policyAppliesToUser(PolicyType.MaximumVaultTimeout, null, userId) + ) { + const policy = await this.policyService.getAll(PolicyType.MaximumVaultTimeout, userId); + const action = policy[0].data.action; + + if (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 }); + } + vaultTimeoutAction = action; + } + } + + return vaultTimeoutAction === VaultTimeoutAction.LogOut + ? VaultTimeoutAction.LogOut + : VaultTimeoutAction.Lock; + } + async clear(userId?: string): Promise { await this.stateService.setEverBeenUnlocked(false, { userId: userId }); await this.stateService.setDecryptedPinProtected(null, { userId: userId });