Skip to content

Commit a8a8c26

Browse files
authored
Merge branch 'main' into ac/pm-35253/add-useinvitelinks-organization-ability
2 parents 5265231 + 27ae3d5 commit a8a8c26

12 files changed

Lines changed: 313 additions & 187 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#nullable enable
2+
3+
using Bit.Core.AdminConsole.Enums;
4+
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
5+
using Bit.Core.Enums;
6+
7+
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
8+
9+
/// <summary>
10+
/// Policy requirements for the Master Password policy.
11+
/// </summary>
12+
public class MasterPasswordPolicyRequirement : IPolicyRequirement
13+
{
14+
/// <summary>
15+
/// The combined Master Password policy options enforced against the user, or null if no policy applies.
16+
/// </summary>
17+
public MasterPasswordPolicyData? EnforcedOptions { get; init; }
18+
}
19+
20+
/// <summary>
21+
/// Factory for <see cref="MasterPasswordPolicyRequirement"/>.
22+
/// Invited and Revoked users are exempt (inherited default from <see cref="BasePolicyRequirementFactory{T}"/>),
23+
/// which is intentional: master password requirements are enforced at login/unlock for active members only.
24+
/// </summary>
25+
public class MasterPasswordPolicyRequirementFactory : BasePolicyRequirementFactory<MasterPasswordPolicyRequirement>
26+
{
27+
public override PolicyType PolicyType => PolicyType.MasterPassword;
28+
29+
/// <summary>
30+
/// No roles are exempt from the master password policy.
31+
/// </summary>
32+
protected override IEnumerable<OrganizationUserType> ExemptRoles { get; } = [];
33+
34+
public override MasterPasswordPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
35+
{
36+
var combined = policyDetails
37+
.Select(p => p.GetDataModel<MasterPasswordPolicyData>())
38+
.Aggregate(new MasterPasswordPolicyData(), (result, data) =>
39+
{
40+
result.CombineWith(data);
41+
return result;
42+
});
43+
44+
// Only set EnforcedOptions if at least one field has a meaningful value.
45+
// A policy saved with no options set produces an all-null MasterPasswordPolicyData,
46+
// and callers rely on EnforcedOptions == null to mean "no policy enforced".
47+
var hasAnyOption = combined.MinComplexity.HasValue || combined.MinLength.HasValue ||
48+
(combined.RequireLower ?? false) || (combined.RequireUpper ?? false) ||
49+
(combined.RequireNumbers ?? false) || (combined.RequireSpecial ?? false) ||
50+
(combined.EnforceOnLogin ?? false);
51+
52+
return new MasterPasswordPolicyRequirement
53+
{
54+
EnforcedOptions = hasAnyOption ? combined : null
55+
};
56+
}
57+
}

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,6 @@ private static void AddPolicyRequirements(this IServiceCollection services)
5656
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireTwoFactorPolicyRequirementFactory>();
5757
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SingleOrganizationPolicyRequirementFactory>();
5858
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, AutomaticUserConfirmationPolicyRequirementFactory>();
59+
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, MasterPasswordPolicyRequirementFactory>();
5960
}
6061
}

src/Core/AdminConsole/Services/Implementations/PolicyService.cs

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
using Bit.Core.AdminConsole.Enums;
55
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
6-
using Bit.Core.AdminConsole.Repositories;
6+
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
7+
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
78
using Bit.Core.Entities;
89
using Bit.Core.Enums;
910
using Bit.Core.Models.Data.Organizations;
@@ -18,41 +19,25 @@ public class PolicyService : IPolicyService
1819
{
1920
private readonly IApplicationCacheService _applicationCacheService;
2021
private readonly IOrganizationUserRepository _organizationUserRepository;
21-
private readonly IPolicyRepository _policyRepository;
22+
private readonly IPolicyRequirementQuery _policyRequirementQuery;
2223
private readonly GlobalSettings _globalSettings;
2324

2425
public PolicyService(
2526
IApplicationCacheService applicationCacheService,
2627
IOrganizationUserRepository organizationUserRepository,
27-
IPolicyRepository policyRepository,
28+
IPolicyRequirementQuery policyRequirementQuery,
2829
GlobalSettings globalSettings)
2930
{
3031
_applicationCacheService = applicationCacheService;
3132
_organizationUserRepository = organizationUserRepository;
32-
_policyRepository = policyRepository;
33+
_policyRequirementQuery = policyRequirementQuery;
3334
_globalSettings = globalSettings;
3435
}
3536

3637
public async Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user)
3738
{
38-
var policies = (await _policyRepository.GetManyByUserIdAsync(user.Id))
39-
.Where(p => p.Type == PolicyType.MasterPassword && p.Enabled)
40-
.ToList();
41-
42-
if (!policies.Any())
43-
{
44-
return null;
45-
}
46-
47-
var enforcedOptions = new MasterPasswordPolicyData();
48-
49-
foreach (var policy in policies)
50-
{
51-
enforcedOptions.CombineWith(policy.GetDataModel<MasterPasswordPolicyData>());
52-
}
53-
54-
return enforcedOptions;
55-
39+
var requirement = await _policyRequirementQuery.GetAsyncVNext<MasterPasswordPolicyRequirement>(user.Id);
40+
return requirement.EnforcedOptions;
5641
}
5742

5843
public async Task<ICollection<OrganizationUserPolicyDetails>> GetPoliciesApplicableToUserAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted)

src/Sql/dbo/Stored Procedures/User_ReadBySsoUserOrganizationIdExternalId.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
CREATE PROCEDURE [dbo].[User_ReadBySsoUserOrganizationIdExternalId]
22
@OrganizationId UNIQUEIDENTIFIER,
3-
@ExternalId NVARCHAR(50)
3+
@ExternalId NVARCHAR(300)
44
AS
55
BEGIN
66
SET NOCOUNT ON

test/Core.IntegrationTest/Core.IntegrationTest.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
</PropertyGroup>
1111

1212
<ItemGroup>
13-
<PackageReference Include="coverlet.collector" Version="8.0.0" />
13+
<PackageReference Include="coverlet.collector" Version="10.0.0" />
1414
<PackageReference Include="MartinCostello.Logging.XUnit" Version="0.7.0" />
1515
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
1616
<PackageReference Include="Rnwood.SmtpServer" Version="3.1.0-ci0868" />
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
using Bit.Core.AdminConsole.Enums;
2+
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
3+
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
4+
using Bit.Core.Enums;
5+
using Bit.Core.Test.AdminConsole.AutoFixture;
6+
using Bit.Test.Common.AutoFixture;
7+
using Bit.Test.Common.AutoFixture.Attributes;
8+
using Xunit;
9+
10+
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
11+
12+
[SutProviderCustomize]
13+
public class MasterPasswordPolicyRequirementFactoryTests
14+
{
15+
[Theory]
16+
[BitAutoData(OrganizationUserType.Owner)]
17+
[BitAutoData(OrganizationUserType.Admin)]
18+
[BitAutoData(OrganizationUserType.User)]
19+
[BitAutoData(OrganizationUserType.Custom)]
20+
public void Enforce_EnforcesAgainstAllRoles(
21+
OrganizationUserType userType,
22+
SutProvider<MasterPasswordPolicyRequirementFactory> sutProvider)
23+
{
24+
var policy = new PolicyDetails
25+
{
26+
PolicyType = PolicyType.MasterPassword,
27+
OrganizationUserType = userType,
28+
OrganizationUserStatus = OrganizationUserStatusType.Confirmed
29+
};
30+
31+
Assert.True(sutProvider.Sut.Enforce(policy));
32+
}
33+
34+
35+
[Theory, BitAutoData]
36+
public void Create_WithNoPolicies_EnforcedOptionsIsNull(SutProvider<MasterPasswordPolicyRequirementFactory> sutProvider)
37+
{
38+
var actual = sutProvider.Sut.Create([]);
39+
40+
Assert.Null(actual.EnforcedOptions);
41+
}
42+
43+
[Theory, BitAutoData]
44+
public void Create_WithAllNullOptions_EnforcedOptionsIsNull(
45+
[PolicyDetails(PolicyType.MasterPassword)] PolicyDetails[] policies,
46+
SutProvider<MasterPasswordPolicyRequirementFactory> sutProvider)
47+
{
48+
foreach (var policy in policies)
49+
{
50+
policy.SetDataModel(new MasterPasswordPolicyData());
51+
}
52+
53+
var actual = sutProvider.Sut.Create(policies);
54+
55+
Assert.Null(actual.EnforcedOptions);
56+
}
57+
58+
[Theory, BitAutoData]
59+
public void Create_WithSinglePolicy_ReturnsEnforcedOptions(
60+
[PolicyDetails(PolicyType.MasterPassword)] PolicyDetails[] policies,
61+
SutProvider<MasterPasswordPolicyRequirementFactory> sutProvider)
62+
{
63+
policies[0].SetDataModel(new MasterPasswordPolicyData { MinLength = 12, RequireUpper = true });
64+
65+
var actual = sutProvider.Sut.Create([policies[0]]);
66+
67+
Assert.NotNull(actual.EnforcedOptions);
68+
Assert.Equal(12, actual.EnforcedOptions.MinLength);
69+
Assert.True(actual.EnforcedOptions.RequireUpper);
70+
}
71+
72+
[Theory, BitAutoData]
73+
public void Create_WithMultiplePolicies_TakesMaxMinComplexity(
74+
[PolicyDetails(PolicyType.MasterPassword)] PolicyDetails[] policies,
75+
SutProvider<MasterPasswordPolicyRequirementFactory> sutProvider)
76+
{
77+
policies[0].SetDataModel(new MasterPasswordPolicyData { MinComplexity = 2 });
78+
policies[1].SetDataModel(new MasterPasswordPolicyData { MinComplexity = 4 });
79+
80+
var actual = sutProvider.Sut.Create([policies[0], policies[1]]);
81+
82+
Assert.NotNull(actual.EnforcedOptions);
83+
Assert.Equal(4, actual.EnforcedOptions.MinComplexity);
84+
}
85+
86+
[Theory, BitAutoData]
87+
public void Create_WithMultiplePolicies_TakesMaxMinLength(
88+
[PolicyDetails(PolicyType.MasterPassword)] PolicyDetails[] policies,
89+
SutProvider<MasterPasswordPolicyRequirementFactory> sutProvider)
90+
{
91+
policies[0].SetDataModel(new MasterPasswordPolicyData { MinLength = 12 });
92+
policies[1].SetDataModel(new MasterPasswordPolicyData { MinLength = 20 });
93+
94+
var actual = sutProvider.Sut.Create([policies[0], policies[1]]);
95+
96+
Assert.NotNull(actual.EnforcedOptions);
97+
Assert.Equal(20, actual.EnforcedOptions.MinLength);
98+
}
99+
100+
[Theory, BitAutoData]
101+
public void Create_WithMultiplePolicies_OrsAllBooleanFlags(
102+
[PolicyDetails(PolicyType.MasterPassword)] PolicyDetails[] policies,
103+
SutProvider<MasterPasswordPolicyRequirementFactory> sutProvider)
104+
{
105+
policies[0].SetDataModel(new MasterPasswordPolicyData { RequireUpper = true });
106+
policies[1].SetDataModel(new MasterPasswordPolicyData { RequireSpecial = true });
107+
108+
var actual = sutProvider.Sut.Create([policies[0], policies[1]]);
109+
110+
Assert.NotNull(actual.EnforcedOptions);
111+
Assert.True(actual.EnforcedOptions.RequireUpper);
112+
Assert.True(actual.EnforcedOptions.RequireSpecial);
113+
}
114+
}

0 commit comments

Comments
 (0)