diff --git a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs index a83eccc301..9abb9cd618 100644 --- a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs +++ b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs @@ -89,7 +89,21 @@ public class PolicyService : IPolicyService private async Task> QueryOrganizationUserPolicyDetailsAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted) { var organizationUserPolicyDetails = await _organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(userId, policyType); - var excludedUserTypes = GetUserTypesExcludedFromPolicy(policyType); + + OrganizationUserType[] excludedUserTypes; + + if (policyType == PolicyType.SingleOrg + && _featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers) + && await GetPoliciesApplicableToUserAsync(userId, PolicyType.AutomaticUserConfirmation, OrganizationUserStatusType.Revoked) is { Count: > 0 }) + { + minStatus = OrganizationUserStatusType.Revoked; + excludedUserTypes = []; + } + else + { + excludedUserTypes = GetUserTypesExcludedFromPolicy(policyType); + } + var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); return organizationUserPolicyDetails.Where(o => (!orgAbilities.TryGetValue(o.OrganizationId, out var orgAbility) || orgAbility.UsePolicies) && diff --git a/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs b/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs index 0af9eef12e..33f4841eb3 100644 --- a/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs @@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services.Implementations; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; @@ -123,6 +124,212 @@ public class PolicyServiceTests Assert.True(result); } + [Theory, BitAutoData] + public async Task GetPoliciesApplicableToUserAsync_WithAutoConfirmEnabled_WithSingleOrgPolicy_IncludesRevokedUsers( + Guid userId, + SutProvider sutProvider) + { + // Arrange - Setup SingleOrg policy with Revoked user + sutProvider.GetDependency() + .GetByUserIdWithPolicyDetailsAsync(userId, PolicyType.SingleOrg) + .Returns(new List + { + new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.SingleOrg, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Revoked, IsProvider = false }, + new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.SingleOrg, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.Owner, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = false } + }); + + // Enable AutomaticConfirmUsers feature flag + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers) + .Returns(true); + + // Setup recursive call - user has AutomaticUserConfirmation policy + var autoConfirmPolicies = new List + { + new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.AutomaticUserConfirmation, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Revoked, IsProvider = false } + }; + sutProvider.GetDependency() + .GetByUserIdWithPolicyDetailsAsync(userId, PolicyType.AutomaticUserConfirmation) + .Returns(autoConfirmPolicies); + + sutProvider.GetDependency() + .GetOrganizationAbilitiesAsync() + .Returns(Task.FromResult>( + new Dictionary())); + + // Act + var result = await sutProvider.Sut + .GetPoliciesApplicableToUserAsync(userId, PolicyType.SingleOrg); + + // Assert - Should include Revoked user because auto-confirm is enabled + Assert.Equal(2, result.Count()); + Assert.Contains(result, p => p.OrganizationUserStatus == OrganizationUserStatusType.Revoked); + Assert.Contains(result, p => p.OrganizationUserType == OrganizationUserType.Owner); + } + + [Theory, BitAutoData] + public async Task GetPoliciesApplicableToUserAsync_WithAutoConfirmEnabled_WithSingleOrgPolicy_IncludesOwnerAndAdmin( + Guid userId, + SutProvider sutProvider) + { + // Arrange - Setup SingleOrg policy with Owner and Admin users (normally excluded from SingleOrg) + sutProvider.GetDependency() + .GetByUserIdWithPolicyDetailsAsync(userId, PolicyType.SingleOrg) + .Returns(new List + { + new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.SingleOrg, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.Owner, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = false }, + new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.SingleOrg, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.Admin, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = false }, + new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.SingleOrg, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = false } + }); + + // Enable AutomaticConfirmUsers feature flag + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers) + .Returns(true); + + // Setup recursive call - user has AutomaticUserConfirmation policy + var autoConfirmPolicies = new List + { + new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.AutomaticUserConfirmation, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = false } + }; + sutProvider.GetDependency() + .GetByUserIdWithPolicyDetailsAsync(userId, PolicyType.AutomaticUserConfirmation) + .Returns(autoConfirmPolicies); + + sutProvider.GetDependency() + .GetOrganizationAbilitiesAsync() + .Returns(Task.FromResult>( + new Dictionary())); + + // Act + var result = await sutProvider.Sut + .GetPoliciesApplicableToUserAsync(userId, PolicyType.SingleOrg); + + // Assert - Should include Owner and Admin because excludedUserTypes is empty when auto-confirm is enabled + Assert.Equal(3, result.Count()); + Assert.Contains(result, p => p.OrganizationUserType == OrganizationUserType.Owner); + Assert.Contains(result, p => p.OrganizationUserType == OrganizationUserType.Admin); + Assert.Contains(result, p => p.OrganizationUserType == OrganizationUserType.User); + } + + [Theory, BitAutoData] + public async Task GetPoliciesApplicableToUserAsync_WithAutoConfirmDisabled_WithSingleOrgPolicy_ExcludesRevokedUsers( + Guid userId, + SutProvider sutProvider) + { + // Arrange - Setup SingleOrg policy with Revoked and Confirmed users + sutProvider.GetDependency() + .GetByUserIdWithPolicyDetailsAsync(userId, PolicyType.SingleOrg) + .Returns(new List + { + new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.SingleOrg, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Revoked, IsProvider = false }, + new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.SingleOrg, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = false } + }); + + // Disable AutomaticConfirmUsers feature flag + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers) + .Returns(false); + + sutProvider.GetDependency() + .GetOrganizationAbilitiesAsync() + .Returns(Task.FromResult>( + new Dictionary())); + + // Act + var result = await sutProvider.Sut + .GetPoliciesApplicableToUserAsync(userId, PolicyType.SingleOrg); + + // Assert - Should NOT include Revoked user because feature flag is disabled + Assert.Single(result); + Assert.DoesNotContain(result, p => p.OrganizationUserStatus == OrganizationUserStatusType.Revoked); + Assert.All(result, p => Assert.True(p.OrganizationUserStatus >= OrganizationUserStatusType.Accepted)); + } + + [Theory, BitAutoData] + public async Task GetPoliciesApplicableToUserAsync_WithAutoConfirmEnabled_NoAutoConfirmPolicy_ExcludesOwnerAndAdmin( + Guid userId, + SutProvider sutProvider) + { + // Arrange - Setup SingleOrg policy with Owner, Admin, and User + sutProvider.GetDependency() + .GetByUserIdWithPolicyDetailsAsync(userId, PolicyType.SingleOrg) + .Returns(new List + { + new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.SingleOrg, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.Owner, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = false }, + new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.SingleOrg, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.Admin, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = false }, + new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.SingleOrg, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = false } + }); + + // Enable AutomaticConfirmUsers feature flag + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers) + .Returns(true); + + // Setup recursive call - user has NO AutomaticUserConfirmation policy + sutProvider.GetDependency() + .GetByUserIdWithPolicyDetailsAsync(userId, PolicyType.AutomaticUserConfirmation) + .Returns(new List()); + + sutProvider.GetDependency() + .GetOrganizationAbilitiesAsync() + .Returns(Task.FromResult>( + new Dictionary())); + + // Act + var result = await sutProvider.Sut + .GetPoliciesApplicableToUserAsync(userId, PolicyType.SingleOrg); + + // Assert - Should NOT include Owner/Admin because user doesn't have auto-confirm policy + Assert.Single(result); + Assert.DoesNotContain(result, p => p.OrganizationUserType == OrganizationUserType.Owner); + Assert.DoesNotContain(result, p => p.OrganizationUserType == OrganizationUserType.Admin); + Assert.All(result, p => Assert.Equal(OrganizationUserType.User, p.OrganizationUserType)); + } + + [Theory, BitAutoData] + public async Task GetPoliciesApplicableToUserAsync_WithNonSingleOrgPolicy_IgnoresAutoConfirmSettings( + Guid userId, + SutProvider sutProvider) + { + // Arrange - Setup DisableSend policy (not SingleOrg) + sutProvider.GetDependency() + .GetByUserIdWithPolicyDetailsAsync(userId, PolicyType.DisableSend) + .Returns(new List + { + new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.DisableSend, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Revoked, IsProvider = false }, + new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.DisableSend, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = false } + }); + + // Enable AutomaticConfirmUsers feature flag + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers) + .Returns(true); + + // User has AutomaticUserConfirmation policy (but we're querying DisableSend, not SingleOrg) + var autoConfirmPolicies = new List + { + new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.AutomaticUserConfirmation, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = false } + }; + sutProvider.GetDependency() + .GetByUserIdWithPolicyDetailsAsync(userId, PolicyType.AutomaticUserConfirmation) + .Returns(autoConfirmPolicies); + + sutProvider.GetDependency() + .GetOrganizationAbilitiesAsync() + .Returns(Task.FromResult>( + new Dictionary())); + + // Act + var result = await sutProvider.Sut + .GetPoliciesApplicableToUserAsync(userId, PolicyType.DisableSend); + + // Assert - Should NOT include Revoked user because auto-confirm only applies to SingleOrg policy + Assert.Single(result); + Assert.DoesNotContain(result, p => p.OrganizationUserStatus == OrganizationUserStatusType.Revoked); + Assert.All(result, p => Assert.Equal(OrganizationUserStatusType.Confirmed, p.OrganizationUserStatus)); + } + [Theory, BitAutoData] public async Task GetMasterPasswordPolicyForUserAsync_WithFeatureFlagEnabled_EvaluatesPolicyRequirement(User user, SutProvider sutProvider) {