1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 07:36:14 -05:00

[PM-13322] [BEEEP] Add PolicyValidators and refactor policy save logic (#4877)

This commit is contained in:
Thomas Rittson
2024-10-22 09:18:34 +10:00
committed by GitHub
parent 75cc907785
commit dfa411131d
27 changed files with 1564 additions and 6 deletions

View File

@ -0,0 +1,43 @@
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using NSubstitute;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;
public class FakeSingleOrgPolicyValidator : IPolicyValidator
{
public PolicyType Type => PolicyType.SingleOrg;
public IEnumerable<PolicyType> RequiredPolicies => Array.Empty<PolicyType>();
public readonly Func<PolicyUpdate, Policy?, Task<string>> ValidateAsyncMock = Substitute.For<Func<PolicyUpdate, Policy?, Task<string>>>();
public readonly Action<PolicyUpdate, Policy?> OnSaveSideEffectsAsyncMock = Substitute.For<Action<PolicyUpdate, Policy?>>();
public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
{
return ValidateAsyncMock(policyUpdate, currentPolicy);
}
public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
{
OnSaveSideEffectsAsyncMock(policyUpdate, currentPolicy);
return Task.FromResult(0);
}
}
public class FakeRequireSsoPolicyValidator : IPolicyValidator
{
public PolicyType Type => PolicyType.RequireSso;
public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];
public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult("");
public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(0);
}
public class FakeVaultTimeoutPolicyValidator : IPolicyValidator
{
public PolicyType Type => PolicyType.MaximumVaultTimeout;
public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];
public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult("");
public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(0);
}

View File

@ -0,0 +1,64 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;
public class PolicyValidatorHelpersTests
{
[Fact]
public void ValidateDecryptionOptionsNotEnabled_RequiredByKeyConnector_ValidationError()
{
var ssoConfig = new SsoConfig();
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });
var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]);
Assert.Contains("Key Connector is enabled", result);
}
[Fact]
public void ValidateDecryptionOptionsNotEnabled_RequiredByTDE_ValidationError()
{
var ssoConfig = new SsoConfig();
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });
var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.TrustedDeviceEncryption]);
Assert.Contains("Trusted device encryption is on", result);
}
[Fact]
public void ValidateDecryptionOptionsNotEnabled_NullSsoConfig_NoValidationError()
{
var ssoConfig = new SsoConfig();
var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]);
Assert.True(string.IsNullOrEmpty(result));
}
[Fact]
public void ValidateDecryptionOptionsNotEnabled_RequiredOptionNotEnabled_NoValidationError()
{
var ssoConfig = new SsoConfig();
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });
var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.TrustedDeviceEncryption]);
Assert.True(string.IsNullOrEmpty(result));
}
[Fact]
public void ValidateDecryptionOptionsNotEnabled_SsoConfigDisabled_NoValidationError()
{
var ssoConfig = new SsoConfig();
ssoConfig.Enabled = false;
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });
var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]);
Assert.True(string.IsNullOrEmpty(result));
}
}

View File

@ -0,0 +1,75 @@
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
[SutProviderCustomize]
public class RequireSsoPolicyValidatorTests
{
[Theory, BitAutoData]
public async Task ValidateAsync_DisablingPolicy_KeyConnectorEnabled_ValidationError(
[PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.SingleOrg)] Policy policy,
SutProvider<RequireSsoPolicyValidator> sutProvider)
{
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = true };
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);
Assert.Contains("Key Connector is enabled", result, StringComparison.OrdinalIgnoreCase);
}
[Theory, BitAutoData]
public async Task ValidateAsync_DisablingPolicy_TdeEnabled_ValidationError(
[PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.SingleOrg)] Policy policy,
SutProvider<RequireSsoPolicyValidator> sutProvider)
{
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = true };
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);
Assert.Contains("Trusted device encryption is on", result, StringComparison.OrdinalIgnoreCase);
}
[Theory, BitAutoData]
public async Task ValidateAsync_DisablingPolicy_DecryptionOptionsNotEnabled_Success(
[PolicyUpdate(PolicyType.ResetPassword, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.ResetPassword)] Policy policy,
SutProvider<RequireSsoPolicyValidator> sutProvider)
{
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = false };
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);
Assert.True(string.IsNullOrEmpty(result));
}
}

View File

@ -0,0 +1,71 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
[SutProviderCustomize]
public class ResetPasswordPolicyValidatorTests
{
[Theory]
[BitAutoData(true, false)]
[BitAutoData(false, true)]
[BitAutoData(false, false)]
public async Task ValidateAsync_DisablingPolicy_TdeEnabled_ValidationError(
bool policyEnabled,
bool autoEnrollEnabled,
[PolicyUpdate(PolicyType.ResetPassword)] PolicyUpdate policyUpdate,
[Policy(PolicyType.ResetPassword)] Policy policy,
SutProvider<ResetPasswordPolicyValidator> sutProvider)
{
policyUpdate.Enabled = policyEnabled;
policyUpdate.SetDataModel(new ResetPasswordDataModel
{
AutoEnrollEnabled = autoEnrollEnabled
});
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = true };
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);
Assert.Contains("Trusted device encryption is on and requires this policy.", result, StringComparison.OrdinalIgnoreCase);
}
[Theory, BitAutoData]
public async Task ValidateAsync_DisablingPolicy_TdeNotEnabled_Success(
[PolicyUpdate(PolicyType.ResetPassword, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.ResetPassword)] Policy policy,
SutProvider<ResetPasswordPolicyValidator> sutProvider)
{
policyUpdate.SetDataModel(new ResetPasswordDataModel
{
AutoEnrollEnabled = false
});
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = false };
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);
Assert.True(string.IsNullOrEmpty(result));
}
}

View File

@ -0,0 +1,129 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
[SutProviderCustomize]
public class SingleOrgPolicyValidatorTests
{
[Theory, BitAutoData]
public async Task ValidateAsync_DisablingPolicy_KeyConnectorEnabled_ValidationError(
[PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.SingleOrg)] Policy policy,
SutProvider<SingleOrgPolicyValidator> sutProvider)
{
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = true };
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);
Assert.Contains("Key Connector is enabled", result, StringComparison.OrdinalIgnoreCase);
}
[Theory, BitAutoData]
public async Task ValidateAsync_DisablingPolicy_KeyConnectorNotEnabled_Success(
[PolicyUpdate(PolicyType.ResetPassword, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.ResetPassword)] Policy policy,
SutProvider<SingleOrgPolicyValidator> sutProvider)
{
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = false };
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);
Assert.True(string.IsNullOrEmpty(result));
}
[Theory, BitAutoData]
public async Task OnSaveSideEffectsAsync_RemovesNonCompliantUsers(
[PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,
[Policy(PolicyType.SingleOrg, false)] Policy policy,
Guid savingUserId,
Guid nonCompliantUserId,
Organization organization, SutProvider<SingleOrgPolicyValidator> sutProvider)
{
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
var compliantUser1 = new OrganizationUserUserDetails
{
OrganizationId = organization.Id,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Confirmed,
UserId = new Guid(),
Email = "user1@example.com"
};
var compliantUser2 = new OrganizationUserUserDetails
{
OrganizationId = organization.Id,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Confirmed,
UserId = new Guid(),
Email = "user2@example.com"
};
var nonCompliantUser = new OrganizationUserUserDetails
{
OrganizationId = organization.Id,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Confirmed,
UserId = nonCompliantUserId,
Email = "user3@example.com"
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([compliantUser1, compliantUser2, nonCompliantUser]);
var otherOrganizationUser = new OrganizationUser
{
OrganizationId = new Guid(),
UserId = nonCompliantUserId,
Status = OrganizationUserStatusType.Confirmed
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(nonCompliantUserId)))
.Returns([otherOrganizationUser]);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(savingUserId);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(policyUpdate.OrganizationId).Returns(organization);
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>()
.Received(1)
.RemoveUserAsync(policyUpdate.OrganizationId, nonCompliantUser.Id, savingUserId);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(),
"user3@example.com");
}
}

View File

@ -0,0 +1,209 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
[SutProviderCustomize]
public class TwoFactorAuthenticationPolicyValidatorTests
{
[Theory, BitAutoData]
public async Task OnSaveSideEffectsAsync_RemovesNonCompliantUsers(
Organization organization,
[PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate,
[Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy,
SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider)
{
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var orgUserDetailUserInvited = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Invited,
Type = OrganizationUserType.User,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "user1@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = false
};
var orgUserDetailUserAcceptedWith2FA = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Accepted,
Type = OrganizationUserType.User,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "user2@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = true
};
var orgUserDetailUserAcceptedWithout2FA = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Accepted,
Type = OrganizationUserType.User,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "user3@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = true
};
var orgUserDetailAdmin = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Admin,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "admin@test.com",
Name = "ADMIN",
UserId = Guid.NewGuid(),
HasMasterPassword = false
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns(new List<OrganizationUserUserDetails>
{
orgUserDetailUserInvited,
orgUserDetailUserAcceptedWith2FA,
orgUserDetailUserAcceptedWithout2FA,
orgUserDetailAdmin
});
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())
.Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>()
{
(orgUserDetailUserInvited, false),
(orgUserDetailUserAcceptedWith2FA, true),
(orgUserDetailUserAcceptedWithout2FA, false),
(orgUserDetailAdmin, false),
});
var savingUserId = Guid.NewGuid();
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(savingUserId);
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);
var removeOrganizationUserCommand = sutProvider.GetDependency<IRemoveOrganizationUserCommand>();
await removeOrganizationUserCommand.Received()
.RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWithout2FA.Id, savingUserId);
await sutProvider.GetDependency<IMailService>().Received()
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWithout2FA.Email);
await removeOrganizationUserCommand.DidNotReceive()
.RemoveUserAsync(policy.OrganizationId, orgUserDetailUserInvited.Id, savingUserId);
await sutProvider.GetDependency<IMailService>().DidNotReceive()
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserInvited.Email);
await removeOrganizationUserCommand.DidNotReceive()
.RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWith2FA.Id, savingUserId);
await sutProvider.GetDependency<IMailService>().DidNotReceive()
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWith2FA.Email);
await removeOrganizationUserCommand.DidNotReceive()
.RemoveUserAsync(policy.OrganizationId, orgUserDetailAdmin.Id, savingUserId);
await sutProvider.GetDependency<IMailService>().DidNotReceive()
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailAdmin.Email);
}
[Theory, BitAutoData]
public async Task OnSaveSideEffectsAsync_UsersToBeRemovedDontHaveMasterPasswords_Throws(
Organization organization,
[PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate,
[Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy,
SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider)
{
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
var orgUserDetailUserWith2FAAndMP = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "user1@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = true
};
var orgUserDetailUserWith2FANoMP = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "user2@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = false
};
var orgUserDetailUserWithout2FA = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "user3@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = false
};
var orgUserDetailAdmin = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Admin,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "admin@test.com",
Name = "ADMIN",
UserId = Guid.NewGuid(),
HasMasterPassword = false
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policy.OrganizationId)
.Returns(new List<OrganizationUserUserDetails>
{
orgUserDetailUserWith2FAAndMP,
orgUserDetailUserWith2FANoMP,
orgUserDetailUserWithout2FA,
orgUserDetailAdmin
});
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids =>
ids.Contains(orgUserDetailUserWith2FANoMP.UserId.Value)
&& ids.Contains(orgUserDetailUserWithout2FA.UserId.Value)
&& ids.Contains(orgUserDetailAdmin.UserId.Value)))
.Returns(new List<(Guid userId, bool hasTwoFactor)>()
{
(orgUserDetailUserWith2FANoMP.UserId.Value, true),
(orgUserDetailUserWithout2FA.UserId.Value, false),
(orgUserDetailAdmin.UserId.Value, false),
});
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy));
Assert.Contains("Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>().DidNotReceiveWithAnyArgs()
.RemoveUserAsync(organizationId: default, organizationUserId: default, deletingUserId: default);
}
}

View File

@ -0,0 +1,330 @@
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
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 Xunit;
using EventType = Bit.Core.Enums.EventType;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;
public class SavePolicyCommandTests
{
[Theory, BitAutoData]
public async Task SaveAsync_NewPolicy_Success([PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate)
{
var fakePolicyValidator = new FakeSingleOrgPolicyValidator();
fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns("");
var sutProvider = SutProviderFactory([fakePolicyValidator]);
ArrangeOrganization(sutProvider, policyUpdate);
sutProvider.GetDependency<IPolicyRepository>().GetManyByOrganizationIdAsync(policyUpdate.OrganizationId).Returns([]);
var creationDate = sutProvider.GetDependency<FakeTimeProvider>().Start;
await sutProvider.Sut.SaveAsync(policyUpdate);
await fakePolicyValidator.ValidateAsyncMock.Received(1).Invoke(policyUpdate, null);
fakePolicyValidator.OnSaveSideEffectsAsyncMock.Received(1).Invoke(policyUpdate, null);
await AssertPolicySavedAsync(sutProvider, policyUpdate);
await sutProvider.GetDependency<IPolicyRepository>().Received(1).UpsertAsync(Arg.Is<Policy>(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)
{
var fakePolicyValidator = new FakeSingleOrgPolicyValidator();
fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns("");
var sutProvider = SutProviderFactory([fakePolicyValidator]);
currentPolicy.OrganizationId = policyUpdate.OrganizationId;
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)
.Returns(currentPolicy);
ArrangeOrganization(sutProvider, policyUpdate);
sutProvider.GetDependency<IPolicyRepository>()
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns([currentPolicy]);
// Store mutable properties separately to assert later
var id = currentPolicy.Id;
var organizationId = currentPolicy.OrganizationId;
var type = currentPolicy.Type;
var creationDate = currentPolicy.CreationDate;
var revisionDate = sutProvider.GetDependency<FakeTimeProvider>().Start;
await sutProvider.Sut.SaveAsync(policyUpdate);
await fakePolicyValidator.ValidateAsyncMock.Received(1).Invoke(policyUpdate, currentPolicy);
fakePolicyValidator.OnSaveSideEffectsAsyncMock.Received(1).Invoke(policyUpdate, currentPolicy);
await AssertPolicySavedAsync(sutProvider, policyUpdate);
// Additional assertions to ensure certain properties have or have not been updated
await sutProvider.GetDependency<IPolicyRepository>().Received(1).UpsertAsync(Arg.Is<Policy>(p =>
p.Id == id &&
p.OrganizationId == organizationId &&
p.Type == type &&
p.CreationDate == creationDate &&
p.RevisionDate == revisionDate));
}
[Fact]
public void Constructor_DuplicatePolicyValidators_Throws()
{
var exception = Assert.Throws<Exception>(() =>
new SavePolicyCommand(
Substitute.For<IApplicationCacheService>(),
Substitute.For<IEventService>(),
Substitute.For<IPolicyRepository>(),
[new FakeSingleOrgPolicyValidator(), new FakeSingleOrgPolicyValidator()],
Substitute.For<TimeProvider>()
));
Assert.Contains("Duplicate PolicyValidator for SingleOrg policy", exception.Message);
}
[Theory, BitAutoData]
public async Task SaveAsync_OrganizationDoesNotExist_ThrowsBadRequest(PolicyUpdate policyUpdate)
{
var sutProvider = SutProviderFactory();
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(policyUpdate.OrganizationId)
.Returns(Task.FromResult<OrganizationAbility?>(null));
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policyUpdate));
Assert.Contains("Organization not found", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await AssertPolicyNotSavedAsync(sutProvider);
}
[Theory, BitAutoData]
public async Task SaveAsync_OrganizationCannotUsePolicies_ThrowsBadRequest(PolicyUpdate policyUpdate)
{
var sutProvider = SutProviderFactory();
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(policyUpdate.OrganizationId)
.Returns(new OrganizationAbility
{
Id = policyUpdate.OrganizationId,
UsePolicies = false
});
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policyUpdate));
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)
{
var sutProvider = SutProviderFactory([
new FakeRequireSsoPolicyValidator(),
new FakeSingleOrgPolicyValidator()
]);
ArrangeOrganization(sutProvider, policyUpdate);
sutProvider.GetDependency<IPolicyRepository>()
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns([]);
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policyUpdate));
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)
{
var sutProvider = SutProviderFactory([
new FakeRequireSsoPolicyValidator(),
new FakeSingleOrgPolicyValidator()
]);
ArrangeOrganization(sutProvider, policyUpdate);
sutProvider.GetDependency<IPolicyRepository>()
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns([singleOrgPolicy]);
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policyUpdate));
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)
{
var sutProvider = SutProviderFactory([
new FakeRequireSsoPolicyValidator(),
new FakeSingleOrgPolicyValidator()
]);
ArrangeOrganization(sutProvider, policyUpdate);
sutProvider.GetDependency<IPolicyRepository>()
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns([singleOrgPolicy]);
await sutProvider.Sut.SaveAsync(policyUpdate);
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) // depends on Single Org
{
var sutProvider = SutProviderFactory([
new FakeRequireSsoPolicyValidator(),
new FakeSingleOrgPolicyValidator()
]);
ArrangeOrganization(sutProvider, policyUpdate);
sutProvider.GetDependency<IPolicyRepository>()
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns([currentPolicy, requireSsoPolicy]);
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policyUpdate));
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, // depends on Single Org
[Policy(PolicyType.MaximumVaultTimeout)] Policy vaultTimeoutPolicy) // depends on Single Org
{
var sutProvider = SutProviderFactory([
new FakeRequireSsoPolicyValidator(),
new FakeSingleOrgPolicyValidator(),
new FakeVaultTimeoutPolicyValidator()
]);
ArrangeOrganization(sutProvider, policyUpdate);
sutProvider.GetDependency<IPolicyRepository>()
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns([currentPolicy, requireSsoPolicy, vaultTimeoutPolicy]);
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policyUpdate));
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) // depends on Single Org but is not enabled
{
var sutProvider = SutProviderFactory([
new FakeRequireSsoPolicyValidator(),
new FakeSingleOrgPolicyValidator()
]);
ArrangeOrganization(sutProvider, policyUpdate);
sutProvider.GetDependency<IPolicyRepository>()
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns([currentPolicy, requireSsoPolicy]);
await sutProvider.Sut.SaveAsync(policyUpdate);
await AssertPolicySavedAsync(sutProvider, policyUpdate);
}
[Theory, BitAutoData]
public async Task SaveAsync_ThrowsOnValidationError([PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate)
{
var fakePolicyValidator = new FakeSingleOrgPolicyValidator();
fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns("Validation error!");
var sutProvider = SutProviderFactory([fakePolicyValidator]);
ArrangeOrganization(sutProvider, policyUpdate);
sutProvider.GetDependency<IPolicyRepository>().GetManyByOrganizationIdAsync(policyUpdate.OrganizationId).Returns([]);
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policyUpdate));
Assert.Contains("Validation error!", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await AssertPolicyNotSavedAsync(sutProvider);
}
/// <summary>
/// Returns a new SutProvider with the PolicyValidators registered in the Sut.
/// </summary>
private static SutProvider<SavePolicyCommand> SutProviderFactory(IEnumerable<IPolicyValidator>? policyValidators = null)
{
return new SutProvider<SavePolicyCommand>()
.WithFakeTimeProvider()
.SetDependency(typeof(IEnumerable<IPolicyValidator>), policyValidators ?? [])
.Create();
}
private static void ArrangeOrganization(SutProvider<SavePolicyCommand> sutProvider, PolicyUpdate policyUpdate)
{
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(policyUpdate.OrganizationId)
.Returns(new OrganizationAbility
{
Id = policyUpdate.OrganizationId,
UsePolicies = true
});
}
private static async Task AssertPolicyNotSavedAsync(SutProvider<SavePolicyCommand> sutProvider)
{
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default!);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default);
}
private static async Task AssertPolicySavedAsync(SutProvider<SavePolicyCommand> sutProvider, PolicyUpdate policyUpdate)
{
var expectedPolicy = () => Arg.Is<Policy>(p =>
p.Type == policyUpdate.Type &&
p.OrganizationId == policyUpdate.OrganizationId &&
p.Enabled == policyUpdate.Enabled &&
p.Data == policyUpdate.Data);
await sutProvider.GetDependency<IPolicyRepository>().Received(1).UpsertAsync(expectedPolicy());
await sutProvider.GetDependency<IEventService>().Received(1)
.LogPolicyEventAsync(expectedPolicy(), EventType.Policy_Updated);
}
}