[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
This commit is contained in:
Jake Fink 2023-04-14 15:39:57 -04:00 committed by GitHub
parent cc75cebdb8
commit a72f267558
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 144 additions and 76 deletions

View file

@ -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<KeyValuePair<string, int?>> _vaultTimeouts =
private readonly static List<KeyValuePair<string, int?>> VaultTimeoutOptions =
new List<KeyValuePair<string, int?>>
{
new KeyValuePair<string, int?>(AppResources.Immediately, 0),
@ -65,7 +62,7 @@ namespace Bit.App.Pages
new KeyValuePair<string, int?>(AppResources.Never, null),
new KeyValuePair<string, int?>(AppResources.Custom, CustomVaultTimeoutValue),
};
private List<KeyValuePair<string, VaultTimeoutAction>> _vaultTimeoutActions =
private readonly static List<KeyValuePair<string, VaultTimeoutAction>> VaultTimeoutActionOptions =
new List<KeyValuePair<string, VaultTimeoutAction>>
{
new KeyValuePair<string, VaultTimeoutAction>(AppResources.Lock, VaultTimeoutAction.Lock),
@ -74,6 +71,8 @@ namespace Bit.App.Pages
private Policy _vaultTimeoutPolicy;
private int? _vaultTimeout;
private List<KeyValuePair<string, int?>> _vaultTimeoutOptions = VaultTimeoutOptions;
private List<KeyValuePair<string, VaultTimeoutAction>> _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;

View file

@ -6677,6 +6677,15 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Your organization policies have set your vault timeout action to {0}..
/// </summary>
public static string VaultTimeoutActionPolicyInEffect {
get {
return ResourceManager.GetString("VaultTimeoutActionPolicyInEffect", resourceCulture);
}
}
/// <summary>
/// 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?.
/// </summary>
@ -6687,7 +6696,7 @@ namespace Bit.App.Resources {
}
/// <summary>
/// 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)..
/// </summary>
public static string VaultTimeoutPolicyInEffect {
get {
@ -6695,6 +6704,15 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// 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}..
/// </summary>
public static string VaultTimeoutPolicyWithActionInEffect {
get {
return ResourceManager.GetString("VaultTimeoutPolicyWithActionInEffect", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Your vault timeout exceeds the restrictions set by your organization..
/// </summary>

View file

@ -2141,7 +2141,13 @@ Scanning will happen automatically.</value>
<value>This organization has an enterprise policy that will automatically enroll you in password reset. Enrollment will allow organization administrators to change your master password.</value>
</data>
<data name="VaultTimeoutPolicyInEffect" xml:space="preserve">
<value>Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is {0} hour(s) and {1} minute(s)</value>
<value>Your organization policies have set your maximum allowed vault timeout to {0} hour(s) and {1} minute(s).</value>
</data>
<data name="VaultTimeoutPolicyWithActionInEffect" xml:space="preserve">
<value>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}.</value>
</data>
<data name="VaultTimeoutActionPolicyInEffect" xml:space="preserve">
<value>Your organization policies have set your vault timeout action to {0}.</value>
</data>
<data name="VaultTimeoutToLarge" xml:space="preserve">
<value>Your vault timeout exceeds the restrictions set by your organization.</value>

View file

@ -20,6 +20,7 @@ namespace Bit.Core.Abstractions
string orgId);
Task<bool> PolicyAppliesToUser(PolicyType policyType, Func<Policy, bool> policyFilter = null, string userId = null);
int? GetPolicyInt(Policy policy, string key);
string GetPolicyString(Policy policy, string key);
Task<bool> ShouldShowVaultFilterAsync();
}
}

View file

@ -22,5 +22,6 @@ namespace Bit.Core.Abstractions
Task LogOutAsync(bool userInitiated = true, string userId = null);
Task SetVaultTimeoutOptionsAsync(int? timeout, VaultTimeoutAction? action);
Task<int?> GetVaultTimeout(string userId = null);
Task<VaultTimeoutAction?> GetVaultTimeoutAction(string userId = null);
}
}

View file

@ -17,6 +17,11 @@ namespace Bit.Core.Services
private IEnumerable<Policy> _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<MasterPasswordPolicyOptions> GetMasterPasswordPolicyOptions(
IEnumerable<Policy> 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<bool> 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;
}
}
}

View file

@ -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<PolicyResponse> response)
private async Task SyncPoliciesAsync(string userId, List<PolicyResponse> response)
{
var policies = response?.ToDictionary(p => p.Id, p => new PolicyData(p)) ??
new Dictionary<string, PolicyData>();
await _policyService.Replace(policies);
await _policyService.Replace(policies, userId);
}
private async Task SyncSendsAsync(string userId, List<SendResponse> response)

View file

@ -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<Tuple<string, bool>, Task> _lockedCallback;
private readonly Func<Tuple<string, bool, bool>, Task> _loggedOutCallback;
@ -32,7 +31,6 @@ namespace Bit.Core.Services
ISearchService searchService,
IMessagingService messagingService,
ITokenService tokenService,
IPolicyService policyService,
IKeyConnectorService keyConnectorService,
Func<Tuple<string, bool>, Task> lockedCallback,
Func<Tuple<string, bool, bool>, 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<int?> 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<VaultTimeoutAction?> GetVaultTimeoutAction(string userId = null)
{
return await _stateService.GetVaultTimeoutActionAsync(userId);
}
}
}

View file

@ -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);