diff --git a/src/Api/AdminConsole/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Controllers/PoliciesController.cs index a80546e2f517..e4023e27c941 100644 --- a/src/Api/AdminConsole/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Controllers/PoliciesController.cs @@ -1,10 +1,13 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using Bit.Api.AdminConsole.Authorization; +using Bit.Api.AdminConsole.Authorization.Requirements; using Bit.Api.AdminConsole.Models.Request; using Bit.Api.AdminConsole.Models.Response.Helpers; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Response; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; @@ -30,7 +33,6 @@ namespace Bit.Api.AdminConsole.Controllers; public class PoliciesController : Controller { private readonly ICurrentContext _currentContext; - private readonly IFeatureService _featureService; private readonly GlobalSettings _globalSettings; private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery; private readonly IOrganizationRepository _organizationRepository; @@ -49,7 +51,6 @@ public PoliciesController(IPolicyRepository policyRepository, GlobalSettings globalSettings, IDataProtectionProvider dataProtectionProvider, IDataProtectorTokenFactory orgUserInviteTokenDataFactory, - IFeatureService featureService, IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery, IOrganizationRepository organizationRepository, ISavePolicyCommand savePolicyCommand) @@ -63,7 +64,6 @@ public PoliciesController(IPolicyRepository policyRepository, "OrganizationServiceDataProtector"); _organizationRepository = organizationRepository; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; - _featureService = featureService; _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery; _savePolicyCommand = savePolicyCommand; } @@ -212,4 +212,18 @@ public async Task Put(Guid orgId, PolicyType type, [FromBod var policy = await _savePolicyCommand.SaveAsync(policyUpdate); return new PolicyResponseModel(policy); } + + + [HttpPut("{type}/vnext")] + [RequireFeatureAttribute(FeatureFlagKeys.CreateDefaultLocation)] + [Authorize] + public async Task PutVNext(Guid orgId, [FromBody] SavePolicyRequest model) + { + var savePolicyRequest = await model.ToSavePolicyModelAsync(orgId, _currentContext); + + var policy = await _savePolicyCommand.VNextSaveAsync(savePolicyRequest); + + return new PolicyResponseModel(policy); + } + } diff --git a/src/Api/AdminConsole/Models/Request/SavePolicyRequest.cs b/src/Api/AdminConsole/Models/Request/SavePolicyRequest.cs new file mode 100644 index 000000000000..fcdc49882b0b --- /dev/null +++ b/src/Api/AdminConsole/Models/Request/SavePolicyRequest.cs @@ -0,0 +1,61 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.Context; +using Bit.Core.Utilities; + +namespace Bit.Api.AdminConsole.Models.Request; + +public class SavePolicyRequest +{ + [Required] + public PolicyRequestModel Policy { get; set; } = null!; + + public Dictionary? Metadata { get; set; } + + public async Task ToSavePolicyModelAsync(Guid organizationId, ICurrentContext currentContext) + { + var performedBy = new StandardUser(currentContext.UserId!.Value, await currentContext.OrganizationOwner(organizationId)); + + var updatedPolicy = new PolicyUpdate() + { + Type = Policy.Type!.Value, + OrganizationId = organizationId, + Data = Policy.Data != null ? JsonSerializer.Serialize(Policy.Data) : null, + Enabled = Policy.Enabled.GetValueOrDefault(), + }; + + var metadata = MapToPolicyMetadata(); + + return new SavePolicyModel(updatedPolicy, performedBy, metadata); + } + + private IPolicyMetadataModel MapToPolicyMetadata() + { + if (Metadata == null) + { + return new EmptyMetadataModel(); + } + + return Policy?.Type switch + { + PolicyType.OrganizationDataOwnership => MapToPolicyMetadata(), + _ => new EmptyMetadataModel() + }; + } + + private IPolicyMetadataModel MapToPolicyMetadata() where T : IPolicyMetadataModel, new() + { + try + { + var json = JsonSerializer.Serialize(Metadata); + return CoreHelpers.LoadClassFromJsonData(json); + } + catch + { + return new EmptyMetadataModel(); + } + } +} diff --git a/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs index 9feafce70c9c..81ca8013081e 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs @@ -10,6 +10,10 @@ namespace Bit.Api.AdminConsole.Models.Response.Organizations; public class PolicyResponseModel : ResponseModel { + public PolicyResponseModel() : base("policy") + { + } + public PolicyResponseModel(Policy policy, string obj = "policy") : base(obj) { diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/IPostSavePolicySideEffect.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPostSavePolicySideEffect.cs new file mode 100644 index 000000000000..e90945d12d10 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPostSavePolicySideEffect.cs @@ -0,0 +1,10 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies; + +public interface IPostSavePolicySideEffect +{ + public Task ExecuteSideEffectsAsync(SavePolicyModel policyRequest, Policy postUpdatedPolicy, + Policy? previousPolicyState); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/ISavePolicyCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/ISavePolicyCommand.cs index 6ca842686ecb..73278d77d2f3 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/ISavePolicyCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/ISavePolicyCommand.cs @@ -6,4 +6,10 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies; public interface ISavePolicyCommand { Task SaveAsync(PolicyUpdate policy); + + /// + /// FIXME: this is a first pass at implementing side effects after the policy has been saved, which was not supported by the validator pattern. + /// However, this needs to be implemented in a policy-agnostic way rather than building out switch statements in the command itself. + /// + Task VNextSaveAsync(SavePolicyModel policyRequest); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs index 71212aaf4c75..e2bca930d11c 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs @@ -1,6 +1,4 @@ -#nullable enable - -using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.Repositories; @@ -17,18 +15,20 @@ public class SavePolicyCommand : ISavePolicyCommand private readonly IPolicyRepository _policyRepository; private readonly IReadOnlyDictionary _policyValidators; private readonly TimeProvider _timeProvider; + private readonly IPostSavePolicySideEffect _postSavePolicySideEffect; - public SavePolicyCommand( - IApplicationCacheService applicationCacheService, + public SavePolicyCommand(IApplicationCacheService applicationCacheService, IEventService eventService, IPolicyRepository policyRepository, IEnumerable policyValidators, - TimeProvider timeProvider) + TimeProvider timeProvider, + IPostSavePolicySideEffect postSavePolicySideEffect) { _applicationCacheService = applicationCacheService; _eventService = eventService; _policyRepository = policyRepository; _timeProvider = timeProvider; + _postSavePolicySideEffect = postSavePolicySideEffect; var policyValidatorsDict = new Dictionary(); foreach (var policyValidator in policyValidators) @@ -78,12 +78,28 @@ public async Task SaveAsync(PolicyUpdate policyUpdate) return policy; } + public async Task VNextSaveAsync(SavePolicyModel policyRequest) + { + var (_, currentPolicy) = await GetCurrentPolicyStateAsync(policyRequest.PolicyUpdate); + + var policy = await SaveAsync(policyRequest.PolicyUpdate); + + await ExecutePostPolicySaveSideEffectsForSupportedPoliciesAsync(policyRequest, policy, currentPolicy); + + return policy; + } + + private async Task ExecutePostPolicySaveSideEffectsForSupportedPoliciesAsync(SavePolicyModel policyRequest, Policy postUpdatedPolicy, Policy? previousPolicyState) + { + if (postUpdatedPolicy.Type == PolicyType.OrganizationDataOwnership) + { + await _postSavePolicySideEffect.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState); + } + } + private async Task RunValidatorAsync(IPolicyValidator validator, PolicyUpdate policyUpdate) { - var savedPolicies = await _policyRepository.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId); - // Note: policies may be missing from this dict if they have never been enabled - var savedPoliciesDict = savedPolicies.ToDictionary(p => p.Type); - var currentPolicy = savedPoliciesDict.GetValueOrDefault(policyUpdate.Type); + var (savedPoliciesDict, currentPolicy) = await GetCurrentPolicyStateAsync(policyUpdate); // If enabling this policy - check that all policy requirements are satisfied if (currentPolicy is not { Enabled: true } && policyUpdate.Enabled) @@ -127,4 +143,13 @@ private async Task RunValidatorAsync(IPolicyValidator validator, PolicyUpdate po // Run side effects await validator.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); } + + private async Task<(Dictionary savedPoliciesDict, Policy? currentPolicy)> GetCurrentPolicyStateAsync(PolicyUpdate policyUpdate) + { + var savedPolicies = await _policyRepository.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId); + // Note: policies may be missing from this dict if they have never been enabled + var savedPoliciesDict = savedPolicies.ToDictionary(p => p.Type); + var currentPolicy = savedPoliciesDict.GetValueOrDefault(policyUpdate.Type); + return (savedPoliciesDict, currentPolicy); + } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/EmptyMetadataModel.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/EmptyMetadataModel.cs new file mode 100644 index 000000000000..0c086ac5752d --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/EmptyMetadataModel.cs @@ -0,0 +1,6 @@ + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; + +public record EmptyMetadataModel : IPolicyMetadataModel +{ +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/IPolicyMetadataModel.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/IPolicyMetadataModel.cs new file mode 100644 index 000000000000..5331524a1d47 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/IPolicyMetadataModel.cs @@ -0,0 +1,6 @@ + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; + +public interface IPolicyMetadataModel +{ +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/OrganizationModelOwnershipPolicyModel.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/OrganizationModelOwnershipPolicyModel.cs new file mode 100644 index 000000000000..0ff9200d8ffc --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/OrganizationModelOwnershipPolicyModel.cs @@ -0,0 +1,16 @@ + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; + +public class OrganizationModelOwnershipPolicyModel : IPolicyMetadataModel +{ + public OrganizationModelOwnershipPolicyModel() + { + } + + public OrganizationModelOwnershipPolicyModel(string? defaultUserCollectionName) + { + DefaultUserCollectionName = defaultUserCollectionName; + } + + public string? DefaultUserCollectionName { get; set; } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/SavePolicyModel.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/SavePolicyModel.cs new file mode 100644 index 000000000000..7c8d5126e86c --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/SavePolicyModel.cs @@ -0,0 +1,8 @@ + +using Bit.Core.AdminConsole.Models.Data; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; + +public record SavePolicyModel(PolicyUpdate PolicyUpdate, IActingUser? PerformedBy, IPolicyMetadataModel Metadata) +{ +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index 12dd3f973d9e..5433d70410c6 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -17,6 +17,7 @@ public static void AddPolicyServices(this IServiceCollection services) services.AddPolicyValidators(); services.AddPolicyRequirements(); + services.AddPolicySideEffects(); } private static void AddPolicyValidators(this IServiceCollection services) @@ -27,8 +28,11 @@ private static void AddPolicyValidators(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); - // This validator will be hooked up in https://bitwarden.atlassian.net/browse/PM-24279. - // services.AddScoped(); + } + + private static void AddPolicySideEffects(this IServiceCollection services) + { + services.AddScoped(); } private static void AddPolicyRequirements(this IServiceCollection services) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs index 2471bda647b3..f4ef6021a7ba 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs @@ -1,44 +1,55 @@ -#nullable enable - + using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Repositories; using Bit.Core.Services; -using Microsoft.Extensions.Logging; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; +/// +/// Please do not extend or expand this validator. We're currently in the process of refactoring our policy validator pattern. +/// This is a stop-gap solution for post-policy-save side effects, but it is not the long-term solution. +/// public class OrganizationDataOwnershipPolicyValidator( IPolicyRepository policyRepository, ICollectionRepository collectionRepository, IEnumerable> factories, - IFeatureService featureService, - ILogger logger) - : OrganizationPolicyValidator(policyRepository, factories) + IFeatureService featureService) + : OrganizationPolicyValidator(policyRepository, factories), IPostSavePolicySideEffect { - public override PolicyType Type => PolicyType.OrganizationDataOwnership; - - public override IEnumerable RequiredPolicies => []; - - public override Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(""); - - public override async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) + public async Task ExecuteSideEffectsAsync( + SavePolicyModel policyRequest, + Policy postUpdatedPolicy, + Policy? previousPolicyState) { if (!featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)) { return; } - if (currentPolicy?.Enabled != true && policyUpdate.Enabled) + if (policyRequest.Metadata is not OrganizationModelOwnershipPolicyModel metadata) { - await UpsertDefaultCollectionsForUsersAsync(policyUpdate); + return; + } + + if (string.IsNullOrWhiteSpace(metadata.DefaultUserCollectionName)) + { + return; + } + + var isFirstTimeEnabled = postUpdatedPolicy.Enabled && previousPolicyState == null; + var reEnabled = previousPolicyState?.Enabled == false + && postUpdatedPolicy.Enabled; + + if (isFirstTimeEnabled || reEnabled) + { + await UpsertDefaultCollectionsForUsersAsync(policyRequest.PolicyUpdate, metadata.DefaultUserCollectionName); } } - private async Task UpsertDefaultCollectionsForUsersAsync(PolicyUpdate policyUpdate) + private async Task UpsertDefaultCollectionsForUsersAsync(PolicyUpdate policyUpdate, string defaultCollectionName) { var requirements = await GetUserPolicyRequirementsByOrganizationIdAsync(policyUpdate.OrganizationId, policyUpdate.Type); @@ -49,20 +60,13 @@ private async Task UpsertDefaultCollectionsForUsersAsync(PolicyUpdate policyUpda if (!userOrgIds.Any()) { - logger.LogError("No UserOrganizationIds found for {OrganizationId}", policyUpdate.OrganizationId); return; } await collectionRepository.UpsertDefaultCollectionsAsync( policyUpdate.OrganizationId, userOrgIds, - GetDefaultUserCollectionName()); + defaultCollectionName); } - private static string GetDefaultUserCollectionName() - { - // TODO: https://bitwarden.atlassian.net/browse/PM-24279 - const string temporaryPlaceHolderValue = "Default"; - return temporaryPlaceHolderValue; - } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidator.cs index 33667b829cf7..15a0b4bb5497 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidator.cs @@ -1,17 +1,16 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; -public abstract class OrganizationPolicyValidator(IPolicyRepository policyRepository, IEnumerable> factories) : IPolicyValidator -{ - public abstract PolicyType Type { get; } - - public abstract IEnumerable RequiredPolicies { get; } +/// +/// Please do not use this validator. We're currently in the process of refactoring our policy validator pattern. +/// This is a stop-gap solution for post-policy-save side effects, but it is not the long-term solution. +/// +public abstract class OrganizationPolicyValidator(IPolicyRepository policyRepository, IEnumerable> factories) +{ protected async Task> GetUserPolicyRequirementsByOrganizationIdAsync(Guid organizationId, PolicyType policyType) where T : IPolicyRequirement { var factory = factories.OfType>().SingleOrDefault(); @@ -36,14 +35,4 @@ protected async Task> GetUserPolicyRequirementsByOrganizationIdAs return requirements; } - - public abstract Task OnSaveSideEffectsAsync( - PolicyUpdate policyUpdate, - Policy? currentPolicy - ); - - public abstract Task ValidateAsync( - PolicyUpdate policyUpdate, - Policy? currentPolicy - ); } diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/PoliciesControllerTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/PoliciesControllerTests.cs new file mode 100644 index 000000000000..1efc2f843dee --- /dev/null +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/PoliciesControllerTests.cs @@ -0,0 +1,214 @@ +using System.Net; +using System.Text.Json; +using Bit.Api.AdminConsole.Models.Request; +using Bit.Api.AdminConsole.Models.Response.Organizations; +using Bit.Api.IntegrationTest.Factories; +using Bit.Api.IntegrationTest.Helpers; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Enums; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Test.Common.Helpers; +using NSubstitute; +using Xunit; + +namespace Bit.Api.IntegrationTest.AdminConsole.Controllers; + +public class PoliciesControllerTests : IClassFixture, IAsyncLifetime +{ + private readonly HttpClient _client; + private readonly ApiApplicationFactory _factory; + private readonly LoginHelper _loginHelper; + + private Organization _organization = null!; + private string _ownerEmail = null!; + + public PoliciesControllerTests(ApiApplicationFactory factory) + { + _factory = factory; + _factory.SubstituteService(featureService => + { + featureService + .IsEnabled("pm-19467-create-default-location") + .Returns(true); + }); + _client = factory.CreateClient(); + _loginHelper = new LoginHelper(_factory, _client); + } + + public async Task InitializeAsync() + { + _ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(_ownerEmail); + + (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually, + ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card); + + await _loginHelper.LoginAsync(_ownerEmail); + } + + public Task DisposeAsync() + { + _client.Dispose(); + return Task.CompletedTask; + } + + [Fact] + public async Task PutVNext_OrganizationDataOwnershipPolicy_Success() + { + // Arrange + const PolicyType policyType = PolicyType.OrganizationDataOwnership; + + const string defaultCollectionName = "Test Default Collection"; + var request = new SavePolicyRequest + { + Policy = new PolicyRequestModel + { + Type = policyType, + Enabled = true, + }, + Metadata = new Dictionary + { + { "defaultUserCollectionName", defaultCollectionName } + } + }; + + var (_, admin) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, + _organization.Id, OrganizationUserType.Admin); + + var (_, user) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, + _organization.Id, OrganizationUserType.User); + + // Act + var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}/vnext", + JsonContent.Create(request)); + + // Assert + await AssertResponse(); + + await AssertPolicy(); + + await AssertDefaultCollectionCreatedOnlyForUserTypeAsync(); + return; + + async Task AssertDefaultCollectionCreatedOnlyForUserTypeAsync() + { + var collectionRepository = _factory.GetService(); + await AssertUserExpectations(collectionRepository); + await AssertAdminExpectations(collectionRepository); + } + + async Task AssertUserExpectations(ICollectionRepository collectionRepository) + { + var collections = await collectionRepository.GetManyByUserIdAsync(user.UserId!.Value); + var defaultCollection = collections.FirstOrDefault(c => c.Name == defaultCollectionName); + Assert.NotNull(defaultCollection); + Assert.Equal(_organization.Id, defaultCollection.OrganizationId); + } + + async Task AssertAdminExpectations(ICollectionRepository collectionRepository) + { + var collections = await collectionRepository.GetManyByUserIdAsync(admin.UserId!.Value); + var defaultCollection = collections.FirstOrDefault(c => c.Name == defaultCollectionName); + Assert.Null(defaultCollection); + } + + async Task AssertResponse() + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadFromJsonAsync(); + + Assert.True(content.Enabled); + Assert.Equal(policyType, content.Type); + Assert.Equal(_organization.Id, content.OrganizationId); + } + + async Task AssertPolicy() + { + var policyRepository = _factory.GetService(); + var policy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType); + + Assert.NotNull(policy); + Assert.True(policy.Enabled); + Assert.Equal(policyType, policy.Type); + Assert.Null(policy.Data); + Assert.Equal(_organization.Id, policy.OrganizationId); + } + } + + [Fact] + public async Task PutVNext_MasterPasswordPolicy_Success() + { + // Arrange + var policyType = PolicyType.MasterPassword; + var request = new SavePolicyRequest + { + Policy = new PolicyRequestModel + { + Type = policyType, + Enabled = true, + Data = new Dictionary + { + { "minComplexity", 10 }, + { "minLength", 12 }, + { "requireUpper", true }, + { "requireLower", false }, + { "requireNumbers", true }, + { "requireSpecial", false }, + { "enforceOnLogin", true } + } + } + }; + + // Act + var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}/vnext", + JsonContent.Create(request)); + + // Assert + await AssertResponse(); + + await AssertPolicyDataForMasterPasswordPolicy(); + return; + + async Task AssertPolicyDataForMasterPasswordPolicy() + { + var policyRepository = _factory.GetService(); + var policy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType); + + AssertPolicy(policy); + AssertMasterPasswordPolicyData(policy); + } + + async Task AssertResponse() + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadFromJsonAsync(); + + Assert.True(content.Enabled); + Assert.Equal(policyType, content.Type); + Assert.Equal(_organization.Id, content.OrganizationId); + } + + void AssertPolicy(Policy policy) + { + Assert.NotNull(policy); + Assert.True(policy.Enabled); + Assert.Equal(policyType, policy.Type); + Assert.Equal(_organization.Id, policy.OrganizationId); + Assert.NotNull(policy.Data); + } + + void AssertMasterPasswordPolicyData(Policy policy) + { + var resultData = policy.GetDataModel(); + + var json = JsonSerializer.Serialize(request.Policy.Data); + var expectedData = JsonSerializer.Deserialize(json); + AssertHelper.AssertPropertyEqual(resultData, expectedData); + } + } + +} diff --git a/test/Api.Test/AdminConsole/Models/Request/SavePolicyRequestTests.cs b/test/Api.Test/AdminConsole/Models/Request/SavePolicyRequestTests.cs new file mode 100644 index 000000000000..057680425a59 --- /dev/null +++ b/test/Api.Test/AdminConsole/Models/Request/SavePolicyRequestTests.cs @@ -0,0 +1,303 @@ + +using System.Text.Json; +using Bit.Api.AdminConsole.Models.Request; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.Context; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.AdminConsole.Models.Request; + +[SutProviderCustomize] +public class SavePolicyRequestTests +{ + [Theory, BitAutoData] + public async Task ToSavePolicyModelAsync_WithValidData_ReturnsCorrectSavePolicyModel( + Guid organizationId, + Guid userId) + { + // Arrange + var currentContext = Substitute.For(); + currentContext.UserId.Returns(userId); + currentContext.OrganizationOwner(organizationId).Returns(true); + + var testData = new Dictionary { { "test", "value" } }; + var model = new SavePolicyRequest + { + Policy = new PolicyRequestModel + { + Type = PolicyType.TwoFactorAuthentication, + Enabled = true, + Data = testData + }, + Metadata = new Dictionary() + }; + + // Act + var result = await model.ToSavePolicyModelAsync(organizationId, currentContext); + + // Assert + Assert.Equal(PolicyType.TwoFactorAuthentication, result.PolicyUpdate.Type); + Assert.Equal(organizationId, result.PolicyUpdate.OrganizationId); + Assert.True(result.PolicyUpdate.Enabled); + Assert.NotNull(result.PolicyUpdate.Data); + + var deserializedData = JsonSerializer.Deserialize>(result.PolicyUpdate.Data); + Assert.Equal("value", deserializedData["test"].ToString()); + + Assert.Equal(userId, result!.PerformedBy.UserId); + Assert.True(result!.PerformedBy.IsOrganizationOwnerOrProvider); + + Assert.IsType(result.Metadata); + } + + [Theory, BitAutoData] + public async Task ToSavePolicyModelAsync_WithNullData_HandlesCorrectly( + Guid organizationId, + Guid userId) + { + // Arrange + var currentContext = Substitute.For(); + currentContext.UserId.Returns(userId); + currentContext.OrganizationOwner(organizationId).Returns(false); + + var model = new SavePolicyRequest + { + Policy = new PolicyRequestModel + { + Type = PolicyType.SingleOrg, + Enabled = false, + Data = null + }, + Metadata = null + }; + + // Act + var result = await model.ToSavePolicyModelAsync(organizationId, currentContext); + + // Assert + Assert.Null(result.PolicyUpdate.Data); + Assert.False(result.PolicyUpdate.Enabled); + + Assert.Equal(userId, result!.PerformedBy.UserId); + Assert.False(result!.PerformedBy.IsOrganizationOwnerOrProvider); + } + + [Theory, BitAutoData] + public async Task ToSavePolicyModelAsync_WithNonOrganizationOwner_HandlesCorrectly( + Guid organizationId, + Guid userId) + { + // Arrange + var currentContext = Substitute.For(); + currentContext.UserId.Returns(userId); + currentContext.OrganizationOwner(organizationId).Returns(true); + + var model = new SavePolicyRequest + { + Policy = new PolicyRequestModel + { + Type = PolicyType.SingleOrg, + Enabled = false, + Data = null + }, + Metadata = null + }; + + // Act + var result = await model.ToSavePolicyModelAsync(organizationId, currentContext); + + // Assert + Assert.Null(result.PolicyUpdate.Data); + Assert.False(result.PolicyUpdate.Enabled); + + Assert.Equal(userId, result!.PerformedBy.UserId); + Assert.True(result!.PerformedBy.IsOrganizationOwnerOrProvider); + } + + [Theory, BitAutoData] + public async Task ToSavePolicyModelAsync_OrganizationDataOwnership_WithValidMetadata_ReturnsCorrectMetadata( + Guid organizationId, + Guid userId, + string defaultCollectionName) + { + // Arrange + var currentContext = Substitute.For(); + currentContext.UserId.Returns(userId); + currentContext.OrganizationOwner(organizationId).Returns(true); + + var model = new SavePolicyRequest + { + Policy = new PolicyRequestModel + { + Type = PolicyType.OrganizationDataOwnership, + Enabled = true, + Data = null + }, + Metadata = new Dictionary + { + { "defaultUserCollectionName", defaultCollectionName } + } + }; + + // Act + var result = await model.ToSavePolicyModelAsync(organizationId, currentContext); + + // Assert + Assert.IsType(result.Metadata); + var metadata = (OrganizationModelOwnershipPolicyModel)result.Metadata; + Assert.Equal(defaultCollectionName, metadata.DefaultUserCollectionName); + } + + [Theory, BitAutoData] + public async Task ToSavePolicyModelAsync_OrganizationDataOwnership_WithNullMetadata_ReturnsEmptyMetadata( + Guid organizationId, + Guid userId) + { + // Arrange + var currentContext = Substitute.For(); + currentContext.UserId.Returns(userId); + currentContext.OrganizationOwner(organizationId).Returns(true); + + var model = new SavePolicyRequest + { + Policy = new PolicyRequestModel + { + Type = PolicyType.OrganizationDataOwnership, + Enabled = true, + Data = null + }, + Metadata = null + }; + + // Act + var result = await model.ToSavePolicyModelAsync(organizationId, currentContext); + + // Assert + Assert.NotNull(result); + Assert.IsType(result.Metadata); + } + + private static readonly Dictionary _complexData = new Dictionary + { + { "stringValue", "test" }, + { "numberValue", 42 }, + { "boolValue", true }, + { "arrayValue", new[] { "item1", "item2" } }, + { "nestedObject", new Dictionary { { "nested", "value" } } } + }; + + [Theory, BitAutoData] + public async Task ToSavePolicyModelAsync_ComplexData_SerializesCorrectly( + Guid organizationId, + Guid userId) + { + // Arrange + var currentContext = Substitute.For(); + currentContext.UserId.Returns(userId); + currentContext.OrganizationOwner(organizationId).Returns(true); + + + var model = new SavePolicyRequest + { + Policy = new PolicyRequestModel + { + Type = PolicyType.ResetPassword, + Enabled = true, + Data = _complexData + }, + Metadata = new Dictionary() + }; + + // Act + var result = await model.ToSavePolicyModelAsync(organizationId, currentContext); + + // Assert + var deserializedData = JsonSerializer.Deserialize>(result.PolicyUpdate.Data); + Assert.Equal("test", deserializedData["stringValue"].GetString()); + Assert.Equal(42, deserializedData["numberValue"].GetInt32()); + Assert.True(deserializedData["boolValue"].GetBoolean()); + Assert.Equal(2, deserializedData["arrayValue"].GetArrayLength()); + var array = deserializedData["arrayValue"].EnumerateArray() + .Select(e => e.GetString()) + .ToArray(); + Assert.Contains("item1", array); + Assert.Contains("item2", array); + Assert.True(deserializedData["nestedObject"].TryGetProperty("nested", out var nestedValue)); + Assert.Equal("value", nestedValue.GetString()); + } + + + [Theory, BitAutoData] + public async Task MapToPolicyMetadata_UnknownPolicyType_ReturnsEmptyMetadata( + Guid organizationId, + Guid userId) + { + // Arrange + var currentContext = Substitute.For(); + currentContext.UserId.Returns(userId); + currentContext.OrganizationOwner(organizationId).Returns(true); + + var model = new SavePolicyRequest + { + Policy = new PolicyRequestModel + { + Type = PolicyType.MaximumVaultTimeout, + Enabled = true, + Data = null + }, + Metadata = new Dictionary + { + { "someProperty", "someValue" } + } + }; + + // Act + var result = await model.ToSavePolicyModelAsync(organizationId, currentContext); + + // Assert + Assert.NotNull(result); + Assert.IsType(result.Metadata); + } + + [Theory, BitAutoData] + public async Task MapToPolicyMetadata_JsonSerializationException_ReturnsEmptyMetadata( + Guid organizationId, + Guid userId) + { + // Arrange + var currentContext = Substitute.For(); + currentContext.UserId.Returns(userId); + currentContext.OrganizationOwner(organizationId).Returns(true); + + var errorDictionary = BuildErrorDictionary(); + + var model = new SavePolicyRequest + { + Policy = new PolicyRequestModel + { + Type = PolicyType.OrganizationDataOwnership, + Enabled = true, + Data = null + }, + Metadata = errorDictionary + }; + + // Act + var result = await model.ToSavePolicyModelAsync(organizationId, currentContext); + + // Assert + Assert.NotNull(result); + Assert.IsType(result.Metadata); + } + + private static Dictionary BuildErrorDictionary() + { + var circularDict = new Dictionary(); + circularDict["self"] = circularDict; + return circularDict; + } +} diff --git a/test/Core.Test/AdminConsole/AutoFixture/PolicyUpdateFixtures.cs b/test/Core.Test/AdminConsole/AutoFixture/PolicyUpdateFixtures.cs index 794f6fddf351..4d004766459b 100644 --- a/test/Core.Test/AdminConsole/AutoFixture/PolicyUpdateFixtures.cs +++ b/test/Core.Test/AdminConsole/AutoFixture/PolicyUpdateFixtures.cs @@ -18,7 +18,7 @@ public void Customize(IFixture fixture) } } -public class PolicyUpdateAttribute(PolicyType type, bool enabled = true) : CustomizeAttribute +public class PolicyUpdateAttribute(PolicyType type = PolicyType.FreeFamiliesSponsorshipPolicy, bool enabled = true) : CustomizeAttribute { public override ICustomization GetCustomization(ParameterInfo parameter) { diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs index 2569bc6988c7..a39382382bc0 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs @@ -10,7 +10,6 @@ using Bit.Core.Test.AdminConsole.AutoFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; -using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -22,9 +21,10 @@ public class OrganizationDataOwnershipPolicyValidatorTests private const string _defaultUserCollectionName = "Default"; [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_FeatureFlagDisabled_DoesNothing( - [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, - [Policy(PolicyType.OrganizationDataOwnership, false)] Policy currentPolicy, + public async Task ExecuteSideEffectsAsync_FeatureFlagDisabled_DoesNothing( + [PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.OrganizationDataOwnership, false)] Policy postUpdatedPolicy, + [Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState, SutProvider sutProvider) { // Arrange @@ -32,95 +32,102 @@ public async Task OnSaveSideEffectsAsync_FeatureFlagDisabled_DoesNothing( .IsEnabled(FeatureFlagKeys.CreateDefaultLocation) .Returns(false); + var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName)); + // Act - await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); + await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState); // Assert await sutProvider.GetDependency() .DidNotReceive() - .UpsertDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); + .UpsertDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); } [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_PolicyAlreadyEnabled_DoesNothing( - [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, - [Policy(PolicyType.OrganizationDataOwnership, true)] Policy currentPolicy, + public async Task ExecuteSideEffectsAsync_PolicyAlreadyEnabled_DoesNothing( + [PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate, + [Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy, + [Policy(PolicyType.OrganizationDataOwnership, true)] Policy previousPolicyState, SutProvider sutProvider) { // Arrange - currentPolicy.OrganizationId = policyUpdate.OrganizationId; - policyUpdate.Enabled = true; + postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId; + previousPolicyState.OrganizationId = policyUpdate.OrganizationId; sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.CreateDefaultLocation) .Returns(true); + var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName)); + // Act - await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); + await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState); // Assert await sutProvider.GetDependency() .DidNotReceive() - .UpsertDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); + .UpsertDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); } [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_PolicyBeingDisabled_DoesNothing( + public async Task ExecuteSideEffectsAsync_PolicyBeingDisabled_DoesNothing( [PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate, - [Policy(PolicyType.OrganizationDataOwnership, true)] Policy currentPolicy, + [Policy(PolicyType.OrganizationDataOwnership, false)] Policy postUpdatedPolicy, + [Policy(PolicyType.OrganizationDataOwnership)] Policy previousPolicyState, SutProvider sutProvider) { // Arrange - currentPolicy.OrganizationId = policyUpdate.OrganizationId; + previousPolicyState.OrganizationId = policyUpdate.OrganizationId; + postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId; sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.CreateDefaultLocation) .Returns(true); + var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName)); + // Act - await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); + await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState); // Assert await sutProvider.GetDependency() .DidNotReceive() - .UpsertDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); + .UpsertDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); } [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_WhenNoUsersExist_ShouldLogError( - [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, - [Policy(PolicyType.OrganizationDataOwnership, false)] Policy currentPolicy, + public async Task ExecuteSideEffectsAsync_WhenNoUsersExist_DoNothing( + [PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate, + [Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy, + [Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState, OrganizationDataOwnershipPolicyRequirementFactory factory) { // Arrange - currentPolicy.OrganizationId = policyUpdate.OrganizationId; - policyUpdate.Enabled = true; + postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId; + previousPolicyState.OrganizationId = policyUpdate.OrganizationId; - var policyRepository = ArrangePolicyRepositoryWithOutUsers(); + var policyRepository = ArrangePolicyRepository([]); var collectionRepository = Substitute.For(); - var logger = Substitute.For>(); - var sut = ArrangeSut(factory, policyRepository, collectionRepository, logger); + var sut = ArrangeSut(factory, policyRepository, collectionRepository); + var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName)); // Act - await sut.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); + await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState); // Assert await collectionRepository .DidNotReceive() .UpsertDefaultCollectionsAsync( Arg.Any(), - Arg.Any>(), + Arg.Any>(), Arg.Any()); - const string expectedErrorMessage = "No UserOrganizationIds found for"; - - logger.Received(1).Log( - LogLevel.Error, - Arg.Any(), - Arg.Is(o => (o.ToString() ?? "").Contains(expectedErrorMessage)), - Arg.Any(), - Arg.Any>()); + await policyRepository + .Received(1) + .GetPolicyDetailsByOrganizationIdAsync( + policyUpdate.OrganizationId, + PolicyType.OrganizationDataOwnership); } public static IEnumerable ShouldUpsertDefaultCollectionsTestCases() @@ -133,13 +140,13 @@ await collectionRepository object?[] WithExistingPolicy() { var organizationId = Guid.NewGuid(); - var policyUpdate = new PolicyUpdate + var postUpdatedPolicy = new Policy { OrganizationId = organizationId, Type = PolicyType.OrganizationDataOwnership, Enabled = true }; - var currentPolicy = new Policy + var previousPolicyState = new Policy { Id = Guid.NewGuid(), OrganizationId = organizationId, @@ -149,51 +156,53 @@ await collectionRepository return new object?[] { - policyUpdate, - currentPolicy + postUpdatedPolicy, + previousPolicyState }; } object?[] WithNoExistingPolicy() { - var policyUpdate = new PolicyUpdate + var postUpdatedPolicy = new Policy { OrganizationId = new Guid(), Type = PolicyType.OrganizationDataOwnership, Enabled = true }; - const Policy currentPolicy = null; + const Policy previousPolicyState = null; return new object?[] { - policyUpdate, - currentPolicy + postUpdatedPolicy, + previousPolicyState }; } } - [Theory, BitAutoData] + [Theory] [BitMemberAutoData(nameof(ShouldUpsertDefaultCollectionsTestCases))] - public async Task OnSaveSideEffectsAsync_WithRequirements_ShouldUpsertDefaultCollections( + public async Task ExecuteSideEffectsAsync_WithRequirements_ShouldUpsertDefaultCollections( + Policy postUpdatedPolicy, + Policy? previousPolicyState, [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, - [Policy(PolicyType.OrganizationDataOwnership, false)] Policy? currentPolicy, [OrganizationPolicyDetails(PolicyType.OrganizationDataOwnership)] IEnumerable orgPolicyDetails, OrganizationDataOwnershipPolicyRequirementFactory factory) { // Arrange - foreach (var policyDetail in orgPolicyDetails) + var orgPolicyDetailsList = orgPolicyDetails.ToList(); + foreach (var policyDetail in orgPolicyDetailsList) { policyDetail.OrganizationId = policyUpdate.OrganizationId; } - var policyRepository = ArrangePolicyRepository(orgPolicyDetails); + var policyRepository = ArrangePolicyRepository(orgPolicyDetailsList); var collectionRepository = Substitute.For(); - var logger = Substitute.For>(); - var sut = ArrangeSut(factory, policyRepository, collectionRepository, logger); + var sut = ArrangeSut(factory, policyRepository, collectionRepository); + var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName)); // Act - await sut.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); + await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState); // Assert await collectionRepository @@ -204,9 +213,40 @@ await collectionRepository _defaultUserCollectionName); } - private static IPolicyRepository ArrangePolicyRepositoryWithOutUsers() + private static IEnumerable WhenDefaultCollectionsDoesNotExistTestCases() + { + yield return [new OrganizationModelOwnershipPolicyModel(null)]; + yield return [new OrganizationModelOwnershipPolicyModel("")]; + yield return [new OrganizationModelOwnershipPolicyModel(" ")]; + yield return [new EmptyMetadataModel()]; + } + [Theory] + [BitMemberAutoData(nameof(WhenDefaultCollectionsDoesNotExistTestCases))] + public async Task ExecuteSideEffectsAsync_WhenDefaultCollectionNameIsInvalid_DoesNothing( + IPolicyMetadataModel metadata, + [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, + [Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy, + [Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState, + SutProvider sutProvider) { - return ArrangePolicyRepository([]); + // Arrange + postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId; + previousPolicyState.OrganizationId = policyUpdate.OrganizationId; + policyUpdate.Enabled = true; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.CreateDefaultLocation) + .Returns(true); + + var policyRequest = new SavePolicyModel(policyUpdate, null, metadata); + + // Act + await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState); + + // Assert + await sutProvider.GetDependency() + .DidNotReceive() + .UpsertDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); } private static IPolicyRepository ArrangePolicyRepository(IEnumerable policyDetails) @@ -222,17 +262,15 @@ private static IPolicyRepository ArrangePolicyRepository(IEnumerable logger = null!) + ICollectionRepository collectionRepository) { - logger ??= Substitute.For>(); var featureService = Substitute.For(); featureService .IsEnabled(FeatureFlagKeys.CreateDefaultLocation) .Returns(true); - var sut = new OrganizationDataOwnershipPolicyValidator(policyRepository, collectionRepository, [factory], featureService, logger); + var sut = new OrganizationDataOwnershipPolicyValidator(policyRepository, collectionRepository, [factory], featureService); return sut; } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidatorTests.cs index aec123042388..bda927f18495 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidatorTests.cs @@ -1,7 +1,5 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Enums; +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.PolicyRequirements; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; using Bit.Core.AdminConsole.Repositories; @@ -161,20 +159,6 @@ public TestOrganizationPolicyValidator( { } - public override PolicyType Type => PolicyType.TwoFactorAuthentication; - - public override IEnumerable RequiredPolicies => []; - - public override Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) - { - return Task.FromResult(""); - } - - public override Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) - { - return Task.CompletedTask; - } - public async Task> TestGetUserPolicyRequirementsByOrganizationIdAsync(Guid organizationId, PolicyType policyType) where T : IPolicyRequirement { diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/SavePolicyCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/SavePolicyCommandTests.cs index 426389f33cc8..6b857607943e 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/SavePolicyCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/SavePolicyCommandTests.cs @@ -94,8 +94,8 @@ public void Constructor_DuplicatePolicyValidators_Throws() Substitute.For(), Substitute.For(), [new FakeSingleOrgPolicyValidator(), new FakeSingleOrgPolicyValidator()], - Substitute.For() - )); + Substitute.For(), + Substitute.For())); Assert.Contains("Duplicate PolicyValidator for SingleOrg policy", exception.Message); } @@ -281,6 +281,85 @@ public async Task SaveAsync_ThrowsOnValidationError([PolicyUpdate(PolicyType.Sin await AssertPolicyNotSavedAsync(sutProvider); } + [Theory, BitAutoData] + public async Task VNextSaveAsync_OrganizationDataOwnershipPolicy_ExecutesPostSaveSideEffects( + [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, + [Policy(PolicyType.OrganizationDataOwnership, false)] Policy currentPolicy) + { + // Arrange + var sutProvider = SutProviderFactory(); + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + currentPolicy.OrganizationId = policyUpdate.OrganizationId; + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type) + .Returns(currentPolicy); + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns([currentPolicy]); + + // Act + var result = await sutProvider.Sut.VNextSaveAsync(savePolicyModel); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .UpsertAsync(result); + + await sutProvider.GetDependency() + .Received(1) + .LogPolicyEventAsync(result, EventType.Policy_Updated); + + await sutProvider.GetDependency() + .Received(1) + .ExecuteSideEffectsAsync(savePolicyModel, result, currentPolicy); + } + + [Theory] + [BitAutoData(PolicyType.SingleOrg)] + [BitAutoData(PolicyType.TwoFactorAuthentication)] + public async Task VNextSaveAsync_NonOrganizationDataOwnershipPolicy_DoesNotExecutePostSaveSideEffects( + PolicyType policyType, + Policy currentPolicy, + [PolicyUpdate] PolicyUpdate policyUpdate) + { + // Arrange + policyUpdate.Type = policyType; + currentPolicy.Type = policyType; + currentPolicy.OrganizationId = policyUpdate.OrganizationId; + + + var sutProvider = SutProviderFactory(); + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type) + .Returns(currentPolicy); + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns([currentPolicy]); + + // Act + var result = await sutProvider.Sut.VNextSaveAsync(savePolicyModel); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .UpsertAsync(result); + + await sutProvider.GetDependency() + .Received(1) + .LogPolicyEventAsync(result, EventType.Policy_Updated); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .ExecuteSideEffectsAsync(default!, default!, default!); + } + /// /// Returns a new SutProvider with the PolicyValidators registered in the Sut. /// @@ -289,6 +368,7 @@ private static SutProvider SutProviderFactory(IEnumerable() .WithFakeTimeProvider() .SetDependency(policyValidators ?? []) + .SetDependency(Substitute.For()) .Create(); }