#nullable enable using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Exceptions; using Bit.Core.Models.Data.Organizations; using Bit.Core.Services; using Bit.Core.Test.AdminConsole.AutoFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.Extensions.Time.Testing; using NSubstitute; using OneOf.Types; using Xunit; using EventType = Bit.Core.Enums.EventType; namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies; public class VNextSavePolicyCommandTests { [Theory, BitAutoData] public async Task SaveAsync_NewPolicy_Success([PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate) { // Arrange var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent(); fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any(), Arg.Any()).Returns(""); var sutProvider = SutProviderFactory([ new FakeSingleOrgDependencyEvent(), fakePolicyValidationEvent ]); var savePolicyModel = new SavePolicyModel(policyUpdate); var newPolicy = new Policy { Type = policyUpdate.Type, OrganizationId = policyUpdate.OrganizationId, Enabled = false }; ArrangeOrganization(sutProvider, policyUpdate); sutProvider.GetDependency().GetManyByOrganizationIdAsync(policyUpdate.OrganizationId).Returns([newPolicy]); var creationDate = sutProvider.GetDependency().Start; // Act await sutProvider.Sut.SaveAsync(savePolicyModel); // Assert await fakePolicyValidationEvent.ValidateAsyncMock .Received(1) .Invoke(Arg.Any(), Arg.Any()); await AssertPolicySavedAsync(sutProvider, policyUpdate); await sutProvider.GetDependency() .Received(1) .UpsertAsync(Arg.Is(p => p.CreationDate == creationDate && p.RevisionDate == creationDate)); } [Theory, BitAutoData] public async Task SaveAsync_ExistingPolicy_Success( [PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate, [Policy(PolicyType.SingleOrg, false)] Policy currentPolicy) { // Arrange var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent(); fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any(), Arg.Any()).Returns(""); var sutProvider = SutProviderFactory([ new FakeSingleOrgDependencyEvent(), fakePolicyValidationEvent ]); var savePolicyModel = new SavePolicyModel(policyUpdate); currentPolicy.OrganizationId = policyUpdate.OrganizationId; sutProvider.GetDependency() .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type) .Returns(currentPolicy); ArrangeOrganization(sutProvider, policyUpdate); sutProvider.GetDependency() .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) .Returns([currentPolicy]); // Act await sutProvider.Sut.SaveAsync(savePolicyModel); // Assert await fakePolicyValidationEvent.ValidateAsyncMock .Received(1) .Invoke(Arg.Any(), currentPolicy); await AssertPolicySavedAsync(sutProvider, policyUpdate); var revisionDate = sutProvider.GetDependency().Start; await sutProvider.GetDependency() .Received(1) .UpsertAsync(Arg.Is(p => p.Id == currentPolicy.Id && p.OrganizationId == currentPolicy.OrganizationId && p.Type == currentPolicy.Type && p.CreationDate == currentPolicy.CreationDate && p.RevisionDate == revisionDate)); } [Theory, BitAutoData] public async Task SaveAsync_OrganizationDoesNotExist_ThrowsBadRequest([PolicyUpdate(PolicyType.ActivateAutofill)] PolicyUpdate policyUpdate) { // Arrange var sutProvider = SutProviderFactory(); var savePolicyModel = new SavePolicyModel(policyUpdate); sutProvider.GetDependency() .GetOrganizationAbilityAsync(policyUpdate.OrganizationId) .Returns(Task.FromResult(null)); // Act var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(savePolicyModel)); // Assert Assert.Contains("Organization not found", badRequestException.Message, StringComparison.OrdinalIgnoreCase); await AssertPolicyNotSavedAsync(sutProvider); } [Theory, BitAutoData] public async Task SaveAsync_OrganizationCannotUsePolicies_ThrowsBadRequest([PolicyUpdate(PolicyType.ActivateAutofill)] PolicyUpdate policyUpdate) { // Arrange var sutProvider = SutProviderFactory(); var savePolicyModel = new SavePolicyModel(policyUpdate); sutProvider.GetDependency() .GetOrganizationAbilityAsync(policyUpdate.OrganizationId) .Returns(new OrganizationAbility { Id = policyUpdate.OrganizationId, UsePolicies = false }); // Act var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(savePolicyModel)); // Assert Assert.Contains("cannot use policies", badRequestException.Message, StringComparison.OrdinalIgnoreCase); await AssertPolicyNotSavedAsync(sutProvider); } [Theory, BitAutoData] public async Task SaveAsync_RequiredPolicyIsNull_Throws( [PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate) { // Arrange var sutProvider = SutProviderFactory( [ new FakeRequireSsoDependencyEvent(), new FakeSingleOrgDependencyEvent() ]); var savePolicyModel = new SavePolicyModel(policyUpdate); var requireSsoPolicy = new Policy { Type = PolicyType.RequireSso, OrganizationId = policyUpdate.OrganizationId, Enabled = false }; ArrangeOrganization(sutProvider, policyUpdate); sutProvider.GetDependency() .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) .Returns([requireSsoPolicy]); // Act var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(savePolicyModel)); // Assert Assert.Contains("Turn on the Single organization policy because it is required for the Require single sign-on authentication policy", badRequestException.Message, StringComparison.OrdinalIgnoreCase); await AssertPolicyNotSavedAsync(sutProvider); } [Theory, BitAutoData] public async Task SaveAsync_RequiredPolicyNotEnabled_Throws( [PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate, [Policy(PolicyType.SingleOrg, false)] Policy singleOrgPolicy) { // Arrange var sutProvider = SutProviderFactory( [ new FakeRequireSsoDependencyEvent(), new FakeSingleOrgDependencyEvent() ]); var savePolicyModel = new SavePolicyModel(policyUpdate); var requireSsoPolicy = new Policy { Type = PolicyType.RequireSso, OrganizationId = policyUpdate.OrganizationId, Enabled = false }; ArrangeOrganization(sutProvider, policyUpdate); sutProvider.GetDependency() .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) .Returns([singleOrgPolicy, requireSsoPolicy]); // Act var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(savePolicyModel)); // Assert Assert.Contains("Turn on the Single organization policy because it is required for the Require single sign-on authentication policy", badRequestException.Message, StringComparison.OrdinalIgnoreCase); await AssertPolicyNotSavedAsync(sutProvider); } [Theory, BitAutoData] public async Task SaveAsync_RequiredPolicyEnabled_Success( [PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate, [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy) { // Arrange var sutProvider = SutProviderFactory( [ new FakeRequireSsoDependencyEvent(), new FakeSingleOrgDependencyEvent() ]); var savePolicyModel = new SavePolicyModel(policyUpdate); var requireSsoPolicy = new Policy { Type = PolicyType.RequireSso, OrganizationId = policyUpdate.OrganizationId, Enabled = false }; ArrangeOrganization(sutProvider, policyUpdate); sutProvider.GetDependency() .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) .Returns([singleOrgPolicy, requireSsoPolicy]); // Act await sutProvider.Sut.SaveAsync(savePolicyModel); // Assert await AssertPolicySavedAsync(sutProvider, policyUpdate); } [Theory, BitAutoData] public async Task SaveAsync_DependentPolicyIsEnabled_Throws( [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate, [Policy(PolicyType.SingleOrg)] Policy currentPolicy, [Policy(PolicyType.RequireSso)] Policy requireSsoPolicy) { // Arrange var sutProvider = SutProviderFactory( [ new FakeRequireSsoDependencyEvent(), new FakeSingleOrgDependencyEvent() ]); var savePolicyModel = new SavePolicyModel(policyUpdate); ArrangeOrganization(sutProvider, policyUpdate); sutProvider.GetDependency() .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) .Returns([currentPolicy, requireSsoPolicy]); // Act var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(savePolicyModel)); // Assert Assert.Contains("Turn off the Require single sign-on authentication policy because it requires the Single organization policy", badRequestException.Message, StringComparison.OrdinalIgnoreCase); await AssertPolicyNotSavedAsync(sutProvider); } [Theory, BitAutoData] public async Task SaveAsync_MultipleDependentPoliciesAreEnabled_Throws( [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate, [Policy(PolicyType.SingleOrg)] Policy currentPolicy, [Policy(PolicyType.RequireSso)] Policy requireSsoPolicy, [Policy(PolicyType.MaximumVaultTimeout)] Policy vaultTimeoutPolicy) { // Arrange var sutProvider = SutProviderFactory( [ new FakeRequireSsoDependencyEvent(), new FakeSingleOrgDependencyEvent(), new FakeVaultTimeoutDependencyEvent() ]); var savePolicyModel = new SavePolicyModel(policyUpdate); ArrangeOrganization(sutProvider, policyUpdate); sutProvider.GetDependency() .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) .Returns([currentPolicy, requireSsoPolicy, vaultTimeoutPolicy]); // Act var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(savePolicyModel)); // Assert Assert.Contains("Turn off all of the policies that require the Single organization policy", badRequestException.Message, StringComparison.OrdinalIgnoreCase); await AssertPolicyNotSavedAsync(sutProvider); } [Theory, BitAutoData] public async Task SaveAsync_DependentPolicyNotEnabled_Success( [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate, [Policy(PolicyType.SingleOrg)] Policy currentPolicy, [Policy(PolicyType.RequireSso, false)] Policy requireSsoPolicy) { // Arrange var sutProvider = SutProviderFactory( [ new FakeRequireSsoDependencyEvent(), new FakeSingleOrgDependencyEvent() ]); var savePolicyModel = new SavePolicyModel(policyUpdate); ArrangeOrganization(sutProvider, policyUpdate); sutProvider.GetDependency() .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) .Returns([currentPolicy, requireSsoPolicy]); // Act await sutProvider.Sut.SaveAsync(savePolicyModel); // Assert await AssertPolicySavedAsync(sutProvider, policyUpdate); } [Theory, BitAutoData] public async Task SaveAsync_ThrowsOnValidationError([PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate) { // Arrange var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent(); fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any(), Arg.Any()).Returns("Validation error!"); var sutProvider = SutProviderFactory([ new FakeSingleOrgDependencyEvent(), fakePolicyValidationEvent ]); var savePolicyModel = new SavePolicyModel(policyUpdate); var singleOrgPolicy = new Policy { Type = PolicyType.SingleOrg, OrganizationId = policyUpdate.OrganizationId, Enabled = false }; ArrangeOrganization(sutProvider, policyUpdate); sutProvider.GetDependency().GetManyByOrganizationIdAsync(policyUpdate.OrganizationId).Returns([singleOrgPolicy]); // Act var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(savePolicyModel)); // Assert Assert.Contains("Validation error!", badRequestException.Message, StringComparison.OrdinalIgnoreCase); await AssertPolicyNotSavedAsync(sutProvider); } /// /// Returns a new SutProvider with the PolicyUpdateEvents registered in the Sut. /// private static SutProvider SutProviderFactory( IEnumerable? policyUpdateEvents = null) { var policyEventHandlerFactory = Substitute.For(); var handlers = policyUpdateEvents ?? []; // Setup factory to return handlers based on type policyEventHandlerFactory.GetHandler(Arg.Any()) .Returns(callInfo => { var policyType = callInfo.Arg(); var handler = handlers.OfType().FirstOrDefault(e => e.Type == policyType); return handler != null ? OneOf.OneOf.FromT0(handler) : OneOf.OneOf.FromT1(new None()); }); policyEventHandlerFactory.GetHandler(Arg.Any()) .Returns(callInfo => { var policyType = callInfo.Arg(); var handler = handlers.OfType().FirstOrDefault(e => e.Type == policyType); return handler != null ? OneOf.OneOf.FromT0(handler) : OneOf.OneOf.FromT1(new None()); }); policyEventHandlerFactory.GetHandler(Arg.Any()) .Returns(new None()); policyEventHandlerFactory.GetHandler(Arg.Any()) .Returns(new None()); return new SutProvider() .WithFakeTimeProvider() .SetDependency(handlers) .SetDependency(policyEventHandlerFactory) .Create(); } private static void ArrangeOrganization(SutProvider sutProvider, PolicyUpdate policyUpdate) { sutProvider.GetDependency() .GetOrganizationAbilityAsync(policyUpdate.OrganizationId) .Returns(new OrganizationAbility { Id = policyUpdate.OrganizationId, UsePolicies = true }); } private static async Task AssertPolicyNotSavedAsync(SutProvider sutProvider) { await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() .UpsertAsync(default!); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() .LogPolicyEventAsync(default, default); } private static async Task AssertPolicySavedAsync(SutProvider sutProvider, PolicyUpdate policyUpdate) { await sutProvider.GetDependency().Received(1).UpsertAsync(ExpectedPolicy()); await sutProvider.GetDependency().Received(1) .LogPolicyEventAsync(ExpectedPolicy(), EventType.Policy_Updated); return; Policy ExpectedPolicy() => Arg.Is( p => p.Type == policyUpdate.Type && p.OrganizationId == policyUpdate.OrganizationId && p.Enabled == policyUpdate.Enabled && p.Data == policyUpdate.Data); } }