diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationPolicyEnforcementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationPolicyEnforcementQuery.cs index 4252cc35f9..fc7acdeb95 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationPolicyEnforcementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationPolicyEnforcementQuery.cs @@ -14,34 +14,32 @@ public class AutomaticUserConfirmationPolicyEnforcementQuery( public async Task> IsCompliantAsync( AutomaticUserConfirmationPolicyEnforcementRequest request) { - var (organizationUser, otherOrganizationsOrganizationUsers, user) = request; - var automaticUserConfirmationPolicyRequirement = await policyRequirementQuery .GetAsync(request.User.Id); - if (automaticUserConfirmationPolicyRequirement.IsEnabled(organizationUser.OrganizationId)) - { - return Invalid(request, new AutoConfirmDoesNotAllowMembershipToOtherOrganizations()); - } - - if (automaticUserConfirmationPolicyRequirement.IsEnabledAndUserIsAProvider(organizationUser.OrganizationId)) - { - return Invalid(request, new ProviderUsersCannotJoin()); - } - - if (automaticUserConfirmationPolicyRequirement.IsEnabledForOrganizationsOtherThan(organizationUser - .OrganizationId)) + if (automaticUserConfirmationPolicyRequirement.IsEnabled(request.OrganizationUser.OrganizationId) + && await OrganizationUserBelongsToAnotherOrganizationAsync(request)) { return Invalid(request, new OrganizationEnforcesSingleOrgPolicy()); } - if (otherOrganizationsOrganizationUsers is { Count: > 0 } - || (await organizationUserRepository.GetManyByUserAsync(user.Id)) - .Any(x => x.OrganizationId != organizationUser.OrganizationId)) + if (automaticUserConfirmationPolicyRequirement.IsEnabledAndUserIsAProvider(request.OrganizationUser.OrganizationId)) + { + return Invalid(request, new ProviderUsersCannotJoin()); + } + + if (automaticUserConfirmationPolicyRequirement.IsEnabledForOrganizationsOtherThan(request.OrganizationUser + .OrganizationId)) { return Invalid(request, new OtherOrganizationEnforcesSingleOrgPolicy()); } return Valid(request); } + + private async Task OrganizationUserBelongsToAnotherOrganizationAsync( + AutomaticUserConfirmationPolicyEnforcementRequest request) => + request.OtherOrganizationsOrganizationUsers?.ToArray() is { Length: > 0 } + || (await organizationUserRepository.GetManyByUserAsync(request.User.Id)) + .Any(x => x.OrganizationId != request.OrganizationUser.OrganizationId); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationPolicyEnforcementRequest.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationPolicyEnforcementRequest.cs index 72ce5248ec..216be4bce8 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationPolicyEnforcementRequest.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationPolicyEnforcementRequest.cs @@ -47,12 +47,5 @@ public record AutomaticUserConfirmationPolicyEnforcementRequest OtherOrganizationsOrganizationUsers = null; User = user; } - - public void Deconstruct(out OrganizationUser organizationUser, out ICollection? otherOrganizationsOrganizationUsers, out User user) - { - organizationUser = OrganizationUser; - otherOrganizationsOrganizationUsers = OtherOrganizationsOrganizationUsers?.ToArray(); - user = User; - } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationPolicyEnforcementQueryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationPolicyEnforcementQueryTests.cs new file mode 100644 index 0000000000..5515d8a4cb --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationPolicyEnforcementQueryTests.cs @@ -0,0 +1,305 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm; + +[SutProviderCustomize] +public class AutomaticUserConfirmationPolicyEnforcementQueryTests +{ + [Theory] + [BitAutoData] + public async Task IsCompliantAsync_WithNoOtherOrganizations_ReturnsValid( + SutProvider sutProvider, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + User user) + { + // Arrange + var request = new AutomaticUserConfirmationPolicyEnforcementRequest( + organizationUser, + [], + user); + + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new AutomaticUserConfirmationPolicyRequirement([])); + + sutProvider.GetDependency() + .GetManyByUserAsync(user.Id) + .Returns([organizationUser]); + + // Act + var result = await sutProvider.Sut.IsCompliantAsync(request); + + // Assert + Assert.True(result.IsValid); + } + + [Theory] + [BitAutoData] + public async Task IsCompliantAsync_WithPolicyEnabledOnSameOrganizationButNoOtherOrgs_ReturnsValid( + SutProvider sutProvider, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + User user) + { + // Arrange + var policyDetails = new PolicyDetails + { + OrganizationId = organizationUser.OrganizationId, + PolicyType = PolicyType.AutomaticUserConfirmation, + IsProvider = false + }; + + var request = new AutomaticUserConfirmationPolicyEnforcementRequest( + organizationUser, + [], + user); + + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new AutomaticUserConfirmationPolicyRequirement([policyDetails])); + + sutProvider.GetDependency() + .GetManyByUserAsync(user.Id) + .Returns([organizationUser]); // Only belongs to this one org + + // Act + var result = await sutProvider.Sut.IsCompliantAsync(request); + + // Assert + Assert.True(result.IsValid); + } + + [Theory] + [BitAutoData] + public async Task IsCompliantAsync_WithPolicyEnabledOnSameOrgAndUserHasOtherOrgs_ReturnsOrganizationEnforcesSingleOrgPolicyError( + SutProvider sutProvider, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + OrganizationUser otherOrgUser, + User user) + { + // Arrange + var policyDetails = new PolicyDetails + { + OrganizationId = organizationUser.OrganizationId, + PolicyType = PolicyType.AutomaticUserConfirmation, + IsProvider = false + }; + + var request = new AutomaticUserConfirmationPolicyEnforcementRequest( + organizationUser, + [otherOrgUser], // User has other org memberships + user); + + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new AutomaticUserConfirmationPolicyRequirement([policyDetails])); + + // Act + var result = await sutProvider.Sut.IsCompliantAsync(request); + + // Assert + Assert.True(result.IsError); + Assert.IsType(result.AsError); + } + + [Theory] + [BitAutoData] + public async Task IsCompliantAsync_WithUserIsProvider_ReturnsProviderUsersCannotJoinError( + SutProvider sutProvider, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + User user) + { + // Arrange + var policyDetails = new PolicyDetails + { + OrganizationId = organizationUser.OrganizationId, + PolicyType = PolicyType.AutomaticUserConfirmation, + IsProvider = true + }; + + var request = new AutomaticUserConfirmationPolicyEnforcementRequest( + organizationUser, + [], + user); + + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new AutomaticUserConfirmationPolicyRequirement([policyDetails])); + + sutProvider.GetDependency() + .GetManyByUserAsync(user.Id) + .Returns([organizationUser]); // Only in this org, so first check passes + + // Act + var result = await sutProvider.Sut.IsCompliantAsync(request); + + // Assert + Assert.True(result.IsError); + Assert.IsType(result.AsError); + } + + [Theory] + [BitAutoData] + public async Task IsCompliantAsync_WithPolicyEnabledOnOtherOrganization_ReturnsOtherOrganizationEnforcesSingleOrgPolicyError( + SutProvider sutProvider, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + User user) + { + // Arrange + var otherOrgId = Guid.NewGuid(); + var policyDetails = new PolicyDetails + { + OrganizationId = otherOrgId, // Different from organizationUser.OrganizationId + PolicyType = PolicyType.AutomaticUserConfirmation, + IsProvider = false + }; + + var request = new AutomaticUserConfirmationPolicyEnforcementRequest( + organizationUser, + [], + user); + + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new AutomaticUserConfirmationPolicyRequirement([policyDetails])); + + sutProvider.GetDependency() + .GetManyByUserAsync(user.Id) + .Returns([organizationUser]); + + // Act + var result = await sutProvider.Sut.IsCompliantAsync(request); + + // Assert + Assert.True(result.IsError); + Assert.IsType(result.AsError); + } + + [Theory] + [BitAutoData] + public async Task IsCompliantAsync_WithOtherOrganizationsButNoPolicyEnabled_ReturnsValid( + SutProvider sutProvider, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + OrganizationUser otherOrgUser, + User user) + { + // Arrange + // No policy enabled, so even with other org memberships, it should be valid + var request = new AutomaticUserConfirmationPolicyEnforcementRequest( + organizationUser, + [otherOrgUser], // User has other organization memberships + user); + + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new AutomaticUserConfirmationPolicyRequirement([])); + + // Act + var result = await sutProvider.Sut.IsCompliantAsync(request); + + // Assert + Assert.True(result.IsValid); + } + + [Theory] + [BitAutoData] + public async Task IsCompliantAsync_WithEmptyOtherOrganizationsAndSingleOrg_ReturnsValid( + SutProvider sutProvider, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + User user) + { + // Arrange + var request = new AutomaticUserConfirmationPolicyEnforcementRequest( + organizationUser, + [], + user); + + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new AutomaticUserConfirmationPolicyRequirement([])); + + sutProvider.GetDependency() + .GetManyByUserAsync(user.Id) + .Returns([organizationUser]); // Only one org + + // Act + var result = await sutProvider.Sut.IsCompliantAsync(request); + + // Assert + Assert.True(result.IsValid); + Assert.Equal(request, result.Request); + } + + [Theory] + [BitAutoData] + public async Task IsCompliantAsync_ChecksConditionsInCorrectOrder_ReturnsFirstFailure( + SutProvider sutProvider, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + OrganizationUser otherOrgUser, + User user) + { + // Arrange - Set up conditions that would fail multiple checks + var policyDetails = new PolicyDetails + { + OrganizationId = organizationUser.OrganizationId, // Would trigger first check + PolicyType = PolicyType.AutomaticUserConfirmation, + IsProvider = true // Would also trigger second check if first passes + }; + + var request = new AutomaticUserConfirmationPolicyEnforcementRequest( + organizationUser, + [otherOrgUser], // Would also fail the last check + user); + + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new AutomaticUserConfirmationPolicyRequirement([policyDetails])); + + // Act + var result = await sutProvider.Sut.IsCompliantAsync(request); + + // Assert - Should fail on the FIRST check (IsEnabled on same org AND has other orgs) + Assert.True(result.IsError); + Assert.IsType(result.AsError); + } + + [Theory] + [BitAutoData] + public async Task IsCompliantAsync_WithNullOtherOrganizations_ReturnsValidWhenNoOtherOrgs( + SutProvider sutProvider, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + User user) + { + // Arrange + var request = new AutomaticUserConfirmationPolicyEnforcementRequest( + organizationUser, + null, // Null other organizations + user); + + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new AutomaticUserConfirmationPolicyRequirement([])); + + sutProvider.GetDependency() + .GetManyByUserAsync(user.Id) + .Returns([organizationUser]); // Only one org + + // Act + var result = await sutProvider.Sut.IsCompliantAsync(request); + + // Assert + Assert.True(result.IsValid); + } +}