diff --git a/src/Core/Auth/Services/Implementations/SsoConfigService.cs b/src/Core/Auth/Services/Implementations/SsoConfigService.cs index fb90d5d8d432..a2cb906f6f36 100644 --- a/src/Core/Auth/Services/Implementations/SsoConfigService.cs +++ b/src/Core/Auth/Services/Implementations/SsoConfigService.cs @@ -63,9 +63,16 @@ public async Task SaveAsync(SsoConfig config, Organization organization) 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 a8411e41982e..6b1009093c1c 100644 --- a/src/Core/Services/Implementations/PolicyService.cs +++ b/src/Core/Services/Implementations/PolicyService.cs @@ -61,6 +61,7 @@ public async Task SaveAsync(Policy policy, IUserService userService, IOrganizati await RequiredBySsoAsync(org); await RequiredByVaultTimeoutAsync(org); await RequiredByKeyConnectorAsync(org); + await RequiredByAccountRecoveryAsync(org); } break; @@ -80,6 +81,11 @@ public async Task SaveAsync(Policy policy, IUserService userService, IOrganizati { await RequiredBySsoTrustedDeviceEncryptionAsync(org); } + + if (policy.Enabled) + { + await DependsOnSingleOrgAsync(org); + } break; case PolicyType.MaximumVaultTimeout: @@ -244,6 +250,15 @@ private async Task RequiredByKeyConnectorAsync(Organization org) } } + 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 fdc8217ba8ed..7886d6f9e817 100644 --- a/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs +++ b/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs @@ -6,7 +6,9 @@ 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 async Task SaveAsync_KeyConnector_Success(SutProvider s 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 3b4d9b1b80c5..6c1218c84a7f 100644 --- a/test/Core.Test/Services/PolicyServiceTests.cs +++ b/test/Core.Test/Services/PolicyServiceTests.cs @@ -217,6 +217,10 @@ public async Task SaveAsync_NewPolicy_Created( 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 @@ await sutProvider.GetDependency() .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) {