Skip to content

Commit

Permalink
[PM-10314] Auto-enable Single Org when a Domain is Verified (#4897)
Browse files Browse the repository at this point in the history
Updated domain verification to auto-enable single org policy.
  • Loading branch information
jrmccannon authored Oct 24, 2024
1 parent a128cf1 commit 0c346d6
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 62 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;

public interface IOrganizationHasVerifiedDomainsQuery
{
Task<bool> HasVerifiedDomainsAsync(Guid orgId);
}
Original file line number Diff line number Diff line change
@@ -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<bool> HasVerifiedDomainsAsync(Guid orgId) =>
(await domainRepository.GetDomainsByOrganizationIdAsync(orgId)).Any(od => od.VerifiedDate is not null);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,19 +18,28 @@ 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<VerifyOrganizationDomainCommand> _logger;

public VerifyOrganizationDomainCommand(
IOrganizationDomainRepository organizationDomainRepository,
IDnsResolverService dnsResolverService,
IEventService eventService,
IGlobalSettings globalSettings,
IPolicyService policyService,
IFeatureService featureService,
IOrganizationService organizationService,
ILogger<VerifyOrganizationDomainCommand> logger)
{
_organizationDomainRepository = organizationDomainRepository;
_dnsResolverService = dnsResolverService;
_eventService = eventService;
_globalSettings = globalSettings;
_policyService = policyService;
_featureService = featureService;
_organizationService = organizationService;
_logger = logger;
}

Expand Down Expand Up @@ -102,6 +114,8 @@ private async Task<OrganizationDomain> VerifyOrganizationDomainAsync(Organizatio
if (await _dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt))
{
domain.SetVerifiedDate();

await EnableSingleOrganizationPolicyAsync(domain.OrganizationId);
}
}
catch (Exception e)
Expand All @@ -112,4 +126,13 @@ private async Task<OrganizationDomain> VerifyOrganizationDomainAsync(Organizatio

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);
}
}
}
4 changes: 0 additions & 4 deletions src/Core/AdminConsole/Services/IOrganizationDomainService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,4 @@ public interface IOrganizationDomainService
{
Task ValidateOrganizationsDomainAsync();
Task OrganizationDomainMaintenanceAsync();
/// <summary>
/// Indicates if the organization has any verified domains.
/// </summary>
Task<bool> HasVerifiedDomainsAsync(Guid orgId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,6 @@ await _mailService.SendUnverifiedOrganizationDomainEmailAsync(adminEmails,
}
}

public async Task<bool> HasVerifiedDomainsAsync(Guid orgId)
{
var orgDomains = await _domainRepository.GetDomainsByOrganizationIdAsync(orgId);
return orgDomains.Any(od => od.VerifiedDate != null);
}

private async Task<List<string>> GetAdminEmailsAsync(Guid organizationId)
{
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
Expand Down
16 changes: 15 additions & 1 deletion src/Core/AdminConsole/Services/Implementations/PolicyService.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -45,7 +47,8 @@ public PolicyService(
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IFeatureService featureService,
ISavePolicyCommand savePolicyCommand,
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery)
{
_applicationCacheService = applicationCacheService;
_eventService = eventService;
Expand All @@ -59,6 +62,7 @@ public PolicyService(
_featureService = featureService;
_savePolicyCommand = savePolicyCommand;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
}

public async Task SaveAsync(Policy policy, Guid? savingUserId)
Expand Down Expand Up @@ -239,6 +243,7 @@ private async Task HandleDependentPoliciesAsync(Policy policy, Organization org)
case PolicyType.SingleOrg:
if (!policy.Enabled)
{
await HasVerifiedDomainsAsync(org);
await RequiredBySsoAsync(org);
await RequiredByVaultTimeoutAsync(org);
await RequiredByKeyConnectorAsync(org);
Expand Down Expand Up @@ -279,6 +284,15 @@ private async Task HandleDependentPoliciesAsync(Policy policy, Organization org)
}
}

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ private static void AddOrganizationDomainCommandsQueries(this IServiceCollection
services.AddScoped<IGetOrganizationDomainByIdOrganizationIdQuery, GetOrganizationDomainByIdOrganizationIdQuery>();
services.AddScoped<IGetOrganizationDomainByOrganizationIdQuery, GetOrganizationDomainByOrganizationIdQuery>();
services.AddScoped<IDeleteOrganizationDomainCommand, DeleteOrganizationDomainCommand>();
services.AddScoped<IOrganizationHasVerifiedDomainsQuery, OrganizationHasVerifiedDomainsQuery>();
}

private static void AddOrganizationAuthCommands(this IServiceCollection services)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<OrganizationHasVerifiedDomainsQuery> sutProvider)
{
organizationDomain.SetVerifiedDate(); // Set the verified date to make it verified

sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetDomainsByOrganizationIdAsync(organizationDomain.OrganizationId)
.Returns(new List<OrganizationDomain> { organizationDomain });

var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationDomain.OrganizationId);

Assert.True(result);
}

[Theory, BitAutoData]
public async Task HasVerifiedDomainsAsync_WithoutVerifiedDomain_ReturnsFalse(
OrganizationDomain organizationDomain,
SutProvider<OrganizationHasVerifiedDomainsQuery> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetDomainsByOrganizationIdAsync(organizationDomain.OrganizationId)
.Returns(new List<OrganizationDomain> { organizationDomain });

var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationDomain.OrganizationId);

Assert.False(result);
}

[Theory, BitAutoData]
public async Task HasVerifiedDomainsAsync_WithoutOrganizationDomains_ReturnsFalse(
Guid organizationId,
SutProvider<OrganizationHasVerifiedDomainsQuery> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetDomainsByOrganizationIdAsync(organizationId)
.Returns(new List<OrganizationDomain>());

var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationId);

Assert.False(result);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<VerifyOrganizationDomainCommand> sutProvider)
{
var expected = new OrganizationDomain
Expand All @@ -37,7 +40,7 @@ public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHas
}

[Theory, BitAutoData]
public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id,
public async Task UserVerifyOrganizationDomainAsync_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id,
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{
var expected = new OrganizationDomain
Expand All @@ -61,7 +64,7 @@ public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHas
}

[Theory, BitAutoData]
public async Task UserVerifyOrganizationDomain_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id,
public async Task UserVerifyOrganizationDomainAsync_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id,
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{
var expected = new OrganizationDomain
Expand Down Expand Up @@ -91,7 +94,7 @@ await sutProvider.GetDependency<IEventService>().Received(1)
}

[Theory, BitAutoData]
public async Task UserVerifyOrganizationDomain_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id,
public async Task UserVerifyOrganizationDomainAsync_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id,
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{
var expected = new OrganizationDomain
Expand Down Expand Up @@ -120,7 +123,7 @@ await sutProvider.GetDependency<IEventService>().Received(1)


[Theory, BitAutoData]
public async Task SystemVerifyOrganizationDomain_CallsEventServiceWithUpdatedJobRunCount(SutProvider<VerifyOrganizationDomainCommand> sutProvider)
public async Task SystemVerifyOrganizationDomainAsync_CallsEventServiceWithUpdatedJobRunCount(SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{
var domain = new OrganizationDomain()
{
Expand All @@ -137,4 +140,97 @@ await sutProvider.GetDependency<IEventService>().ReceivedWithAnyArgs(1)
.LogOrganizationDomainEventAsync(default, EventType.OrganizationDomain_NotVerified,
EventSystemUser.DomainVerification);
}

[Theory, BitAutoData]
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeEnabled(
OrganizationDomain domain, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetClaimedDomainsByDomainNameAsync(domain.DomainName)
.Returns([]);

sutProvider.GetDependency<IDnsResolverService>()
.ResolveAsync(domain.DomainName, domain.Txt)
.Returns(true);

sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);

_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);

await sutProvider.GetDependency<IPolicyService>()
.Received(1)
.SaveAsync(Arg.Is<Policy>(x => x.Type == PolicyType.SingleOrg && x.OrganizationId == domain.OrganizationId && x.Enabled), null);
}

[Theory, BitAutoData]
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningDisabled_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeNotBeEnabled(
OrganizationDomain domain, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetClaimedDomainsByDomainNameAsync(domain.DomainName)
.Returns([]);

sutProvider.GetDependency<IDnsResolverService>()
.ResolveAsync(domain.DomainName, domain.Txt)
.Returns(true);

sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(false);

_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);

await sutProvider.GetDependency<IPolicyService>()
.DidNotReceive()
.SaveAsync(Arg.Any<Policy>(), null);
}

[Theory, BitAutoData]
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldNotBeEnabled(
OrganizationDomain domain, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetClaimedDomainsByDomainNameAsync(domain.DomainName)
.Returns([]);

sutProvider.GetDependency<IDnsResolverService>()
.ResolveAsync(domain.DomainName, domain.Txt)
.Returns(false);

sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);

_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);

await sutProvider.GetDependency<IPolicyService>()
.DidNotReceive()
.SaveAsync(Arg.Any<Policy>(), null);

}

[Theory, BitAutoData]
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningDisabled_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldBeNotBeEnabled(
OrganizationDomain domain, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetClaimedDomainsByDomainNameAsync(domain.DomainName)
.Returns([]);

sutProvider.GetDependency<IDnsResolverService>()
.ResolveAsync(domain.DomainName, domain.Txt)
.Returns(false);

sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);

_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);

await sutProvider.GetDependency<IPolicyService>()
.DidNotReceive()
.SaveAsync(Arg.Any<Policy>(), null);
}
}
Loading

0 comments on commit 0c346d6

Please sign in to comment.