diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandler.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandler.cs index c0d302df02..86c94147f4 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandler.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandler.cs @@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; @@ -17,26 +18,13 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; ///
  • All organization users are compliant with the Single organization policy
  • ///
  • No provider users exist
  • /// -/// -/// This class also performs side effects when the policy is being enabled or disabled. They are: -/// /// public class AutomaticUserConfirmationPolicyEventHandler( IOrganizationUserRepository organizationUserRepository, - IProviderUserRepository providerUserRepository, - IPolicyRepository policyRepository, - IOrganizationRepository organizationRepository, - TimeProvider timeProvider) - : IPolicyValidator, IPolicyValidationEvent, IOnPolicyPreUpdateEvent, IEnforceDependentPoliciesEvent + IProviderUserRepository providerUserRepository) + : IPolicyValidator, IPolicyValidationEvent, IEnforceDependentPoliciesEvent { public PolicyType Type => PolicyType.AutomaticUserConfirmation; - public async Task ExecutePreUpsertSideEffectAsync(SavePolicyModel policyRequest, Policy? currentPolicy) => - await OnSaveSideEffectsAsync(policyRequest.PolicyUpdate, currentPolicy); - - private const string _singleOrgPolicyNotEnabledErrorMessage = - "The Single organization policy must be enabled before enabling the Automatically confirm invited users policy."; private const string _usersNotCompliantWithSingleOrgErrorMessage = "All organization users must be compliant with the Single organization policy before enabling the Automatically confirm invited users policy. Please remove users who are members of multiple organizations."; @@ -61,27 +49,20 @@ public class AutomaticUserConfirmationPolicyEventHandler( public async Task ValidateAsync(SavePolicyModel savePolicyModel, Policy? currentPolicy) => await ValidateAsync(savePolicyModel.PolicyUpdate, currentPolicy); - public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) - { - var organization = await organizationRepository.GetByIdAsync(policyUpdate.OrganizationId); - - if (organization is not null) - { - organization.UseAutomaticUserConfirmation = policyUpdate.Enabled; - organization.RevisionDate = timeProvider.GetUtcNow().UtcDateTime; - await organizationRepository.UpsertAsync(organization); - } - } + public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => + Task.CompletedTask; private async Task ValidateEnablingPolicyAsync(Guid organizationId) { - var singleOrgValidationError = await ValidateSingleOrgPolicyComplianceAsync(organizationId); + var organizationUsers = await organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId); + + var singleOrgValidationError = await ValidateUserComplianceWithSingleOrgAsync(organizationId, organizationUsers); if (!string.IsNullOrWhiteSpace(singleOrgValidationError)) { return singleOrgValidationError; } - var providerValidationError = await ValidateNoProviderUsersAsync(organizationId); + var providerValidationError = await ValidateNoProviderUsersAsync(organizationUsers); if (!string.IsNullOrWhiteSpace(providerValidationError)) { return providerValidationError; @@ -90,42 +71,24 @@ public class AutomaticUserConfirmationPolicyEventHandler( return string.Empty; } - private async Task ValidateSingleOrgPolicyComplianceAsync(Guid organizationId) + private async Task ValidateUserComplianceWithSingleOrgAsync(Guid organizationId, + ICollection organizationUsers) { - var singleOrgPolicy = await policyRepository.GetByOrganizationIdTypeAsync(organizationId, PolicyType.SingleOrg); - if (singleOrgPolicy is not { Enabled: true }) - { - return _singleOrgPolicyNotEnabledErrorMessage; - } - - return await ValidateUserComplianceWithSingleOrgAsync(organizationId); - } - - private async Task ValidateUserComplianceWithSingleOrgAsync(Guid organizationId) - { - var organizationUsers = (await organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId)) - .Where(ou => ou.Status != OrganizationUserStatusType.Invited && - ou.Status != OrganizationUserStatusType.Revoked && - ou.UserId.HasValue) - .ToList(); - - if (organizationUsers.Count == 0) - { - return string.Empty; - } - var hasNonCompliantUser = (await organizationUserRepository.GetManyByManyUsersAsync( organizationUsers.Select(ou => ou.UserId!.Value))) - .Any(uo => uo.OrganizationId != organizationId && - uo.Status != OrganizationUserStatusType.Invited); + .Any(uo => uo.OrganizationId != organizationId + && uo.Status != OrganizationUserStatusType.Invited); return hasNonCompliantUser ? _usersNotCompliantWithSingleOrgErrorMessage : string.Empty; } - private async Task ValidateNoProviderUsersAsync(Guid organizationId) + private async Task ValidateNoProviderUsersAsync(ICollection organizationUsers) { - var providerUsers = await providerUserRepository.GetManyByOrganizationAsync(organizationId); + var userIds = organizationUsers.Where(x => x.UserId is not null) + .Select(x => x.UserId!.Value); - return providerUsers.Count > 0 ? _providerUsersExistErrorMessage : string.Empty; + return (await providerUserRepository.GetManyByManyUsersAsync(userIds)).Count != 0 + ? _providerUsersExistErrorMessage + : string.Empty; } } diff --git a/src/Core/AdminConsole/Repositories/IProviderUserRepository.cs b/src/Core/AdminConsole/Repositories/IProviderUserRepository.cs index 7bc4125778..0a640b7530 100644 --- a/src/Core/AdminConsole/Repositories/IProviderUserRepository.cs +++ b/src/Core/AdminConsole/Repositories/IProviderUserRepository.cs @@ -12,6 +12,7 @@ public interface IProviderUserRepository : IRepository Task GetCountByProviderAsync(Guid providerId, string email, bool onlyRegisteredUsers); Task> GetManyAsync(IEnumerable ids); Task> GetManyByUserAsync(Guid userId); + Task> GetManyByManyUsersAsync(IEnumerable userIds); Task GetByProviderUserAsync(Guid providerId, Guid userId); Task> GetManyByProviderAsync(Guid providerId, ProviderUserType? type = null); Task> GetManyDetailsByProviderAsync(Guid providerId, ProviderUserStatusType? status = null); diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/ProviderUserRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/ProviderUserRepository.cs index 467857612f..c05ff040e5 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/ProviderUserRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/ProviderUserRepository.cs @@ -61,6 +61,18 @@ public class ProviderUserRepository : Repository, IProviderU } } + public async Task> GetManyByManyUsersAsync(IEnumerable userIds) + { + await using var connection = new SqlConnection(ConnectionString); + + var results = await connection.QueryAsync( + "[dbo].[ProviderUser_ReadManyByManyUserIds]", + new { UserIds = userIds.ToGuidIdArrayTVP() }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + public async Task GetByProviderUserAsync(Guid providerId, Guid userId) { using (var connection = new SqlConnection(ConnectionString)) diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/ProviderUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/ProviderUserRepository.cs index 5474e3e217..8f9a38f9b6 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/ProviderUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/ProviderUserRepository.cs @@ -96,6 +96,20 @@ public class ProviderUserRepository : return await query.ToArrayAsync(); } } + + public async Task> GetManyByManyUsersAsync(IEnumerable userIds) + { + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + + var dbContext = GetDatabaseContext(scope); + + var query = from pu in dbContext.ProviderUsers + where pu.UserId != null && userIds.Contains(pu.UserId.Value) + select pu; + + return await query.ToArrayAsync(); + } + public async Task GetByProviderUserAsync(Guid providerId, Guid userId) { using (var scope = ServiceScopeFactory.CreateScope()) diff --git a/src/Sql/dbo/Stored Procedures/ProviderUser_ReadManyByManyUserIds.sql b/src/Sql/dbo/Stored Procedures/ProviderUser_ReadManyByManyUserIds.sql new file mode 100644 index 0000000000..4fe8d153e4 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/ProviderUser_ReadManyByManyUserIds.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[ProviderUser_ReadManyByManyUserIds] + @UserIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + SELECT + [pu].* + FROM + [dbo].[ProviderUserView] AS [pu] + INNER JOIN + @UserIds [u] ON [u].[Id] = [pu].[UserId] +END diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandlerTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandlerTests.cs index 4781127a3d..3c9fd9a9e9 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandlerTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandlerTests.cs @@ -21,52 +21,23 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidat public class AutomaticUserConfirmationPolicyEventHandlerTests { [Theory, BitAutoData] - public async Task ValidateAsync_EnablingPolicy_SingleOrgNotEnabled_ReturnsError( - [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, + public void RequiredPolicies_IncludesSingleOrg( SutProvider sutProvider) { - // Arrange - sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg) - .Returns((Policy?)null); - // Act - var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null); + var requiredPolicies = sutProvider.Sut.RequiredPolicies; // Assert - Assert.Contains("Single organization policy must be enabled", result, StringComparison.OrdinalIgnoreCase); - } - - [Theory, BitAutoData] - public async Task ValidateAsync_EnablingPolicy_SingleOrgPolicyDisabled_ReturnsError( - [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, - [Policy(PolicyType.SingleOrg, false)] Policy singleOrgPolicy, - SutProvider sutProvider) - { - // Arrange - singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId; - - sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg) - .Returns(singleOrgPolicy); - - // Act - var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null); - - // Assert - Assert.Contains("Single organization policy must be enabled", result, StringComparison.OrdinalIgnoreCase); + Assert.Contains(PolicyType.SingleOrg, requiredPolicies); } [Theory, BitAutoData] public async Task ValidateAsync_EnablingPolicy_UsersNotCompliantWithSingleOrg_ReturnsError( [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, - [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy, Guid nonCompliantUserId, SutProvider sutProvider) { // Arrange - singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId; - var orgUser = new OrganizationUserUserDetails { Id = Guid.NewGuid(), @@ -85,10 +56,6 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests Status = OrganizationUserStatusType.Confirmed }; - sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg) - .Returns(singleOrgPolicy); - sutProvider.GetDependency() .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) .Returns([orgUser]); @@ -107,13 +74,10 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests [Theory, BitAutoData] public async Task ValidateAsync_EnablingPolicy_UserWithInvitedStatusInOtherOrg_ValidationPasses( [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, - [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy, Guid userId, SutProvider sutProvider) { // Arrange - singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId; - var orgUser = new OrganizationUserUserDetails { Id = Guid.NewGuid(), @@ -121,7 +85,6 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests Type = OrganizationUserType.User, Status = OrganizationUserStatusType.Confirmed, UserId = userId, - Email = "test@email.com" }; var otherOrgUser = new OrganizationUser @@ -133,10 +96,6 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests Email = orgUser.Email }; - sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg) - .Returns(singleOrgPolicy); - sutProvider.GetDependency() .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) .Returns([orgUser]); @@ -146,7 +105,7 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests .Returns([otherOrgUser]); sutProvider.GetDependency() - .GetManyByOrganizationAsync(policyUpdate.OrganizationId) + .GetManyByManyUsersAsync(Arg.Any>()) .Returns([]); // Act @@ -159,30 +118,37 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests [Theory, BitAutoData] public async Task ValidateAsync_EnablingPolicy_ProviderUsersExist_ReturnsError( [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, - [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy, + Guid userId, SutProvider sutProvider) { // Arrange - singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId; + var orgUser = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = policyUpdate.OrganizationId, + Type = OrganizationUserType.User, + Status = OrganizationUserStatusType.Confirmed, + UserId = userId + }; var providerUser = new ProviderUser { Id = Guid.NewGuid(), ProviderId = Guid.NewGuid(), - UserId = Guid.NewGuid(), + UserId = userId, Status = ProviderUserStatusType.Confirmed }; - sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg) - .Returns(singleOrgPolicy); - sutProvider.GetDependency() .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) + .Returns([orgUser]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) .Returns([]); sutProvider.GetDependency() - .GetManyByOrganizationAsync(policyUpdate.OrganizationId) + .GetManyByManyUsersAsync(Arg.Any>()) .Returns([providerUser]); // Act @@ -196,26 +162,18 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests [Theory, BitAutoData] public async Task ValidateAsync_EnablingPolicy_AllValidationsPassed_ReturnsEmptyString( [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, - [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy, SutProvider sutProvider) { // Arrange - singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId; - var orgUser = new OrganizationUserUserDetails { Id = Guid.NewGuid(), OrganizationId = policyUpdate.OrganizationId, Type = OrganizationUserType.User, Status = OrganizationUserStatusType.Confirmed, - UserId = Guid.NewGuid(), - Email = "user@example.com" + UserId = Guid.NewGuid() }; - sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg) - .Returns(singleOrgPolicy); - sutProvider.GetDependency() .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) .Returns([orgUser]); @@ -225,7 +183,7 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests .Returns([]); sutProvider.GetDependency() - .GetManyByOrganizationAsync(policyUpdate.OrganizationId) + .GetManyByManyUsersAsync(Arg.Any>()) .Returns([]); // Act @@ -249,9 +207,10 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests // Assert Assert.True(string.IsNullOrEmpty(result)); - await sutProvider.GetDependency() + + await sutProvider.GetDependency() .DidNotReceive() - .GetByOrganizationIdTypeAsync(Arg.Any(), Arg.Any()); + .GetManyDetailsByOrganizationAsync(Arg.Any()); } [Theory, BitAutoData] @@ -268,21 +227,18 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests // Assert Assert.True(string.IsNullOrEmpty(result)); - await sutProvider.GetDependency() + await sutProvider.GetDependency() .DidNotReceive() - .GetByOrganizationIdTypeAsync(Arg.Any(), Arg.Any()); + .GetManyDetailsByOrganizationAsync(Arg.Any()); } [Theory, BitAutoData] public async Task ValidateAsync_EnablingPolicy_IncludesOwnersAndAdmins_InComplianceCheck( [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, - [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy, Guid nonCompliantOwnerId, SutProvider sutProvider) { // Arrange - singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId; - var ownerUser = new OrganizationUserUserDetails { Id = Guid.NewGuid(), @@ -290,7 +246,6 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests Type = OrganizationUserType.Owner, Status = OrganizationUserStatusType.Confirmed, UserId = nonCompliantOwnerId, - Email = "owner@example.com" }; var otherOrgUser = new OrganizationUser @@ -301,10 +256,6 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests Status = OrganizationUserStatusType.Confirmed }; - sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg) - .Returns(singleOrgPolicy); - sutProvider.GetDependency() .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) .Returns([ownerUser]); @@ -323,12 +274,9 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests [Theory, BitAutoData] public async Task ValidateAsync_EnablingPolicy_InvitedUsersExcluded_FromComplianceCheck( [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, - [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy, SutProvider sutProvider) { // Arrange - singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId; - var invitedUser = new OrganizationUserUserDetails { Id = Guid.NewGuid(), @@ -339,16 +287,12 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests Email = "invited@example.com" }; - sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg) - .Returns(singleOrgPolicy); - sutProvider.GetDependency() .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) .Returns([invitedUser]); sutProvider.GetDependency() - .GetManyByOrganizationAsync(policyUpdate.OrganizationId) + .GetManyByManyUsersAsync(Arg.Any>()) .Returns([]); // Act @@ -359,14 +303,11 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests } [Theory, BitAutoData] - public async Task ValidateAsync_EnablingPolicy_RevokedUsersExcluded_FromComplianceCheck( + public async Task ValidateAsync_EnablingPolicy_RevokedUsersIncluded_InComplianceCheck( [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, - [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy, SutProvider sutProvider) { // Arrange - singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId; - var revokedUser = new OrganizationUserUserDetails { Id = Guid.NewGuid(), @@ -374,38 +315,44 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests Type = OrganizationUserType.User, Status = OrganizationUserStatusType.Revoked, UserId = Guid.NewGuid(), - Email = "revoked@example.com" }; - sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg) - .Returns(singleOrgPolicy); + var additionalOrgUser = new OrganizationUser + { + Id = Guid.NewGuid(), + OrganizationId = Guid.NewGuid(), + Type = OrganizationUserType.User, + Status = OrganizationUserStatusType.Revoked, + UserId = revokedUser.UserId, + }; - sutProvider.GetDependency() + var orgUserRepository = sutProvider.GetDependency(); + + orgUserRepository .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) .Returns([revokedUser]); + orgUserRepository.GetManyByManyUsersAsync(Arg.Any>()) + .Returns([additionalOrgUser]); + sutProvider.GetDependency() - .GetManyByOrganizationAsync(policyUpdate.OrganizationId) + .GetManyByManyUsersAsync(Arg.Any>()) .Returns([]); // Act var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null); // Assert - Assert.True(string.IsNullOrEmpty(result)); + Assert.Contains("compliant with the Single organization policy", result, StringComparison.OrdinalIgnoreCase); } [Theory, BitAutoData] public async Task ValidateAsync_EnablingPolicy_AcceptedUsersIncluded_InComplianceCheck( [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, - [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy, Guid nonCompliantUserId, SutProvider sutProvider) { // Arrange - singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId; - var acceptedUser = new OrganizationUserUserDetails { Id = Guid.NewGuid(), @@ -413,7 +360,6 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests Type = OrganizationUserType.User, Status = OrganizationUserStatusType.Accepted, UserId = nonCompliantUserId, - Email = "accepted@example.com" }; var otherOrgUser = new OrganizationUser @@ -424,10 +370,6 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests Status = OrganizationUserStatusType.Confirmed }; - sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg) - .Returns(singleOrgPolicy); - sutProvider.GetDependency() .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) .Returns([acceptedUser]); @@ -443,186 +385,22 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests Assert.Contains("compliant with the Single organization policy", result, StringComparison.OrdinalIgnoreCase); } - [Theory, BitAutoData] - public async Task ValidateAsync_EnablingPolicy_EmptyOrganization_ReturnsEmptyString( - [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, - [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy, - SutProvider sutProvider) - { - // Arrange - singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId; - - sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg) - .Returns(singleOrgPolicy); - - sutProvider.GetDependency() - .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) - .Returns([]); - - sutProvider.GetDependency() - .GetManyByOrganizationAsync(policyUpdate.OrganizationId) - .Returns([]); - - // Act - var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null); - - // Assert - Assert.True(string.IsNullOrEmpty(result)); - } - [Theory, BitAutoData] public async Task ValidateAsync_WithSavePolicyModel_CallsValidateWithPolicyUpdate( [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, - [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy, SutProvider sutProvider) { // Arrange - singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId; - var savePolicyModel = new SavePolicyModel(policyUpdate); - sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg) - .Returns(singleOrgPolicy); - sutProvider.GetDependency() .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) .Returns([]); - sutProvider.GetDependency() - .GetManyByOrganizationAsync(policyUpdate.OrganizationId) - .Returns([]); - // Act var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, null); // Assert Assert.True(string.IsNullOrEmpty(result)); } - - [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_EnablingPolicy_SetsUseAutomaticUserConfirmationToTrue( - [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, - Organization organization, - SutProvider sutProvider) - { - // Arrange - organization.Id = policyUpdate.OrganizationId; - organization.UseAutomaticUserConfirmation = false; - - sutProvider.GetDependency() - .GetByIdAsync(policyUpdate.OrganizationId) - .Returns(organization); - - // Act - await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, null); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .UpsertAsync(Arg.Is(o => - o.Id == organization.Id && - o.UseAutomaticUserConfirmation == true && - o.RevisionDate > DateTime.MinValue)); - } - - [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_DisablingPolicy_SetsUseAutomaticUserConfirmationToFalse( - [PolicyUpdate(PolicyType.AutomaticUserConfirmation, false)] PolicyUpdate policyUpdate, - Organization organization, - SutProvider sutProvider) - { - // Arrange - organization.Id = policyUpdate.OrganizationId; - organization.UseAutomaticUserConfirmation = true; - - sutProvider.GetDependency() - .GetByIdAsync(policyUpdate.OrganizationId) - .Returns(organization); - - // Act - await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, null); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .UpsertAsync(Arg.Is(o => - o.Id == organization.Id && - o.UseAutomaticUserConfirmation == false && - o.RevisionDate > DateTime.MinValue)); - } - - [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_OrganizationNotFound_DoesNotThrowException( - [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, - SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency() - .GetByIdAsync(policyUpdate.OrganizationId) - .Returns((Organization?)null); - - // Act - await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, null); - - // Assert - await sutProvider.GetDependency() - .DidNotReceive() - .UpsertAsync(Arg.Any()); - } - - [Theory, BitAutoData] - public async Task ExecutePreUpsertSideEffectAsync_CallsOnSaveSideEffectsAsync( - [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, - [Policy(PolicyType.AutomaticUserConfirmation)] Policy currentPolicy, - Organization organization, - SutProvider sutProvider) - { - // Arrange - organization.Id = policyUpdate.OrganizationId; - currentPolicy.OrganizationId = policyUpdate.OrganizationId; - - var savePolicyModel = new SavePolicyModel(policyUpdate); - - sutProvider.GetDependency() - .GetByIdAsync(policyUpdate.OrganizationId) - .Returns(organization); - - // Act - await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, currentPolicy); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .UpsertAsync(Arg.Is(o => - o.Id == organization.Id && - o.UseAutomaticUserConfirmation == policyUpdate.Enabled)); - } - - [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_UpdatesRevisionDate( - [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, - Organization organization, - SutProvider sutProvider) - { - // Arrange - organization.Id = policyUpdate.OrganizationId; - var originalRevisionDate = DateTime.UtcNow.AddDays(-1); - organization.RevisionDate = originalRevisionDate; - - sutProvider.GetDependency() - .GetByIdAsync(policyUpdate.OrganizationId) - .Returns(organization); - - // Act - await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, null); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .UpsertAsync(Arg.Is(o => - o.Id == organization.Id && - o.RevisionDate > originalRevisionDate)); - } } diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/ProviderUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/ProviderUserRepositoryTests.cs index 0d1d28f33d..b502c6c997 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/ProviderUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/ProviderUserRepositoryTests.cs @@ -89,6 +89,286 @@ public class ProviderUserRepositoryTests Assert.Equal(serializedSsoConfigData, orgWithSsoDetails.SsoConfig); } + [Theory, DatabaseData] + public async Task GetManyByManyUsersAsync_WithMultipleUsers_ReturnsAllProviderUsers( + IUserRepository userRepository, + IProviderRepository providerRepository, + IProviderUserRepository providerUserRepository) + { + var user1 = await userRepository.CreateTestUserAsync(); + var user2 = await userRepository.CreateTestUserAsync(); + var user3 = await userRepository.CreateTestUserAsync(); + + var provider1 = await providerRepository.CreateAsync(new Provider + { + Name = "Test Provider 1", + Enabled = true, + Type = ProviderType.Msp + }); + + var provider2 = await providerRepository.CreateAsync(new Provider + { + Name = "Test Provider 2", + Enabled = true, + Type = ProviderType.Reseller + }); + + var providerUser1 = await providerUserRepository.CreateAsync(new ProviderUser + { + Id = Guid.NewGuid(), + ProviderId = provider1.Id, + UserId = user1.Id, + Status = ProviderUserStatusType.Confirmed, + Type = ProviderUserType.ProviderAdmin + }); + + var providerUser2 = await providerUserRepository.CreateAsync(new ProviderUser + { + Id = Guid.NewGuid(), + ProviderId = provider1.Id, + UserId = user2.Id, + Status = ProviderUserStatusType.Invited, + Type = ProviderUserType.ServiceUser + }); + + var providerUser3 = await providerUserRepository.CreateAsync(new ProviderUser + { + Id = Guid.NewGuid(), + ProviderId = provider2.Id, + UserId = user3.Id, + Status = ProviderUserStatusType.Confirmed, + Type = ProviderUserType.ProviderAdmin + }); + + var userIds = new[] { user1.Id, user2.Id, user3.Id }; + + var results = (await providerUserRepository.GetManyByManyUsersAsync(userIds)).ToList(); + + Assert.Equal(3, results.Count); + Assert.Contains(results, pu => pu.Id == providerUser1.Id && pu.UserId == user1.Id); + Assert.Contains(results, pu => pu.Id == providerUser2.Id && pu.UserId == user2.Id); + Assert.Contains(results, pu => pu.Id == providerUser3.Id && pu.UserId == user3.Id); + } + + [Theory, DatabaseData] + public async Task GetManyByManyUsersAsync_WithSingleUser_ReturnsSingleProviderUser( + IUserRepository userRepository, + IProviderRepository providerRepository, + IProviderUserRepository providerUserRepository) + { + var user = await userRepository.CreateTestUserAsync(); + + var provider = await providerRepository.CreateAsync(new Provider + { + Name = "Test Provider", + Enabled = true, + Type = ProviderType.Msp + }); + + var providerUser = await providerUserRepository.CreateAsync(new ProviderUser + { + Id = Guid.NewGuid(), + ProviderId = provider.Id, + UserId = user.Id, + Status = ProviderUserStatusType.Confirmed, + Type = ProviderUserType.ProviderAdmin + }); + + var results = (await providerUserRepository.GetManyByManyUsersAsync([user.Id])).ToList(); + + Assert.Single(results); + Assert.Equal(user.Id, results[0].UserId); + Assert.Equal(provider.Id, results[0].ProviderId); + } + + [Theory, DatabaseData] + public async Task GetManyByManyUsersAsync_WithUserHavingMultipleProviders_ReturnsAllProviderUsers( + IUserRepository userRepository, + IProviderRepository providerRepository, + IProviderUserRepository providerUserRepository) + { + var user = await userRepository.CreateTestUserAsync(); + + var provider1 = await providerRepository.CreateAsync(new Provider + { + Name = "Test Provider 1", + Enabled = true, + Type = ProviderType.Msp + }); + + var provider2 = await providerRepository.CreateAsync(new Provider + { + Name = "Test Provider 2", + Enabled = true, + Type = ProviderType.Reseller + }); + + var providerUser1 = await providerUserRepository.CreateAsync(new ProviderUser + { + ProviderId = provider1.Id, + UserId = user.Id, + Status = ProviderUserStatusType.Confirmed, + Type = ProviderUserType.ProviderAdmin + }); + + var providerUser2 = await providerUserRepository.CreateAsync(new ProviderUser + { + ProviderId = provider2.Id, + UserId = user.Id, + Status = ProviderUserStatusType.Confirmed, + Type = ProviderUserType.ServiceUser + }); + + var results = (await providerUserRepository.GetManyByManyUsersAsync([user.Id])).ToList(); + + Assert.Equal(2, results.Count); + Assert.Contains(results, pu => pu.Id == providerUser1.Id); + Assert.Contains(results, pu => pu.Id == providerUser2.Id); + } + + [Theory, DatabaseData] + public async Task GetManyByManyUsersAsync_WithEmptyUserIds_ReturnsEmpty( + IProviderUserRepository providerUserRepository) + { + var results = await providerUserRepository.GetManyByManyUsersAsync(Array.Empty()); + + Assert.Empty(results); + } + + [Theory, DatabaseData] + public async Task GetManyByManyUsersAsync_WithNonExistentUserIds_ReturnsEmpty( + IProviderUserRepository providerUserRepository) + { + var nonExistentUserIds = new[] { Guid.NewGuid(), Guid.NewGuid() }; + + var results = await providerUserRepository.GetManyByManyUsersAsync(nonExistentUserIds); + + Assert.Empty(results); + } + + [Theory, DatabaseData] + public async Task GetManyByManyUsersAsync_WithMixedExistentAndNonExistentUserIds_ReturnsOnlyExistent( + IUserRepository userRepository, + IProviderRepository providerRepository, + IProviderUserRepository providerUserRepository) + { + var existingUser = await userRepository.CreateTestUserAsync(); + + var provider = await providerRepository.CreateAsync(new Provider + { + Name = "Test Provider", + Enabled = true, + Type = ProviderType.Msp + }); + + var providerUser = await providerUserRepository.CreateAsync(new ProviderUser + { + ProviderId = provider.Id, + UserId = existingUser.Id, + Status = ProviderUserStatusType.Confirmed, + Type = ProviderUserType.ProviderAdmin + }); + + var userIds = new[] { existingUser.Id, Guid.NewGuid(), Guid.NewGuid() }; + + var results = (await providerUserRepository.GetManyByManyUsersAsync(userIds)).ToList(); + + Assert.Single(results); + Assert.Equal(existingUser.Id, results[0].UserId); + } + + [Theory, DatabaseData] + public async Task GetManyByManyUsersAsync_ReturnsAllStatuses( + IUserRepository userRepository, + IProviderRepository providerRepository, + IProviderUserRepository providerUserRepository) + { + var user1 = await userRepository.CreateTestUserAsync(); + var user2 = await userRepository.CreateTestUserAsync(); + var user3 = await userRepository.CreateTestUserAsync(); + + var provider = await providerRepository.CreateAsync(new Provider + { + Name = "Test Provider", + Enabled = true, + Type = ProviderType.Msp + }); + + await providerUserRepository.CreateAsync(new ProviderUser + { + ProviderId = provider.Id, + UserId = user1.Id, + Status = ProviderUserStatusType.Invited, + Type = ProviderUserType.ServiceUser + }); + + await providerUserRepository.CreateAsync(new ProviderUser + { + ProviderId = provider.Id, + UserId = user2.Id, + Status = ProviderUserStatusType.Accepted, + Type = ProviderUserType.ServiceUser + }); + + await providerUserRepository.CreateAsync(new ProviderUser + { + ProviderId = provider.Id, + UserId = user3.Id, + Status = ProviderUserStatusType.Confirmed, + Type = ProviderUserType.ProviderAdmin + }); + + var userIds = new[] { user1.Id, user2.Id, user3.Id }; + + var results = (await providerUserRepository.GetManyByManyUsersAsync(userIds)).ToList(); + + Assert.Equal(3, results.Count); + Assert.Contains(results, pu => pu.UserId == user1.Id && pu.Status == ProviderUserStatusType.Invited); + Assert.Contains(results, pu => pu.UserId == user2.Id && pu.Status == ProviderUserStatusType.Accepted); + Assert.Contains(results, pu => pu.UserId == user3.Id && pu.Status == ProviderUserStatusType.Confirmed); + } + + [Theory, DatabaseData] + public async Task GetManyByManyUsersAsync_ReturnsAllProviderUserTypes( + IUserRepository userRepository, + IProviderRepository providerRepository, + IProviderUserRepository providerUserRepository) + { + var user1 = await userRepository.CreateTestUserAsync(); + var user2 = await userRepository.CreateTestUserAsync(); + + var provider = await providerRepository.CreateAsync(new Provider + { + Name = "Test Provider", + Enabled = true, + Type = ProviderType.Msp + }); + + await providerUserRepository.CreateAsync(new ProviderUser + { + ProviderId = provider.Id, + UserId = user1.Id, + Status = ProviderUserStatusType.Confirmed, + Type = ProviderUserType.ServiceUser + }); + + await providerUserRepository.CreateAsync(new ProviderUser + { + ProviderId = provider.Id, + UserId = user2.Id, + Status = ProviderUserStatusType.Confirmed, + Type = ProviderUserType.ProviderAdmin + }); + + var userIds = new[] { user1.Id, user2.Id }; + + var results = (await providerUserRepository.GetManyByManyUsersAsync(userIds)).ToList(); + + Assert.Equal(2, results.Count); + Assert.Contains(results, pu => pu.UserId == user1.Id && pu.Type == ProviderUserType.ServiceUser); + Assert.Contains(results, pu => pu.UserId == user2.Id && pu.Type == ProviderUserType.ProviderAdmin); + } + private static void AssertProviderOrganizationDetails( ProviderUserOrganizationDetails actual, Organization expectedOrganization, @@ -139,4 +419,6 @@ public class ProviderUserRepositoryTests Assert.Equal(expectedProviderUser.Status, actual.Status); Assert.Equal(expectedProviderUser.Type, actual.Type); } + + } diff --git a/util/Migrator/DbScripts/2025-12-03_00_ProviderUserGetManyByUserIds.sql b/util/Migrator/DbScripts/2025-12-03_00_ProviderUserGetManyByUserIds.sql new file mode 100644 index 0000000000..b112e02263 --- /dev/null +++ b/util/Migrator/DbScripts/2025-12-03_00_ProviderUserGetManyByUserIds.sql @@ -0,0 +1,13 @@ +CREATE OR ALTER PROCEDURE [dbo].[ProviderUser_ReadManyByManyUserIds] + @UserIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + SELECT + [pu].* + FROM + [dbo].[ProviderUserView] AS [pu] + INNER JOIN + @UserIds [u] ON [u].[Id] = [pu].[UserId] +END