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:
-///
-/// - Sets the UseAutomaticUserConfirmation organization feature to match the policy update
-///
///
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