mirror of
https://github.com/bitwarden/server
synced 2025-12-11 13:53:40 +00:00
[PM-18239] Master password policy requirement (#5936)
* wip * initial implementation * add tests * more tests, fix policy Enabled * remove exempt statuses * test EnforcedOptions is populated * clean up, add test * fix test, add json attributes for deserialization * fix attribute casing * fix test --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
@@ -1,20 +1,28 @@
|
|||||||
namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
using System.Text.Json.Serialization;
|
||||||
|
namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
|
||||||
public class MasterPasswordPolicyData : IPolicyDataModel
|
public class MasterPasswordPolicyData : IPolicyDataModel
|
||||||
{
|
{
|
||||||
|
[JsonPropertyName("minComplexity")]
|
||||||
public int? MinComplexity { get; set; }
|
public int? MinComplexity { get; set; }
|
||||||
|
[JsonPropertyName("minLength")]
|
||||||
public int? MinLength { get; set; }
|
public int? MinLength { get; set; }
|
||||||
|
[JsonPropertyName("requireLower")]
|
||||||
public bool? RequireLower { get; set; }
|
public bool? RequireLower { get; set; }
|
||||||
|
[JsonPropertyName("requireUpper")]
|
||||||
public bool? RequireUpper { get; set; }
|
public bool? RequireUpper { get; set; }
|
||||||
|
[JsonPropertyName("requireNumbers")]
|
||||||
public bool? RequireNumbers { get; set; }
|
public bool? RequireNumbers { get; set; }
|
||||||
|
[JsonPropertyName("requireSpecial")]
|
||||||
public bool? RequireSpecial { get; set; }
|
public bool? RequireSpecial { get; set; }
|
||||||
|
[JsonPropertyName("enforceOnLogin")]
|
||||||
public bool? EnforceOnLogin { get; set; }
|
public bool? EnforceOnLogin { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Combine the other policy data with this instance, taking the most secure options
|
/// Combine the other policy data with this instance, taking the most secure options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="other">The other policy instance to combine with this</param>
|
/// <param name="other">The other policy instance to combine with this</param>
|
||||||
public void CombineWith(MasterPasswordPolicyData other)
|
public void CombineWith(MasterPasswordPolicyData? other)
|
||||||
{
|
{
|
||||||
if (other == null)
|
if (other == null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Policy requirements for the Master Password Requirements policy.
|
||||||
|
/// </summary>
|
||||||
|
public class MasterPasswordPolicyRequirement : IPolicyRequirement
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether MasterPassword requirements are enabled for the user.
|
||||||
|
/// </summary>
|
||||||
|
public bool Enabled { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Master Password Policy data model associated with this Policy
|
||||||
|
/// </summary>
|
||||||
|
public MasterPasswordPolicyData? EnforcedOptions { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MasterPasswordPolicyRequirementFactory : BasePolicyRequirementFactory<MasterPasswordPolicyRequirement>
|
||||||
|
{
|
||||||
|
public override PolicyType PolicyType => PolicyType.MasterPassword;
|
||||||
|
|
||||||
|
protected override bool ExemptProviders => false;
|
||||||
|
|
||||||
|
protected override IEnumerable<OrganizationUserType> ExemptRoles => [];
|
||||||
|
|
||||||
|
protected override IEnumerable<OrganizationUserStatusType> ExemptStatuses =>
|
||||||
|
[OrganizationUserStatusType.Accepted,
|
||||||
|
OrganizationUserStatusType.Invited,
|
||||||
|
OrganizationUserStatusType.Revoked,
|
||||||
|
];
|
||||||
|
|
||||||
|
public override MasterPasswordPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
|
||||||
|
{
|
||||||
|
var result = policyDetails
|
||||||
|
.Select(p => p.GetDataModel<MasterPasswordPolicyData>())
|
||||||
|
.Aggregate(
|
||||||
|
new MasterPasswordPolicyRequirement(),
|
||||||
|
(result, data) =>
|
||||||
|
{
|
||||||
|
data.CombineWith(result.EnforcedOptions);
|
||||||
|
return new MasterPasswordPolicyRequirement
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
EnforcedOptions = data
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@@ -19,21 +21,39 @@ public class PolicyService : IPolicyService
|
|||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyRepository _policyRepository;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||||
|
|
||||||
public PolicyService(
|
public PolicyService(
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IPolicyRepository policyRepository,
|
IPolicyRepository policyRepository,
|
||||||
GlobalSettings globalSettings)
|
GlobalSettings globalSettings,
|
||||||
|
IFeatureService featureService,
|
||||||
|
IPolicyRequirementQuery policyRequirementQuery)
|
||||||
{
|
{
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_policyRepository = policyRepository;
|
_policyRepository = policyRepository;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
|
_featureService = featureService;
|
||||||
|
_policyRequirementQuery = policyRequirementQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user)
|
public async Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user)
|
||||||
{
|
{
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
|
||||||
|
{
|
||||||
|
var masterPaswordPolicy = (await _policyRequirementQuery.GetAsync<MasterPasswordPolicyRequirement>(user.Id));
|
||||||
|
|
||||||
|
if (!masterPaswordPolicy.Enabled)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return masterPaswordPolicy.EnforcedOptions;
|
||||||
|
}
|
||||||
|
|
||||||
var policies = (await _policyRepository.GetManyByUserIdAsync(user.Id))
|
var policies = (await _policyRepository.GetManyByUserIdAsync(user.Id))
|
||||||
.Where(p => p.Type == PolicyType.MasterPassword && p.Enabled)
|
.Where(p => p.Type == PolicyType.MasterPassword && p.Enabled)
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -51,6 +71,7 @@ public class PolicyService : IPolicyService
|
|||||||
}
|
}
|
||||||
|
|
||||||
return enforcedOptions;
|
return enforcedOptions;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ICollection<OrganizationUserPolicyDetails>> GetPoliciesApplicableToUserAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted)
|
public async Task<ICollection<OrganizationUserPolicyDetails>> GetPoliciesApplicableToUserAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted)
|
||||||
|
|||||||
@@ -73,13 +73,13 @@ public class PoliciesControllerTests
|
|||||||
|
|
||||||
// Assert that the data is deserialized correctly into a Dictionary<string, object>
|
// Assert that the data is deserialized correctly into a Dictionary<string, object>
|
||||||
// for all MasterPasswordPolicyData properties
|
// for all MasterPasswordPolicyData properties
|
||||||
Assert.Equal(mpPolicyData.MinComplexity, ((JsonElement)result.Data["MinComplexity"]).GetInt32());
|
Assert.Equal(mpPolicyData.MinComplexity, ((JsonElement)result.Data["minComplexity"]).GetInt32());
|
||||||
Assert.Equal(mpPolicyData.MinLength, ((JsonElement)result.Data["MinLength"]).GetInt32());
|
Assert.Equal(mpPolicyData.MinLength, ((JsonElement)result.Data["minLength"]).GetInt32());
|
||||||
Assert.Equal(mpPolicyData.RequireLower, ((JsonElement)result.Data["RequireLower"]).GetBoolean());
|
Assert.Equal(mpPolicyData.RequireLower, ((JsonElement)result.Data["requireLower"]).GetBoolean());
|
||||||
Assert.Equal(mpPolicyData.RequireUpper, ((JsonElement)result.Data["RequireUpper"]).GetBoolean());
|
Assert.Equal(mpPolicyData.RequireUpper, ((JsonElement)result.Data["requireUpper"]).GetBoolean());
|
||||||
Assert.Equal(mpPolicyData.RequireNumbers, ((JsonElement)result.Data["RequireNumbers"]).GetBoolean());
|
Assert.Equal(mpPolicyData.RequireNumbers, ((JsonElement)result.Data["requireNumbers"]).GetBoolean());
|
||||||
Assert.Equal(mpPolicyData.RequireSpecial, ((JsonElement)result.Data["RequireSpecial"]).GetBoolean());
|
Assert.Equal(mpPolicyData.RequireSpecial, ((JsonElement)result.Data["requireSpecial"]).GetBoolean());
|
||||||
Assert.Equal(mpPolicyData.EnforceOnLogin, ((JsonElement)result.Data["EnforceOnLogin"]).GetBoolean());
|
Assert.Equal(mpPolicyData.EnforceOnLogin, ((JsonElement)result.Data["enforceOnLogin"]).GetBoolean());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
|
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class MasterPasswordPolicyRequirementFactoryTests
|
||||||
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void MasterPasswordPolicyData_CombineWith_Joins_Policy_Options(SutProvider<MasterPasswordPolicyRequirementFactory> sutProvider)
|
||||||
|
{
|
||||||
|
var mpd1 = JsonSerializer.Serialize(new MasterPasswordPolicyData { MinLength = 20, RequireLower = false, RequireSpecial = false });
|
||||||
|
var mpd2 = JsonSerializer.Serialize(new MasterPasswordPolicyData { RequireLower = true });
|
||||||
|
var mpd3 = JsonSerializer.Serialize(new MasterPasswordPolicyData { RequireSpecial = true });
|
||||||
|
|
||||||
|
var policyDetails1 = new PolicyDetails
|
||||||
|
{
|
||||||
|
PolicyType = PolicyType.MasterPassword,
|
||||||
|
PolicyData = mpd1
|
||||||
|
};
|
||||||
|
|
||||||
|
var policyDetails2 = new PolicyDetails
|
||||||
|
{
|
||||||
|
PolicyType = PolicyType.MasterPassword,
|
||||||
|
PolicyData = mpd2
|
||||||
|
};
|
||||||
|
var policyDetails3 = new PolicyDetails
|
||||||
|
{
|
||||||
|
PolicyType = PolicyType.MasterPassword,
|
||||||
|
PolicyData = mpd3
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var actual = sutProvider.Sut.Create([policyDetails1, policyDetails2, policyDetails3]);
|
||||||
|
|
||||||
|
Assert.NotNull(actual);
|
||||||
|
Assert.True(actual.Enabled);
|
||||||
|
Assert.True(actual.EnforcedOptions.RequireLower);
|
||||||
|
Assert.True(actual.EnforcedOptions.RequireSpecial);
|
||||||
|
Assert.Equal(20, actual.EnforcedOptions.MinLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void MasterPassword_IsFalse_IfNoPolicies(SutProvider<MasterPasswordPolicyRequirementFactory> sutProvider)
|
||||||
|
{
|
||||||
|
var actual = sutProvider.Sut.Create([]);
|
||||||
|
|
||||||
|
Assert.False(actual.Enabled);
|
||||||
|
Assert.Null(actual.EnforcedOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void MasterPassword_IsTrue_IfAnyDisableSendPolicies(
|
||||||
|
[PolicyDetails(PolicyType.MasterPassword)] PolicyDetails[] policies,
|
||||||
|
SutProvider<MasterPasswordPolicyRequirementFactory> sutProvider)
|
||||||
|
{
|
||||||
|
var actual = sutProvider.Sut.Create(policies);
|
||||||
|
|
||||||
|
Assert.True(actual.Enabled);
|
||||||
|
Assert.NotNull(actual.EnforcedOptions);
|
||||||
|
Assert.NotNull(actual.EnforcedOptions.EnforceOnLogin);
|
||||||
|
Assert.NotNull(actual.EnforcedOptions.RequireLower);
|
||||||
|
Assert.NotNull(actual.EnforcedOptions.RequireNumbers);
|
||||||
|
Assert.NotNull(actual.EnforcedOptions.RequireSpecial);
|
||||||
|
Assert.NotNull(actual.EnforcedOptions.RequireUpper);
|
||||||
|
Assert.Null(actual.EnforcedOptions.MinComplexity);
|
||||||
|
Assert.Null(actual.EnforcedOptions.MinLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services.Implementations;
|
using Bit.Core.AdminConsole.Services.Implementations;
|
||||||
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
@@ -117,6 +123,38 @@ public class PolicyServiceTests
|
|||||||
Assert.True(result);
|
Assert.True(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetMasterPasswordPolicyForUserAsync_WithFeatureFlagEnabled_EvaluatesPolicyRequirement(User user, SutProvider<PolicyService> sutProvider)
|
||||||
|
{
|
||||||
|
SetupUserPolicies(user.Id, sutProvider);
|
||||||
|
var policyRequirement = new MasterPasswordPolicyRequirement
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
EnforcedOptions = new MasterPasswordPolicyData()
|
||||||
|
};
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<MasterPasswordPolicyRequirement>(user.Id).Returns(policyRequirement);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.GetMasterPasswordPolicyForUserAsync(user);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.PolicyRequirements);
|
||||||
|
await sutProvider.GetDependency<IPolicyRepository>().DidNotReceive().GetManyByUserIdAsync(user.Id);
|
||||||
|
await sutProvider.GetDependency<IPolicyRequirementQuery>().Received(1).GetAsync<MasterPasswordPolicyRequirement>(user.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetMasterPasswordPolicyForUserAsync_WithFeatureFlagDisabled_EvaluatesPolicyDetails(User user, SutProvider<PolicyService> sutProvider)
|
||||||
|
{
|
||||||
|
SetupUserPolicies(user.Id, sutProvider);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(false);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.GetMasterPasswordPolicyForUserAsync(user);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.PolicyRequirements);
|
||||||
|
await sutProvider.GetDependency<IPolicyRepository>().Received(1).GetManyByUserIdAsync(user.Id);
|
||||||
|
await sutProvider.GetDependency<IPolicyRequirementQuery>().DidNotReceive().GetAsync<MasterPasswordPolicyRequirement>(user.Id);
|
||||||
|
}
|
||||||
|
|
||||||
private static void SetupOrg(SutProvider<PolicyService> sutProvider, Guid organizationId, Organization organization)
|
private static void SetupOrg(SutProvider<PolicyService> sutProvider, Guid organizationId, Organization organization)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IOrganizationRepository>()
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
|||||||
Reference in New Issue
Block a user