From a095e02e86a36abb0713db9d4c032eef1ea2127a Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Tue, 18 Jul 2023 08:00:49 -0700 Subject: [PATCH] [AC-1435] Single Organization policy prerequisite for Account Recovery policy (#3082) * [AC-1435] Automatically enable Single Org policy when selecting TDE * [AC-1435] Add test for automatic policy enablement * [AC-1435] Prevent disabling single org when account recovery is enabled * [AC-1435] Require Single Org policy when enabling Account recovery * [AC-1435] Add unit test to check for account recovery policy when attempting to disable single org * [AC-1435] Add test to verify single org policy is enabled for account recovery policy * [AC-1435] Fix failing test --- .../Implementations/SsoConfigService.cs | 9 ++- .../Services/Implementations/PolicyService.cs | 15 ++++ .../Auth/Services/SsoConfigServiceTests.cs | 38 +++++++++++ test/Core.Test/Services/PolicyServiceTests.cs | 68 +++++++++++++++++++ 4 files changed, 129 insertions(+), 1 deletion(-) diff --git a/src/Core/Auth/Services/Implementations/SsoConfigService.cs b/src/Core/Auth/Services/Implementations/SsoConfigService.cs index fb90d5d8d..a2cb906f6 100644 --- a/src/Core/Auth/Services/Implementations/SsoConfigService.cs +++ b/src/Core/Auth/Services/Implementations/SsoConfigService.cs @@ -63,9 +63,16 @@ public class SsoConfigService : ISsoConfigService throw new BadRequestException("Key Connector cannot be disabled at this moment."); } - // Automatically enable reset password policy if trusted device encryption is selected + // Automatically enable account recovery and single org policies if trusted device encryption is selected if (config.GetData().MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption) { + var singleOrgPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.SingleOrg) ?? + new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.SingleOrg }; + + singleOrgPolicy.Enabled = true; + + await _policyService.SaveAsync(singleOrgPolicy, _userService, _organizationService, null); + var resetPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.ResetPassword) ?? new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.ResetPassword, }; diff --git a/src/Core/Services/Implementations/PolicyService.cs b/src/Core/Services/Implementations/PolicyService.cs index a8411e419..6b1009093 100644 --- a/src/Core/Services/Implementations/PolicyService.cs +++ b/src/Core/Services/Implementations/PolicyService.cs @@ -61,6 +61,7 @@ public class PolicyService : IPolicyService await RequiredBySsoAsync(org); await RequiredByVaultTimeoutAsync(org); await RequiredByKeyConnectorAsync(org); + await RequiredByAccountRecoveryAsync(org); } break; @@ -80,6 +81,11 @@ public class PolicyService : IPolicyService { await RequiredBySsoTrustedDeviceEncryptionAsync(org); } + + if (policy.Enabled) + { + await DependsOnSingleOrgAsync(org); + } break; case PolicyType.MaximumVaultTimeout: @@ -244,6 +250,15 @@ public class PolicyService : IPolicyService } } + private async Task RequiredByAccountRecoveryAsync(Organization org) + { + var requireSso = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.ResetPassword); + if (requireSso?.Enabled == true) + { + throw new BadRequestException("Account recovery policy is enabled."); + } + } + private async Task RequiredByVaultTimeoutAsync(Organization org) { var vaultTimeout = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.MaximumVaultTimeout); diff --git a/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs b/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs index fdc8217ba..7886d6f9e 100644 --- a/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs +++ b/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs @@ -6,7 +6,9 @@ using Bit.Core.Auth.Services; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Models.Data.Organizations.Policies; using Bit.Core.Repositories; +using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -317,4 +319,40 @@ public class SsoConfigServiceTests await sutProvider.GetDependency().ReceivedWithAnyArgs() .UpsertAsync(default); } + + [Theory, BitAutoData] + public async Task SaveAsync_Tde_Enable_Required_Policies(SutProvider sutProvider, Organization organization) + { + var ssoConfig = new SsoConfig + { + Id = default, + Data = new SsoConfigurationData + { + MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption, + }.Serialize(), + Enabled = true, + OrganizationId = organization.Id, + }; + + await sutProvider.Sut.SaveAsync(ssoConfig, organization); + + await sutProvider.GetDependency().Received(1) + .SaveAsync( + Arg.Is(t => t.Type == Enums.PolicyType.SingleOrg), + Arg.Any(), + Arg.Any(), + null + ); + + await sutProvider.GetDependency().Received(1) + .SaveAsync( + Arg.Is(t => t.Type == Enums.PolicyType.ResetPassword && t.GetDataModel().AutoEnrollEnabled), + Arg.Any(), + Arg.Any(), + null + ); + + await sutProvider.GetDependency().ReceivedWithAnyArgs() + .UpsertAsync(default); + } } diff --git a/test/Core.Test/Services/PolicyServiceTests.cs b/test/Core.Test/Services/PolicyServiceTests.cs index 3b4d9b1b8..6c1218c84 100644 --- a/test/Core.Test/Services/PolicyServiceTests.cs +++ b/test/Core.Test/Services/PolicyServiceTests.cs @@ -217,6 +217,10 @@ public class PolicyServiceTests UsePolicies = true, }); + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policy.OrganizationId, Enums.PolicyType.SingleOrg) + .Returns(Task.FromResult(new Policy { Enabled = true })); + var utcNow = DateTime.UtcNow; await sutProvider.Sut.SaveAsync(policy, Substitute.For(), Substitute.For(), Guid.NewGuid()); @@ -444,6 +448,70 @@ public class PolicyServiceTests .LogPolicyEventAsync(default, default, default); } + [Theory, BitAutoData] + public async Task SaveAsync_PolicyRequiredForAccountRecovery_NotEnabled_ThrowsBadRequestAsync( + [PolicyFixtures.Policy(Enums.PolicyType.ResetPassword)] Policy policy, SutProvider sutProvider) + { + policy.Enabled = true; + policy.SetDataModel(new ResetPasswordDataModel()); + + SetupOrg(sutProvider, policy.OrganizationId, new Organization + { + Id = policy.OrganizationId, + UsePolicies = true, + }); + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.SingleOrg) + .Returns(Task.FromResult(new Policy { Enabled = false })); + + var badRequestException = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(policy, + Substitute.For(), + Substitute.For(), + Guid.NewGuid())); + + Assert.Contains("Single Organization policy not enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogPolicyEventAsync(default, default, default); + } + + + [Theory, BitAutoData] + public async Task SaveAsync_SingleOrg_AccountRecoveryEnabled_ThrowsBadRequest( + [PolicyFixtures.Policy(Enums.PolicyType.SingleOrg)] Policy policy, SutProvider sutProvider) + { + policy.Enabled = false; + + SetupOrg(sutProvider, policy.OrganizationId, new Organization + { + Id = policy.OrganizationId, + UsePolicies = true, + }); + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policy.OrganizationId, Enums.PolicyType.ResetPassword) + .Returns(new Policy { Enabled = true }); + + var badRequestException = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(policy, + Substitute.For(), + Substitute.For(), + Guid.NewGuid())); + + Assert.Contains("Account recovery policy is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertAsync(default); + } + [Theory, BitAutoData] public async Task GetPoliciesApplicableToUserAsync_WithRequireSsoTypeFilter_WithDefaultOrganizationUserStatusFilter_ReturnsNoPolicies(Guid userId, SutProvider sutProvider) {