mirror of
https://github.com/bitwarden/server.git
synced 2025-12-10 00:42:07 -06:00
[PM-24279] Add vnext policy endpoint (#6253)
This commit is contained in:
parent
52045b89fa
commit
d43b00dad9
@ -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 class PoliciesController : Controller
|
||||
GlobalSettings globalSettings,
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
|
||||
IFeatureService featureService,
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ISavePolicyCommand savePolicyCommand)
|
||||
@ -63,7 +64,6 @@ public class PoliciesController : Controller
|
||||
"OrganizationServiceDataProtector");
|
||||
_organizationRepository = organizationRepository;
|
||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||
_featureService = featureService;
|
||||
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
||||
_savePolicyCommand = savePolicyCommand;
|
||||
}
|
||||
@ -212,4 +212,18 @@ public class PoliciesController : Controller
|
||||
var policy = await _savePolicyCommand.SaveAsync(policyUpdate);
|
||||
return new PolicyResponseModel(policy);
|
||||
}
|
||||
|
||||
|
||||
[HttpPut("{type}/vnext")]
|
||||
[RequireFeatureAttribute(FeatureFlagKeys.CreateDefaultLocation)]
|
||||
[Authorize<ManagePoliciesRequirement>]
|
||||
public async Task<PolicyResponseModel> PutVNext(Guid orgId, [FromBody] SavePolicyRequest model)
|
||||
{
|
||||
var savePolicyRequest = await model.ToSavePolicyModelAsync(orgId, _currentContext);
|
||||
|
||||
var policy = await _savePolicyCommand.VNextSaveAsync(savePolicyRequest);
|
||||
|
||||
return new PolicyResponseModel(policy);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
61
src/Api/AdminConsole/Models/Request/SavePolicyRequest.cs
Normal file
61
src/Api/AdminConsole/Models/Request/SavePolicyRequest.cs
Normal file
@ -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<string, object>? Metadata { get; set; }
|
||||
|
||||
public async Task<SavePolicyModel> 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<OrganizationModelOwnershipPolicyModel>(),
|
||||
_ => new EmptyMetadataModel()
|
||||
};
|
||||
}
|
||||
|
||||
private IPolicyMetadataModel MapToPolicyMetadata<T>() where T : IPolicyMetadataModel, new()
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(Metadata);
|
||||
return CoreHelpers.LoadClassFromJsonData<T>(json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new EmptyMetadataModel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -6,4 +6,10 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
public interface ISavePolicyCommand
|
||||
{
|
||||
Task<Policy> SaveAsync(PolicyUpdate policy);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
Task<Policy> VNextSaveAsync(SavePolicyModel policyRequest);
|
||||
}
|
||||
|
||||
@ -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<PolicyType, IPolicyValidator> _policyValidators;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IPostSavePolicySideEffect _postSavePolicySideEffect;
|
||||
|
||||
public SavePolicyCommand(
|
||||
IApplicationCacheService applicationCacheService,
|
||||
public SavePolicyCommand(IApplicationCacheService applicationCacheService,
|
||||
IEventService eventService,
|
||||
IPolicyRepository policyRepository,
|
||||
IEnumerable<IPolicyValidator> policyValidators,
|
||||
TimeProvider timeProvider)
|
||||
TimeProvider timeProvider,
|
||||
IPostSavePolicySideEffect postSavePolicySideEffect)
|
||||
{
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_eventService = eventService;
|
||||
_policyRepository = policyRepository;
|
||||
_timeProvider = timeProvider;
|
||||
_postSavePolicySideEffect = postSavePolicySideEffect;
|
||||
|
||||
var policyValidatorsDict = new Dictionary<PolicyType, IPolicyValidator>();
|
||||
foreach (var policyValidator in policyValidators)
|
||||
@ -78,12 +78,28 @@ public class SavePolicyCommand : ISavePolicyCommand
|
||||
return policy;
|
||||
}
|
||||
|
||||
public async Task<Policy> 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 @@ public class SavePolicyCommand : ISavePolicyCommand
|
||||
// Run side effects
|
||||
await validator.OnSaveSideEffectsAsync(policyUpdate, currentPolicy);
|
||||
}
|
||||
|
||||
private async Task<(Dictionary<PolicyType, Policy> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
|
||||
public record EmptyMetadataModel : IPolicyMetadataModel
|
||||
{
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
|
||||
public interface IPolicyMetadataModel
|
||||
{
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -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)
|
||||
{
|
||||
}
|
||||
@ -17,6 +17,7 @@ public static class PolicyServiceCollectionExtensions
|
||||
|
||||
services.AddPolicyValidators();
|
||||
services.AddPolicyRequirements();
|
||||
services.AddPolicySideEffects();
|
||||
}
|
||||
|
||||
private static void AddPolicyValidators(this IServiceCollection services)
|
||||
@ -27,8 +28,11 @@ public static class PolicyServiceCollectionExtensions
|
||||
services.AddScoped<IPolicyValidator, ResetPasswordPolicyValidator>();
|
||||
services.AddScoped<IPolicyValidator, MaximumVaultTimeoutPolicyValidator>();
|
||||
services.AddScoped<IPolicyValidator, FreeFamiliesForEnterprisePolicyValidator>();
|
||||
// This validator will be hooked up in https://bitwarden.atlassian.net/browse/PM-24279.
|
||||
// services.AddScoped<IPolicyValidator, OrganizationDataOwnershipPolicyValidator>();
|
||||
}
|
||||
|
||||
private static void AddPolicySideEffects(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IPostSavePolicySideEffect, OrganizationDataOwnershipPolicyValidator>();
|
||||
}
|
||||
|
||||
private static void AddPolicyRequirements(this IServiceCollection services)
|
||||
|
||||
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class OrganizationDataOwnershipPolicyValidator(
|
||||
IPolicyRepository policyRepository,
|
||||
ICollectionRepository collectionRepository,
|
||||
IEnumerable<IPolicyRequirementFactory<IPolicyRequirement>> factories,
|
||||
IFeatureService featureService,
|
||||
ILogger<OrganizationDataOwnershipPolicyValidator> logger)
|
||||
: OrganizationPolicyValidator(policyRepository, factories)
|
||||
IFeatureService featureService)
|
||||
: OrganizationPolicyValidator(policyRepository, factories), IPostSavePolicySideEffect
|
||||
{
|
||||
public override PolicyType Type => PolicyType.OrganizationDataOwnership;
|
||||
|
||||
public override IEnumerable<PolicyType> RequiredPolicies => [];
|
||||
|
||||
public override Task<string> 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<OrganizationDataOwnershipPolicyRequirement>(policyUpdate.OrganizationId, policyUpdate.Type);
|
||||
|
||||
@ -49,20 +60,13 @@ public class OrganizationDataOwnershipPolicyValidator(
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<IPolicyRequirementFactory<IPolicyRequirement>> factories) : IPolicyValidator
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public abstract class OrganizationPolicyValidator(IPolicyRepository policyRepository, IEnumerable<IPolicyRequirementFactory<IPolicyRequirement>> factories)
|
||||
{
|
||||
public abstract PolicyType Type { get; }
|
||||
|
||||
public abstract IEnumerable<PolicyType> RequiredPolicies { get; }
|
||||
|
||||
protected async Task<IEnumerable<T>> GetUserPolicyRequirementsByOrganizationIdAsync<T>(Guid organizationId, PolicyType policyType) where T : IPolicyRequirement
|
||||
{
|
||||
var factory = factories.OfType<IPolicyRequirementFactory<T>>().SingleOrDefault();
|
||||
@ -36,14 +35,4 @@ public abstract class OrganizationPolicyValidator(IPolicyRepository policyReposi
|
||||
|
||||
return requirements;
|
||||
}
|
||||
|
||||
public abstract Task OnSaveSideEffectsAsync(
|
||||
PolicyUpdate policyUpdate,
|
||||
Policy? currentPolicy
|
||||
);
|
||||
|
||||
public abstract Task<string> ValidateAsync(
|
||||
PolicyUpdate policyUpdate,
|
||||
Policy? currentPolicy
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<ApiApplicationFactory>, 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<Core.Services.IFeatureService>(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<string, object>
|
||||
{
|
||||
{ "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<ICollectionRepository>();
|
||||
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<PolicyResponseModel>();
|
||||
|
||||
Assert.True(content.Enabled);
|
||||
Assert.Equal(policyType, content.Type);
|
||||
Assert.Equal(_organization.Id, content.OrganizationId);
|
||||
}
|
||||
|
||||
async Task AssertPolicy()
|
||||
{
|
||||
var policyRepository = _factory.GetService<IPolicyRepository>();
|
||||
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<string, object>
|
||||
{
|
||||
{ "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<IPolicyRepository>();
|
||||
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<PolicyResponseModel>();
|
||||
|
||||
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<MasterPasswordPolicyData>();
|
||||
|
||||
var json = JsonSerializer.Serialize(request.Policy.Data);
|
||||
var expectedData = JsonSerializer.Deserialize<MasterPasswordPolicyData>(json);
|
||||
AssertHelper.AssertPropertyEqual(resultData, expectedData);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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<ICurrentContext>();
|
||||
currentContext.UserId.Returns(userId);
|
||||
currentContext.OrganizationOwner(organizationId).Returns(true);
|
||||
|
||||
var testData = new Dictionary<string, object> { { "test", "value" } };
|
||||
var model = new SavePolicyRequest
|
||||
{
|
||||
Policy = new PolicyRequestModel
|
||||
{
|
||||
Type = PolicyType.TwoFactorAuthentication,
|
||||
Enabled = true,
|
||||
Data = testData
|
||||
},
|
||||
Metadata = new Dictionary<string, object>()
|
||||
};
|
||||
|
||||
// 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<Dictionary<string, object>>(result.PolicyUpdate.Data);
|
||||
Assert.Equal("value", deserializedData["test"].ToString());
|
||||
|
||||
Assert.Equal(userId, result!.PerformedBy.UserId);
|
||||
Assert.True(result!.PerformedBy.IsOrganizationOwnerOrProvider);
|
||||
|
||||
Assert.IsType<EmptyMetadataModel>(result.Metadata);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ToSavePolicyModelAsync_WithNullData_HandlesCorrectly(
|
||||
Guid organizationId,
|
||||
Guid userId)
|
||||
{
|
||||
// Arrange
|
||||
var currentContext = Substitute.For<ICurrentContext>();
|
||||
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<ICurrentContext>();
|
||||
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<ICurrentContext>();
|
||||
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<string, object>
|
||||
{
|
||||
{ "defaultUserCollectionName", defaultCollectionName }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<OrganizationModelOwnershipPolicyModel>(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<ICurrentContext>();
|
||||
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<EmptyMetadataModel>(result.Metadata);
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, object> _complexData = new Dictionary<string,
|
||||
object>
|
||||
{
|
||||
{ "stringValue", "test" },
|
||||
{ "numberValue", 42 },
|
||||
{ "boolValue", true },
|
||||
{ "arrayValue", new[] { "item1", "item2" } },
|
||||
{ "nestedObject", new Dictionary<string, object> { { "nested", "value" } } }
|
||||
};
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ToSavePolicyModelAsync_ComplexData_SerializesCorrectly(
|
||||
Guid organizationId,
|
||||
Guid userId)
|
||||
{
|
||||
// Arrange
|
||||
var currentContext = Substitute.For<ICurrentContext>();
|
||||
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<string, object>()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
|
||||
|
||||
// Assert
|
||||
var deserializedData = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(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<ICurrentContext>();
|
||||
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<string, object>
|
||||
{
|
||||
{ "someProperty", "someValue" }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<EmptyMetadataModel>(result.Metadata);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task MapToPolicyMetadata_JsonSerializationException_ReturnsEmptyMetadata(
|
||||
Guid organizationId,
|
||||
Guid userId)
|
||||
{
|
||||
// Arrange
|
||||
var currentContext = Substitute.For<ICurrentContext>();
|
||||
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<EmptyMetadataModel>(result.Metadata);
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> BuildErrorDictionary()
|
||||
{
|
||||
var circularDict = new Dictionary<string, object>();
|
||||
circularDict["self"] = circularDict;
|
||||
return circularDict;
|
||||
}
|
||||
}
|
||||
@ -18,7 +18,7 @@ internal class PolicyUpdateCustomization(PolicyType type, bool enabled) : ICusto
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
@ -10,7 +10,6 @@ using Bit.Core.Services;
|
||||
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<OrganizationDataOwnershipPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
@ -32,95 +32,102 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
.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<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<List<Guid>>(), Arg.Any<string>());
|
||||
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[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<OrganizationDataOwnershipPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
currentPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
policyUpdate.Enabled = true;
|
||||
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.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<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<List<Guid>>(), Arg.Any<string>());
|
||||
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[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<OrganizationDataOwnershipPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
currentPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
|
||||
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.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<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<List<Guid>>(), Arg.Any<string>());
|
||||
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[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<ICollectionRepository>();
|
||||
var logger = Substitute.For<ILogger<OrganizationDataOwnershipPolicyValidator>>();
|
||||
|
||||
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<Guid>(),
|
||||
Arg.Any<List<Guid>>(),
|
||||
Arg.Any<IEnumerable<Guid>>(),
|
||||
Arg.Any<string>());
|
||||
|
||||
const string expectedErrorMessage = "No UserOrganizationIds found for";
|
||||
|
||||
logger.Received(1).Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o => (o.ToString() ?? "").Contains(expectedErrorMessage)),
|
||||
Arg.Any<Exception>(),
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
await policyRepository
|
||||
.Received(1)
|
||||
.GetPolicyDetailsByOrganizationIdAsync(
|
||||
policyUpdate.OrganizationId,
|
||||
PolicyType.OrganizationDataOwnership);
|
||||
}
|
||||
|
||||
public static IEnumerable<object?[]> ShouldUpsertDefaultCollectionsTestCases()
|
||||
@ -133,13 +140,13 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
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 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
|
||||
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<OrganizationPolicyDetails> 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<ICollectionRepository>();
|
||||
var logger = Substitute.For<ILogger<OrganizationDataOwnershipPolicyValidator>>();
|
||||
|
||||
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 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
_defaultUserCollectionName);
|
||||
}
|
||||
|
||||
private static IPolicyRepository ArrangePolicyRepositoryWithOutUsers()
|
||||
private static IEnumerable<object?[]> WhenDefaultCollectionsDoesNotExistTestCases()
|
||||
{
|
||||
return ArrangePolicyRepository([]);
|
||||
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<OrganizationDataOwnershipPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
|
||||
policyUpdate.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, null, metadata);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
private static IPolicyRepository ArrangePolicyRepository(IEnumerable<OrganizationPolicyDetails> policyDetails)
|
||||
@ -222,17 +262,15 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
private static OrganizationDataOwnershipPolicyValidator ArrangeSut(
|
||||
OrganizationDataOwnershipPolicyRequirementFactory factory,
|
||||
IPolicyRepository policyRepository,
|
||||
ICollectionRepository collectionRepository,
|
||||
ILogger<OrganizationDataOwnershipPolicyValidator> logger = null!)
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
logger ??= Substitute.For<ILogger<OrganizationDataOwnershipPolicyValidator>>();
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@ -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 class TestOrganizationPolicyValidator : OrganizationPolicyValidator
|
||||
{
|
||||
}
|
||||
|
||||
public override PolicyType Type => PolicyType.TwoFactorAuthentication;
|
||||
|
||||
public override IEnumerable<PolicyType> RequiredPolicies => [];
|
||||
|
||||
public override Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||
{
|
||||
return Task.FromResult("");
|
||||
}
|
||||
|
||||
public override Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<T>> TestGetUserPolicyRequirementsByOrganizationIdAsync<T>(Guid organizationId, PolicyType policyType)
|
||||
where T : IPolicyRequirement
|
||||
{
|
||||
|
||||
@ -94,8 +94,8 @@ public class SavePolicyCommandTests
|
||||
Substitute.For<IEventService>(),
|
||||
Substitute.For<IPolicyRepository>(),
|
||||
[new FakeSingleOrgPolicyValidator(), new FakeSingleOrgPolicyValidator()],
|
||||
Substitute.For<TimeProvider>()
|
||||
));
|
||||
Substitute.For<TimeProvider>(),
|
||||
Substitute.For<IPostSavePolicySideEffect>()));
|
||||
Assert.Contains("Duplicate PolicyValidator for SingleOrg policy", exception.Message);
|
||||
}
|
||||
|
||||
@ -281,6 +281,85 @@ public class SavePolicyCommandTests
|
||||
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<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)
|
||||
.Returns(currentPolicy);
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns([currentPolicy]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.VNextSaveAsync(savePolicyModel);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IPolicyRepository>()
|
||||
.Received(1)
|
||||
.UpsertAsync(result);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogPolicyEventAsync(result, EventType.Policy_Updated);
|
||||
|
||||
await sutProvider.GetDependency<IPostSavePolicySideEffect>()
|
||||
.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<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)
|
||||
.Returns(currentPolicy);
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns([currentPolicy]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.VNextSaveAsync(savePolicyModel);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IPolicyRepository>()
|
||||
.Received(1)
|
||||
.UpsertAsync(result);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogPolicyEventAsync(result, EventType.Policy_Updated);
|
||||
|
||||
await sutProvider.GetDependency<IPostSavePolicySideEffect>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.ExecuteSideEffectsAsync(default!, default!, default!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new SutProvider with the PolicyValidators registered in the Sut.
|
||||
/// </summary>
|
||||
@ -289,6 +368,7 @@ public class SavePolicyCommandTests
|
||||
return new SutProvider<SavePolicyCommand>()
|
||||
.WithFakeTimeProvider()
|
||||
.SetDependency(policyValidators ?? [])
|
||||
.SetDependency(Substitute.For<IPostSavePolicySideEffect>())
|
||||
.Create();
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user