diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/Interfaces/IOrganizationHasVerifiedDomainsQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/Interfaces/IOrganizationHasVerifiedDomainsQuery.cs new file mode 100644 index 000000000..b7df7f83e --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/Interfaces/IOrganizationHasVerifiedDomainsQuery.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; + +public interface IOrganizationHasVerifiedDomainsQuery +{ + Task HasVerifiedDomainsAsync(Guid orgId); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/OrganizationHasVerifiedDomainsQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/OrganizationHasVerifiedDomainsQuery.cs new file mode 100644 index 000000000..15a36e4f0 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/OrganizationHasVerifiedDomainsQuery.cs @@ -0,0 +1,10 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; +using Bit.Core.Repositories; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; + +public class OrganizationHasVerifiedDomainsQuery(IOrganizationDomainRepository domainRepository) : IOrganizationHasVerifiedDomainsQuery +{ + public async Task HasVerifiedDomainsAsync(Guid orgId) => + (await domainRepository.GetDomainsByOrganizationIdAsync(orgId)).Any(od => od.VerifiedDate is not null); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs index 8e1a4d573..4a597a290 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; +using Bit.Core.AdminConsole.Services; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -15,6 +18,9 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand private readonly IDnsResolverService _dnsResolverService; private readonly IEventService _eventService; private readonly IGlobalSettings _globalSettings; + private readonly IPolicyService _policyService; + private readonly IFeatureService _featureService; + private readonly IOrganizationService _organizationService; private readonly ILogger _logger; public VerifyOrganizationDomainCommand( @@ -22,12 +28,18 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand IDnsResolverService dnsResolverService, IEventService eventService, IGlobalSettings globalSettings, + IPolicyService policyService, + IFeatureService featureService, + IOrganizationService organizationService, ILogger logger) { _organizationDomainRepository = organizationDomainRepository; _dnsResolverService = dnsResolverService; _eventService = eventService; _globalSettings = globalSettings; + _policyService = policyService; + _featureService = featureService; + _organizationService = organizationService; _logger = logger; } @@ -102,6 +114,8 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand if (await _dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt)) { domain.SetVerifiedDate(); + + await EnableSingleOrganizationPolicyAsync(domain.OrganizationId); } } catch (Exception e) @@ -112,4 +126,13 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand return domain; } + + private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId) + { + if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) + { + await _policyService.SaveAsync( + new Policy { OrganizationId = organizationId, Type = PolicyType.SingleOrg, Enabled = true }, null); + } + } } diff --git a/src/Core/AdminConsole/Services/IOrganizationDomainService.cs b/src/Core/AdminConsole/Services/IOrganizationDomainService.cs index 8ed543f0e..463371c14 100644 --- a/src/Core/AdminConsole/Services/IOrganizationDomainService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationDomainService.cs @@ -4,8 +4,4 @@ public interface IOrganizationDomainService { Task ValidateOrganizationsDomainAsync(); Task OrganizationDomainMaintenanceAsync(); - /// - /// Indicates if the organization has any verified domains. - /// - Task HasVerifiedDomainsAsync(Guid orgId); } diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs index 890042b31..4ce33f3b5 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs @@ -106,12 +106,6 @@ public class OrganizationDomainService : IOrganizationDomainService } } - public async Task HasVerifiedDomainsAsync(Guid orgId) - { - var orgDomains = await _domainRepository.GetDomainsByOrganizationIdAsync(orgId); - return orgDomains.Any(od => od.VerifiedDate != null); - } - private async Task> GetAdminEmailsAsync(Guid organizationId) { var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId); diff --git a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs index 6ab90afe0..072aa8283 100644 --- a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs +++ b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; @@ -32,6 +33,7 @@ public class PolicyService : IPolicyService private readonly IFeatureService _featureService; private readonly ISavePolicyCommand _savePolicyCommand; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; + private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery; public PolicyService( IApplicationCacheService applicationCacheService, @@ -45,7 +47,8 @@ public class PolicyService : IPolicyService ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IFeatureService featureService, ISavePolicyCommand savePolicyCommand, - IRemoveOrganizationUserCommand removeOrganizationUserCommand) + IRemoveOrganizationUserCommand removeOrganizationUserCommand, + IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery) { _applicationCacheService = applicationCacheService; _eventService = eventService; @@ -59,6 +62,7 @@ public class PolicyService : IPolicyService _featureService = featureService; _savePolicyCommand = savePolicyCommand; _removeOrganizationUserCommand = removeOrganizationUserCommand; + _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery; } public async Task SaveAsync(Policy policy, Guid? savingUserId) @@ -239,6 +243,7 @@ public class PolicyService : IPolicyService case PolicyType.SingleOrg: if (!policy.Enabled) { + await HasVerifiedDomainsAsync(org); await RequiredBySsoAsync(org); await RequiredByVaultTimeoutAsync(org); await RequiredByKeyConnectorAsync(org); @@ -279,6 +284,15 @@ public class PolicyService : IPolicyService } } + private async Task HasVerifiedDomainsAsync(Organization org) + { + if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + && await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(org.Id)) + { + throw new BadRequestException("Organization has verified domains."); + } + } + private async Task SetPolicyConfiguration(Policy policy) { await _policyRepository.UpsertAsync(policy); diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 58eb65faf..d11da2119 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -130,6 +130,7 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } private static void AddOrganizationAuthCommands(this IServiceCollection services) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/OrganizationHasVerifiedDomainsQueryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/OrganizationHasVerifiedDomainsQueryTests.cs new file mode 100644 index 000000000..f63f6e48b --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/OrganizationHasVerifiedDomainsQueryTests.cs @@ -0,0 +1,57 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; +using Bit.Core.Entities; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationDomains; + +[SutProviderCustomize] +public class OrganizationHasVerifiedDomainsQueryTests +{ + [Theory, BitAutoData] + public async Task HasVerifiedDomainsAsync_WithVerifiedDomain_ReturnsTrue( + OrganizationDomain organizationDomain, + SutProvider sutProvider) + { + organizationDomain.SetVerifiedDate(); // Set the verified date to make it verified + + sutProvider.GetDependency() + .GetDomainsByOrganizationIdAsync(organizationDomain.OrganizationId) + .Returns(new List { organizationDomain }); + + var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationDomain.OrganizationId); + + Assert.True(result); + } + + [Theory, BitAutoData] + public async Task HasVerifiedDomainsAsync_WithoutVerifiedDomain_ReturnsFalse( + OrganizationDomain organizationDomain, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetDomainsByOrganizationIdAsync(organizationDomain.OrganizationId) + .Returns(new List { organizationDomain }); + + var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationDomain.OrganizationId); + + Assert.False(result); + } + + [Theory, BitAutoData] + public async Task HasVerifiedDomainsAsync_WithoutOrganizationDomains_ReturnsFalse( + Guid organizationId, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetDomainsByOrganizationIdAsync(organizationId) + .Returns(new List()); + + var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationId); + + Assert.False(result); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs index d61ded28b..2fcaf8134 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; +using Bit.Core.AdminConsole.Services; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -15,7 +18,7 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationDomains; public class VerifyOrganizationDomainCommandTests { [Theory, BitAutoData] - public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id, + public async Task UserVerifyOrganizationDomainAsync_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id, SutProvider sutProvider) { var expected = new OrganizationDomain @@ -37,7 +40,7 @@ public class VerifyOrganizationDomainCommandTests } [Theory, BitAutoData] - public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id, + public async Task UserVerifyOrganizationDomainAsync_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id, SutProvider sutProvider) { var expected = new OrganizationDomain @@ -61,7 +64,7 @@ public class VerifyOrganizationDomainCommandTests } [Theory, BitAutoData] - public async Task UserVerifyOrganizationDomain_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id, + public async Task UserVerifyOrganizationDomainAsync_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id, SutProvider sutProvider) { var expected = new OrganizationDomain @@ -91,7 +94,7 @@ public class VerifyOrganizationDomainCommandTests } [Theory, BitAutoData] - public async Task UserVerifyOrganizationDomain_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id, + public async Task UserVerifyOrganizationDomainAsync_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id, SutProvider sutProvider) { var expected = new OrganizationDomain @@ -120,7 +123,7 @@ public class VerifyOrganizationDomainCommandTests [Theory, BitAutoData] - public async Task SystemVerifyOrganizationDomain_CallsEventServiceWithUpdatedJobRunCount(SutProvider sutProvider) + public async Task SystemVerifyOrganizationDomainAsync_CallsEventServiceWithUpdatedJobRunCount(SutProvider sutProvider) { var domain = new OrganizationDomain() { @@ -137,4 +140,97 @@ public class VerifyOrganizationDomainCommandTests .LogOrganizationDomainEventAsync(default, EventType.OrganizationDomain_NotVerified, EventSystemUser.DomainVerification); } + + [Theory, BitAutoData] + public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeEnabled( + OrganizationDomain domain, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetClaimedDomainsByDomainNameAsync(domain.DomainName) + .Returns([]); + + sutProvider.GetDependency() + .ResolveAsync(domain.DomainName, domain.Txt) + .Returns(true); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + + _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); + + await sutProvider.GetDependency() + .Received(1) + .SaveAsync(Arg.Is(x => x.Type == PolicyType.SingleOrg && x.OrganizationId == domain.OrganizationId && x.Enabled), null); + } + + [Theory, BitAutoData] + public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningDisabled_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeNotBeEnabled( + OrganizationDomain domain, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetClaimedDomainsByDomainNameAsync(domain.DomainName) + .Returns([]); + + sutProvider.GetDependency() + .ResolveAsync(domain.DomainName, domain.Txt) + .Returns(true); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(false); + + _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); + + await sutProvider.GetDependency() + .DidNotReceive() + .SaveAsync(Arg.Any(), null); + } + + [Theory, BitAutoData] + public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldNotBeEnabled( + OrganizationDomain domain, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetClaimedDomainsByDomainNameAsync(domain.DomainName) + .Returns([]); + + sutProvider.GetDependency() + .ResolveAsync(domain.DomainName, domain.Txt) + .Returns(false); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + + _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); + + await sutProvider.GetDependency() + .DidNotReceive() + .SaveAsync(Arg.Any(), null); + + } + + [Theory, BitAutoData] + public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningDisabled_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldBeNotBeEnabled( + OrganizationDomain domain, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetClaimedDomainsByDomainNameAsync(domain.DomainName) + .Returns([]); + + sutProvider.GetDependency() + .ResolveAsync(domain.DomainName, domain.Txt) + .Returns(false); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + + _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); + + await sutProvider.GetDependency() + .DidNotReceive() + .SaveAsync(Arg.Any(), null); + } } diff --git a/test/Core.Test/AdminConsole/Services/OrganizationDomainServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationDomainServiceTests.cs index c779e3a1c..210726061 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationDomainServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationDomainServiceTests.cs @@ -76,48 +76,4 @@ public class OrganizationDomainServiceTests await sutProvider.GetDependency().ReceivedWithAnyArgs(1) .DeleteExpiredAsync(7); } - - [Theory, BitAutoData] - public async Task HasVerifiedDomainsAsync_WithVerifiedDomain_ReturnsTrue( - OrganizationDomain organizationDomain, - SutProvider sutProvider) - { - organizationDomain.SetVerifiedDate(); // Set the verified date to make it verified - - sutProvider.GetDependency() - .GetDomainsByOrganizationIdAsync(organizationDomain.OrganizationId) - .Returns(new List { organizationDomain }); - - var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationDomain.OrganizationId); - - Assert.True(result); - } - - [Theory, BitAutoData] - public async Task HasVerifiedDomainsAsync_WithoutVerifiedDomain_ReturnsFalse( - OrganizationDomain organizationDomain, - SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetDomainsByOrganizationIdAsync(organizationDomain.OrganizationId) - .Returns(new List { organizationDomain }); - - var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationDomain.OrganizationId); - - Assert.False(result); - } - - [Theory, BitAutoData] - public async Task HasVerifiedDomainsAsync_WithoutOrganizationDomains_ReturnsFalse( - Guid organizationId, - SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetDomainsByOrganizationIdAsync(organizationId) - .Returns(new List()); - - var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationId); - - Assert.False(result); - } } diff --git a/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs b/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs index f9bc49bbe..da3f2b267 100644 --- a/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services.Implementations; @@ -815,4 +816,32 @@ public class PolicyServiceTests new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.DisableSend, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Invited, IsProvider = true } }); } + + + [Theory, BitAutoData] + public async Task SaveAsync_GivenOrganizationUsingPoliciesAndHasVerifiedDomains_WhenSingleOrgPolicyIsDisabled_ThenAnErrorShouldBeThrownOrganizationHasVerifiedDomains( + [AdminConsoleFixtures.Policy(PolicyType.SingleOrg)] Policy policy, Organization org, SutProvider sutProvider) + { + org.Id = policy.OrganizationId; + org.UsePolicies = true; + + policy.Enabled = false; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + + sutProvider.GetDependency() + .GetByIdAsync(policy.OrganizationId) + .Returns(org); + + sutProvider.GetDependency() + .HasVerifiedDomainsAsync(org.Id) + .Returns(true); + + var badRequestException = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(policy, null)); + + Assert.Equal("Organization has verified domains.", badRequestException.Message); + } }