From a72f2675582694995a40a4b706f78369309e521d Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Fri, 14 Apr 2023 15:39:57 -0400 Subject: [PATCH] [AC-1045] vault timeout action policy (#2415) * [EC-1045] lock action if policy and show message * [EC-1045] add text for policy message * [EC-1045] add consts to policy service * [EC-1045] missed a const * [AC-1045] fix build * [AC-1045] fix bug where UI wasn't updating after sync * [AC-1045] change FirstOrDefault to First to avoid nulls * [AC-1045] refactor get vault timeout functions * [AC-1045] don't filter action options unecessarily * [AC-1045] refactor build alert logic for readability * [AC-1045] use policy to filter timeout options instead of current timeout * [AC-1045] update timeout during sync instead of getter - remove encrypted from state since it's not encrypted - if policies return a timeout policy, check and update vault timeout * [AC-1045] default to custom if we can't find vault timeout option * [AC-1045] revert Encrypted Policies rename --- .../SettingsPage/SettingsPageViewModel.cs | 89 +++++++++++++------ src/App/Resources/AppResources.Designer.cs | 20 ++++- src/App/Resources/AppResources.resx | 8 +- src/Core/Abstractions/IPolicyService.cs | 1 + src/Core/Abstractions/IVaultTimeoutService.cs | 1 + src/Core/Services/PolicyService.cs | 57 +++++++++--- src/Core/Services/SyncService.cs | 6 +- src/Core/Services/VaultTimeoutService.cs | 36 ++------ src/Core/Utilities/ServiceContainer.cs | 2 +- 9 files changed, 144 insertions(+), 76 deletions(-) diff --git a/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs b/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs index 2845401bf..d71bef46e 100644 --- a/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs +++ b/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs @@ -7,10 +7,7 @@ using Bit.App.Pages.Accounts; using Bit.App.Resources; using Bit.Core.Abstractions; using Bit.Core.Enums; -using Bit.Core.Models; using Bit.Core.Models.Domain; -using Bit.Core.Models.Response; -using Bit.Core.Models.View; using Bit.Core.Services; using Bit.Core.Utilities; using Xamarin.CommunityToolkit.ObjectModel; @@ -51,7 +48,7 @@ namespace Bit.App.Pages private bool _reportLoggingEnabled; private bool _approvePasswordlessLoginRequests; private bool _shouldConnectToWatch; - private List> _vaultTimeouts = + private readonly static List> VaultTimeoutOptions = new List> { new KeyValuePair(AppResources.Immediately, 0), @@ -65,7 +62,7 @@ namespace Bit.App.Pages new KeyValuePair(AppResources.Never, null), new KeyValuePair(AppResources.Custom, CustomVaultTimeoutValue), }; - private List> _vaultTimeoutActions = + private readonly static List> VaultTimeoutActionOptions = new List> { new KeyValuePair(AppResources.Lock, VaultTimeoutAction.Lock), @@ -74,6 +71,8 @@ namespace Bit.App.Pages private Policy _vaultTimeoutPolicy; private int? _vaultTimeout; + private List> _vaultTimeoutOptions = VaultTimeoutOptions; + private List> _vaultTimeoutActionOptions = VaultTimeoutActionOptions; public SettingsPageViewModel() { @@ -117,20 +116,28 @@ namespace Bit.App.Pages _localizeService.GetLocaleShortTime(lastSync.Value)); } + _vaultTimeoutPolicy = null; + _vaultTimeoutOptions = VaultTimeoutOptions; + _vaultTimeoutActionOptions = VaultTimeoutActionOptions; + + _vaultTimeout = await _vaultTimeoutService.GetVaultTimeout(); + _vaultTimeoutDisplayValue = _vaultTimeoutOptions.FirstOrDefault(o => o.Value == _vaultTimeout).Key; + _vaultTimeoutDisplayValue ??= _vaultTimeoutOptions.Where(o => o.Value == CustomVaultTimeoutValue).First().Key; + + var action = await _vaultTimeoutService.GetVaultTimeoutAction() ?? VaultTimeoutAction.Lock; + _vaultTimeoutActionDisplayValue = _vaultTimeoutActionOptions.FirstOrDefault(o => o.Value == action).Key; + if (await _policyService.PolicyAppliesToUser(PolicyType.MaximumVaultTimeout)) { + // if we have a vault timeout policy, we need to filter the timeout options _vaultTimeoutPolicy = (await _policyService.GetAll(PolicyType.MaximumVaultTimeout)).First(); - var minutes = _policyService.GetPolicyInt(_vaultTimeoutPolicy, "minutes").GetValueOrDefault(); - _vaultTimeouts = _vaultTimeouts.Where(t => - t.Value <= minutes && + var policyMinutes = _policyService.GetPolicyInt(_vaultTimeoutPolicy, PolicyService.TIMEOUT_POLICY_MINUTES); + _vaultTimeoutOptions = _vaultTimeoutOptions.Where(t => + t.Value <= policyMinutes && (t.Value > 0 || t.Value == CustomVaultTimeoutValue) && t.Value != null).ToList(); } - _vaultTimeout = await _vaultTimeoutService.GetVaultTimeout(); - _vaultTimeoutDisplayValue = _vaultTimeouts.FirstOrDefault(o => o.Value == _vaultTimeout).Key; - var action = await _stateService.GetVaultTimeoutActionAsync() ?? VaultTimeoutAction.Lock; - _vaultTimeoutActionDisplayValue = _vaultTimeoutActions.FirstOrDefault(o => o.Value == action).Key; var pinSet = await _vaultTimeoutService.IsPinLockSetAsync(); _pin = pinSet.Item1 || pinSet.Item2; _biometric = await _vaultTimeoutService.IsBiometricLockSetAsync(); @@ -266,7 +273,7 @@ namespace Bit.App.Pages { var oldTimeout = _vaultTimeout; - var options = _vaultTimeouts.Select( + var options = _vaultTimeoutOptions.Select( o => o.Key == _vaultTimeoutDisplayValue ? $"✓ {o.Key}" : o.Key).ToArray(); if (promptOptions) { @@ -277,7 +284,7 @@ namespace Bit.App.Pages return; } var cleanSelection = selection.Replace("✓ ", string.Empty); - var selectionOption = _vaultTimeouts.FirstOrDefault(o => o.Key == cleanSelection); + var selectionOption = _vaultTimeoutOptions.FirstOrDefault(o => o.Key == cleanSelection); // Check if the selected Timeout action is "Never" and if it's different from the previous selected value if (selectionOption.Value == null && selectionOption.Value != oldTimeout) @@ -295,13 +302,13 @@ namespace Bit.App.Pages if (_vaultTimeoutPolicy != null) { - var maximumTimeout = _policyService.GetPolicyInt(_vaultTimeoutPolicy, "minutes"); + var maximumTimeout = _policyService.GetPolicyInt(_vaultTimeoutPolicy, PolicyService.TIMEOUT_POLICY_MINUTES); if (newTimeout > maximumTimeout) { await _platformUtilsService.ShowDialogAsync(AppResources.VaultTimeoutToLarge, AppResources.Warning); var timeout = await _vaultTimeoutService.GetVaultTimeout(); - _vaultTimeoutDisplayValue = _vaultTimeouts.FirstOrDefault(o => o.Value == timeout).Key ?? + _vaultTimeoutDisplayValue = _vaultTimeoutOptions.FirstOrDefault(o => o.Value == timeout).Key ?? AppResources.Custom; return; } @@ -374,7 +381,13 @@ namespace Bit.App.Pages public async Task VaultTimeoutActionAsync() { - var options = _vaultTimeoutActions.Select(o => + if (_vaultTimeoutPolicy != null && + !string.IsNullOrEmpty(_policyService.GetPolicyString(_vaultTimeoutPolicy, PolicyService.TIMEOUT_POLICY_ACTION))) + { + // do nothing if we have a policy set + return; + } + var options = _vaultTimeoutActionOptions.Select(o => o.Key == _vaultTimeoutActionDisplayValue ? $"✓ {o.Key}" : o.Key).ToArray(); var selection = await Page.DisplayActionSheet(AppResources.VaultTimeoutAction, AppResources.Cancel, null, options); @@ -393,7 +406,7 @@ namespace Bit.App.Pages cleanSelection = AppResources.Lock; } } - var selectionOption = _vaultTimeoutActions.FirstOrDefault(o => o.Key == cleanSelection); + var selectionOption = _vaultTimeoutActionOptions.FirstOrDefault(o => o.Key == cleanSelection); var changed = _vaultTimeoutActionDisplayValue != selectionOption.Key; _vaultTimeoutActionDisplayValue = selectionOption.Key; await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(_vaultTimeout, @@ -597,14 +610,36 @@ namespace Bit.App.Pages } if (_vaultTimeoutPolicy != null) { - var maximumTimeout = _policyService.GetPolicyInt(_vaultTimeoutPolicy, "minutes").GetValueOrDefault(); - securityItems.Insert(0, new SettingsPageListItem + var policyMinutes = _policyService.GetPolicyInt(_vaultTimeoutPolicy, PolicyService.TIMEOUT_POLICY_MINUTES); + var policyAction = _policyService.GetPolicyString(_vaultTimeoutPolicy, PolicyService.TIMEOUT_POLICY_ACTION); + + if (policyMinutes.HasValue || !string.IsNullOrWhiteSpace(policyAction)) { - Name = string.Format(AppResources.VaultTimeoutPolicyInEffect, - Math.Floor((float)maximumTimeout / 60), - maximumTimeout % 60), - UseFrame = true, - }); + string policyAlert; + if (policyMinutes.HasValue && string.IsNullOrWhiteSpace(policyAction)) + { + policyAlert = string.Format(AppResources.VaultTimeoutPolicyInEffect, + Math.Floor((float)policyMinutes / 60), + policyMinutes % 60); + } + else if (!policyMinutes.HasValue && !string.IsNullOrWhiteSpace(policyAction)) + { + policyAlert = string.Format(AppResources.VaultTimeoutActionPolicyInEffect, + policyAction == PolicyService.TIMEOUT_POLICY_ACTION_LOCK ? AppResources.Lock : AppResources.LogOut); + } + else + { + policyAlert = string.Format(AppResources.VaultTimeoutPolicyWithActionInEffect, + Math.Floor((float)policyMinutes / 60), + policyMinutes % 60, + policyAction == PolicyService.TIMEOUT_POLICY_ACTION_LOCK ? AppResources.Lock : AppResources.LogOut); + } + securityItems.Insert(0, new SettingsPageListItem + { + Name = policyAlert, + UseFrame = true, + }); + } } if (Device.RuntimePlatform == Device.Android) { @@ -792,12 +827,12 @@ namespace Bit.App.Pages private VaultTimeoutAction GetVaultTimeoutActionFromKey(string key) { - return _vaultTimeoutActions.FirstOrDefault(o => o.Key == key).Value; + return _vaultTimeoutActionOptions.FirstOrDefault(o => o.Key == key).Value; } private int? GetVaultTimeoutFromKey(string key) { - return _vaultTimeouts.FirstOrDefault(o => o.Key == key).Value; + return _vaultTimeoutOptions.FirstOrDefault(o => o.Key == key).Value; } private string CreateSelectableOption(string option, bool selected) => selected ? $"✓ {option}" : option; diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index 16763104f..243501dac 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -6677,6 +6677,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Your organization policies have set your vault timeout action to {0}.. + /// + public static string VaultTimeoutActionPolicyInEffect { + get { + return ResourceManager.GetString("VaultTimeoutActionPolicyInEffect", resourceCulture); + } + } + /// /// Looks up a localized string similar to Logging out will remove all access to your vault and requires online authentication after the timeout period. Are you sure you want to use this setting?. /// @@ -6687,7 +6696,7 @@ namespace Bit.App.Resources { } /// - /// Looks up a localized string similar to Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is {0} hour(s) and {1} minute(s). + /// Looks up a localized string similar to Your organization policies have set your maximum allowed vault timeout to {0} hour(s) and {1} minute(s).. /// public static string VaultTimeoutPolicyInEffect { get { @@ -6695,6 +6704,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is {0} hour(s) and {1} minute(s). Your vault timeout action is set to {2}.. + /// + public static string VaultTimeoutPolicyWithActionInEffect { + get { + return ResourceManager.GetString("VaultTimeoutPolicyWithActionInEffect", resourceCulture); + } + } + /// /// Looks up a localized string similar to Your vault timeout exceeds the restrictions set by your organization.. /// diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 61742c5f6..d7b15ae3f 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -2141,7 +2141,13 @@ Scanning will happen automatically. This organization has an enterprise policy that will automatically enroll you in password reset. Enrollment will allow organization administrators to change your master password. - Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is {0} hour(s) and {1} minute(s) + Your organization policies have set your maximum allowed vault timeout to {0} hour(s) and {1} minute(s). + + + Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is {0} hour(s) and {1} minute(s). Your vault timeout action is set to {2}. + + + Your organization policies have set your vault timeout action to {0}. Your vault timeout exceeds the restrictions set by your organization. diff --git a/src/Core/Abstractions/IPolicyService.cs b/src/Core/Abstractions/IPolicyService.cs index caabeca3d..8acca2c9a 100644 --- a/src/Core/Abstractions/IPolicyService.cs +++ b/src/Core/Abstractions/IPolicyService.cs @@ -20,6 +20,7 @@ namespace Bit.Core.Abstractions string orgId); Task PolicyAppliesToUser(PolicyType policyType, Func policyFilter = null, string userId = null); int? GetPolicyInt(Policy policy, string key); + string GetPolicyString(Policy policy, string key); Task ShouldShowVaultFilterAsync(); } } diff --git a/src/Core/Abstractions/IVaultTimeoutService.cs b/src/Core/Abstractions/IVaultTimeoutService.cs index ed71b7f41..c74001b17 100644 --- a/src/Core/Abstractions/IVaultTimeoutService.cs +++ b/src/Core/Abstractions/IVaultTimeoutService.cs @@ -22,5 +22,6 @@ namespace Bit.Core.Abstractions Task LogOutAsync(bool userInitiated = true, string userId = null); Task SetVaultTimeoutOptionsAsync(int? timeout, VaultTimeoutAction? action); Task GetVaultTimeout(string userId = null); + Task GetVaultTimeoutAction(string userId = null); } } diff --git a/src/Core/Services/PolicyService.cs b/src/Core/Services/PolicyService.cs index c7b374643..83de663cc 100644 --- a/src/Core/Services/PolicyService.cs +++ b/src/Core/Services/PolicyService.cs @@ -17,6 +17,11 @@ namespace Bit.Core.Services private IEnumerable _policyCache; + public const string TIMEOUT_POLICY_MINUTES = "minutes"; + public const string TIMEOUT_POLICY_ACTION = "action"; + public const string TIMEOUT_POLICY_ACTION_LOCK = "lock"; + public const string TIMEOUT_POLICY_ACTION_LOGOUT = "logOut"; + public PolicyService( IStateService stateService, IOrganizationService organizationService) @@ -56,6 +61,12 @@ namespace Bit.Core.Services { await _stateService.SetEncryptedPoliciesAsync(policies, userId); _policyCache = null; + + var vaultTimeoutPolicy = policies.FirstOrDefault(p => p.Value.Type == PolicyType.MaximumVaultTimeout); + if (!vaultTimeoutPolicy.Equals(default)) + { + await UpdateVaultTimeoutFromPolicyAsync(new Policy(vaultTimeoutPolicy.Value)); + } } public async Task ClearAsync(string userId) @@ -64,6 +75,35 @@ namespace Bit.Core.Services _policyCache = null; } + public async Task UpdateVaultTimeoutFromPolicyAsync(Policy policy, string userId = null) + { + var policyTimeout = GetPolicyInt(policy, PolicyService.TIMEOUT_POLICY_MINUTES); + if (policyTimeout != null) + { + var vaultTimeout = await _stateService.GetVaultTimeoutAsync(userId); + var timeout = vaultTimeout.HasValue ? Math.Min(vaultTimeout.Value, policyTimeout.Value) : policyTimeout.Value; + if (timeout < 0) + { + timeout = policyTimeout.Value; + } + if (vaultTimeout != timeout) + { + await _stateService.SetVaultTimeoutAsync(timeout, userId); + } + } + + var policyAction = GetPolicyString(policy, PolicyService.TIMEOUT_POLICY_ACTION); + if (!string.IsNullOrEmpty(policyAction)) + { + var vaultTimeoutAction = await _stateService.GetVaultTimeoutActionAsync(userId); + var action = policyAction == PolicyService.TIMEOUT_POLICY_ACTION_LOCK ? VaultTimeoutAction.Lock : VaultTimeoutAction.Logout; + if (vaultTimeoutAction != action) + { + await _stateService.SetVaultTimeoutActionAsync(action, userId); + } + } + } + public async Task GetMasterPasswordPolicyOptions( IEnumerable policies = null, string userId = null) { @@ -247,6 +287,10 @@ namespace Bit.Core.Services return null; } + public string GetPolicyString(Policy policy, string key) => + policy.Data.TryGetValue(key, out var val) ? val as string : null; + + public async Task ShouldShowVaultFilterAsync() { var personalOwnershipPolicyApplies = await PolicyAppliesToUser(PolicyType.PersonalOwnership); @@ -272,17 +316,6 @@ namespace Bit.Core.Services return null; } - private string GetPolicyString(Policy policy, string key) - { - if (policy.Data.ContainsKey(key)) - { - var value = policy.Data[key]; - if (value != null) - { - return (string)value; - } - } - return null; - } + } } diff --git a/src/Core/Services/SyncService.cs b/src/Core/Services/SyncService.cs index 595eb11c9..4351e0f08 100644 --- a/src/Core/Services/SyncService.cs +++ b/src/Core/Services/SyncService.cs @@ -111,7 +111,7 @@ namespace Bit.Core.Services await SyncCollectionsAsync(response.Collections); await SyncCiphersAsync(userId, response.Ciphers); await SyncSettingsAsync(userId, response.Domains); - await SyncPoliciesAsync(response.Policies); + await SyncPoliciesAsync(userId, response.Policies); await SyncSendsAsync(userId, response.Sends); await SetLastSyncAsync(now); _watchDeviceService.Value.SyncDataToWatchAsync().FireAndForget(); @@ -378,11 +378,11 @@ namespace Bit.Core.Services await _settingsService.SetEquivalentDomainsAsync(eqDomains); } - private async Task SyncPoliciesAsync(List response) + private async Task SyncPoliciesAsync(string userId, List response) { var policies = response?.ToDictionary(p => p.Id, p => new PolicyData(p)) ?? new Dictionary(); - await _policyService.Replace(policies); + await _policyService.Replace(policies, userId); } private async Task SyncSendsAsync(string userId, List response) diff --git a/src/Core/Services/VaultTimeoutService.cs b/src/Core/Services/VaultTimeoutService.cs index 43adc7866..eb800c984 100644 --- a/src/Core/Services/VaultTimeoutService.cs +++ b/src/Core/Services/VaultTimeoutService.cs @@ -17,7 +17,6 @@ namespace Bit.Core.Services private readonly ISearchService _searchService; private readonly IMessagingService _messagingService; private readonly ITokenService _tokenService; - private readonly IPolicyService _policyService; private readonly IKeyConnectorService _keyConnectorService; private readonly Func, Task> _lockedCallback; private readonly Func, Task> _loggedOutCallback; @@ -32,7 +31,6 @@ namespace Bit.Core.Services ISearchService searchService, IMessagingService messagingService, ITokenService tokenService, - IPolicyService policyService, IKeyConnectorService keyConnectorService, Func, Task> lockedCallback, Func, Task> loggedOutCallback) @@ -46,7 +44,6 @@ namespace Bit.Core.Services _searchService = searchService; _messagingService = messagingService; _tokenService = tokenService; - _policyService = policyService; _keyConnectorService = keyConnectorService; _lockedCallback = lockedCallback; _loggedOutCallback = loggedOutCallback; @@ -241,35 +238,12 @@ namespace Bit.Core.Services public async Task GetVaultTimeout(string userId = null) { - var vaultTimeout = await _stateService.GetVaultTimeoutAsync(userId); + return await _stateService.GetVaultTimeoutAsync(userId); + } - if (await _policyService.PolicyAppliesToUser(PolicyType.MaximumVaultTimeout, null, userId)) - { - var policy = (await _policyService.GetAll(PolicyType.MaximumVaultTimeout, userId)).First(); - // Remove negative values, and ensure it's smaller than maximum allowed value according to policy - var policyTimeout = _policyService.GetPolicyInt(policy, "minutes"); - if (!policyTimeout.HasValue) - { - return vaultTimeout; - } - - var timeout = vaultTimeout.HasValue ? Math.Min(vaultTimeout.Value, policyTimeout.Value) : policyTimeout.Value; - - if (timeout < 0) - { - timeout = policyTimeout.Value; - } - - // We really shouldn't need to set the value here, but multiple services relies on this value being correct. - if (vaultTimeout != timeout) - { - await _stateService.SetVaultTimeoutAsync(timeout, userId); - } - - return timeout; - } - - return vaultTimeout; + public async Task GetVaultTimeoutAction(string userId = null) + { + return await _stateService.GetVaultTimeoutActionAsync(userId); } } } diff --git a/src/Core/Utilities/ServiceContainer.cs b/src/Core/Utilities/ServiceContainer.cs index ed60f9c99..7e2159762 100644 --- a/src/Core/Utilities/ServiceContainer.cs +++ b/src/Core/Utilities/ServiceContainer.cs @@ -57,7 +57,7 @@ namespace Bit.Core.Utilities organizationService); var vaultTimeoutService = new VaultTimeoutService(cryptoService, stateService, platformUtilsService, folderService, cipherService, collectionService, searchService, messagingService, tokenService, - policyService, keyConnectorService, + keyConnectorService, (extras) => { messagingService.Send("locked", extras);