1
0
mirror of https://github.com/bitwarden/server synced 2025-12-14 07:13:39 +00:00

[PM-24279] Add vnext policy endpoint (#6253)

This commit is contained in:
Jimmy Vo
2025-09-10 10:13:04 -04:00
committed by GitHub
parent 52045b89fa
commit d43b00dad9
19 changed files with 908 additions and 136 deletions

View File

@@ -1,10 +1,13 @@
// FIXME: Update this file to be null safe and then delete the line below // FIXME: Update this file to be null safe and then delete the line below
#nullable disable #nullable disable
using Bit.Api.AdminConsole.Authorization;
using Bit.Api.AdminConsole.Authorization.Requirements;
using Bit.Api.AdminConsole.Models.Request; using Bit.Api.AdminConsole.Models.Request;
using Bit.Api.AdminConsole.Models.Response.Helpers; using Bit.Api.AdminConsole.Models.Response.Helpers;
using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
@@ -30,7 +33,6 @@ namespace Bit.Api.AdminConsole.Controllers;
public class PoliciesController : Controller public class PoliciesController : Controller
{ {
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IFeatureService _featureService;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery; private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
@@ -49,7 +51,6 @@ public class PoliciesController : Controller
GlobalSettings globalSettings, GlobalSettings globalSettings,
IDataProtectionProvider dataProtectionProvider, IDataProtectionProvider dataProtectionProvider,
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory, IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
IFeatureService featureService,
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery, IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
ISavePolicyCommand savePolicyCommand) ISavePolicyCommand savePolicyCommand)
@@ -63,7 +64,6 @@ public class PoliciesController : Controller
"OrganizationServiceDataProtector"); "OrganizationServiceDataProtector");
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
_featureService = featureService;
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery; _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
_savePolicyCommand = savePolicyCommand; _savePolicyCommand = savePolicyCommand;
} }
@@ -212,4 +212,18 @@ public class PoliciesController : Controller
var policy = await _savePolicyCommand.SaveAsync(policyUpdate); var policy = await _savePolicyCommand.SaveAsync(policyUpdate);
return new PolicyResponseModel(policy); 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);
}
} }

View 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();
}
}
}

View File

@@ -10,6 +10,10 @@ namespace Bit.Api.AdminConsole.Models.Response.Organizations;
public class PolicyResponseModel : ResponseModel public class PolicyResponseModel : ResponseModel
{ {
public PolicyResponseModel() : base("policy")
{
}
public PolicyResponseModel(Policy policy, string obj = "policy") public PolicyResponseModel(Policy policy, string obj = "policy")
: base(obj) : base(obj)
{ {

View File

@@ -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);
}

View File

@@ -6,4 +6,10 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;
public interface ISavePolicyCommand public interface ISavePolicyCommand
{ {
Task<Policy> SaveAsync(PolicyUpdate policy); 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);
} }

View File

@@ -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.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
@@ -17,18 +15,20 @@ public class SavePolicyCommand : ISavePolicyCommand
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly IReadOnlyDictionary<PolicyType, IPolicyValidator> _policyValidators; private readonly IReadOnlyDictionary<PolicyType, IPolicyValidator> _policyValidators;
private readonly TimeProvider _timeProvider; private readonly TimeProvider _timeProvider;
private readonly IPostSavePolicySideEffect _postSavePolicySideEffect;
public SavePolicyCommand( public SavePolicyCommand(IApplicationCacheService applicationCacheService,
IApplicationCacheService applicationCacheService,
IEventService eventService, IEventService eventService,
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
IEnumerable<IPolicyValidator> policyValidators, IEnumerable<IPolicyValidator> policyValidators,
TimeProvider timeProvider) TimeProvider timeProvider,
IPostSavePolicySideEffect postSavePolicySideEffect)
{ {
_applicationCacheService = applicationCacheService; _applicationCacheService = applicationCacheService;
_eventService = eventService; _eventService = eventService;
_policyRepository = policyRepository; _policyRepository = policyRepository;
_timeProvider = timeProvider; _timeProvider = timeProvider;
_postSavePolicySideEffect = postSavePolicySideEffect;
var policyValidatorsDict = new Dictionary<PolicyType, IPolicyValidator>(); var policyValidatorsDict = new Dictionary<PolicyType, IPolicyValidator>();
foreach (var policyValidator in policyValidators) foreach (var policyValidator in policyValidators)
@@ -78,12 +78,28 @@ public class SavePolicyCommand : ISavePolicyCommand
return policy; 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) private async Task RunValidatorAsync(IPolicyValidator validator, PolicyUpdate policyUpdate)
{ {
var savedPolicies = await _policyRepository.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId); var (savedPoliciesDict, currentPolicy) = await GetCurrentPolicyStateAsync(policyUpdate);
// 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);
// If enabling this policy - check that all policy requirements are satisfied // If enabling this policy - check that all policy requirements are satisfied
if (currentPolicy is not { Enabled: true } && policyUpdate.Enabled) if (currentPolicy is not { Enabled: true } && policyUpdate.Enabled)
@@ -127,4 +143,13 @@ public class SavePolicyCommand : ISavePolicyCommand
// Run side effects // Run side effects
await validator.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); 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);
}
} }

View File

@@ -0,0 +1,6 @@

namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
public record EmptyMetadataModel : IPolicyMetadataModel
{
}

View File

@@ -0,0 +1,6 @@

namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
public interface IPolicyMetadataModel
{
}

View File

@@ -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; }
}

View File

@@ -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)
{
}

View File

@@ -17,6 +17,7 @@ public static class PolicyServiceCollectionExtensions
services.AddPolicyValidators(); services.AddPolicyValidators();
services.AddPolicyRequirements(); services.AddPolicyRequirements();
services.AddPolicySideEffects();
} }
private static void AddPolicyValidators(this IServiceCollection services) private static void AddPolicyValidators(this IServiceCollection services)
@@ -27,8 +28,11 @@ public static class PolicyServiceCollectionExtensions
services.AddScoped<IPolicyValidator, ResetPasswordPolicyValidator>(); services.AddScoped<IPolicyValidator, ResetPasswordPolicyValidator>();
services.AddScoped<IPolicyValidator, MaximumVaultTimeoutPolicyValidator>(); services.AddScoped<IPolicyValidator, MaximumVaultTimeoutPolicyValidator>();
services.AddScoped<IPolicyValidator, FreeFamiliesForEnterprisePolicyValidator>(); 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) private static void AddPolicyRequirements(this IServiceCollection services)

View File

@@ -1,44 +1,55 @@
#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.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Microsoft.Extensions.Logging;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; 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( public class OrganizationDataOwnershipPolicyValidator(
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
ICollectionRepository collectionRepository, ICollectionRepository collectionRepository,
IEnumerable<IPolicyRequirementFactory<IPolicyRequirement>> factories, IEnumerable<IPolicyRequirementFactory<IPolicyRequirement>> factories,
IFeatureService featureService, IFeatureService featureService)
ILogger<OrganizationDataOwnershipPolicyValidator> logger) : OrganizationPolicyValidator(policyRepository, factories), IPostSavePolicySideEffect
: OrganizationPolicyValidator(policyRepository, factories)
{ {
public override PolicyType Type => PolicyType.OrganizationDataOwnership; public async Task ExecuteSideEffectsAsync(
SavePolicyModel policyRequest,
public override IEnumerable<PolicyType> RequiredPolicies => []; Policy postUpdatedPolicy,
Policy? previousPolicyState)
public override Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult("");
public override async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
{ {
if (!featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)) if (!featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation))
{ {
return; 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); var requirements = await GetUserPolicyRequirementsByOrganizationIdAsync<OrganizationDataOwnershipPolicyRequirement>(policyUpdate.OrganizationId, policyUpdate.Type);
@@ -49,20 +60,13 @@ public class OrganizationDataOwnershipPolicyValidator(
if (!userOrgIds.Any()) if (!userOrgIds.Any())
{ {
logger.LogError("No UserOrganizationIds found for {OrganizationId}", policyUpdate.OrganizationId);
return; return;
} }
await collectionRepository.UpsertDefaultCollectionsAsync( await collectionRepository.UpsertDefaultCollectionsAsync(
policyUpdate.OrganizationId, policyUpdate.OrganizationId,
userOrgIds, userOrgIds,
GetDefaultUserCollectionName()); defaultCollectionName);
} }
private static string GetDefaultUserCollectionName()
{
// TODO: https://bitwarden.atlassian.net/browse/PM-24279
const string temporaryPlaceHolderValue = "Default";
return temporaryPlaceHolderValue;
}
} }

View File

@@ -1,17 +1,16 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; 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 protected async Task<IEnumerable<T>> GetUserPolicyRequirementsByOrganizationIdAsync<T>(Guid organizationId, PolicyType policyType) where T : IPolicyRequirement
{ {
var factory = factories.OfType<IPolicyRequirementFactory<T>>().SingleOrDefault(); var factory = factories.OfType<IPolicyRequirementFactory<T>>().SingleOrDefault();
@@ -36,14 +35,4 @@ public abstract class OrganizationPolicyValidator(IPolicyRepository policyReposi
return requirements; return requirements;
} }
public abstract Task OnSaveSideEffectsAsync(
PolicyUpdate policyUpdate,
Policy? currentPolicy
);
public abstract Task<string> ValidateAsync(
PolicyUpdate policyUpdate,
Policy? currentPolicy
);
} }

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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) public override ICustomization GetCustomization(ParameterInfo parameter)
{ {

View File

@@ -10,7 +10,6 @@ using Bit.Core.Services;
using Bit.Core.Test.AdminConsole.AutoFixture; using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.Extensions.Logging;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
@@ -22,9 +21,10 @@ public class OrganizationDataOwnershipPolicyValidatorTests
private const string _defaultUserCollectionName = "Default"; private const string _defaultUserCollectionName = "Default";
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task OnSaveSideEffectsAsync_FeatureFlagDisabled_DoesNothing( public async Task ExecuteSideEffectsAsync_FeatureFlagDisabled_DoesNothing(
[PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, [PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy currentPolicy, [Policy(PolicyType.OrganizationDataOwnership, false)] Policy postUpdatedPolicy,
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,
SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider) SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)
{ {
// Arrange // Arrange
@@ -32,95 +32,102 @@ public class OrganizationDataOwnershipPolicyValidatorTests
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation) .IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(false); .Returns(false);
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act // Act
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert // Assert
await sutProvider.GetDependency<ICollectionRepository>() await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceive() .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] [Theory, BitAutoData]
public async Task OnSaveSideEffectsAsync_PolicyAlreadyEnabled_DoesNothing( public async Task ExecuteSideEffectsAsync_PolicyAlreadyEnabled_DoesNothing(
[PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, [PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate,
[Policy(PolicyType.OrganizationDataOwnership, true)] Policy currentPolicy, [Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy,
[Policy(PolicyType.OrganizationDataOwnership, true)] Policy previousPolicyState,
SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider) SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)
{ {
// Arrange // Arrange
currentPolicy.OrganizationId = policyUpdate.OrganizationId; postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
policyUpdate.Enabled = true; previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
sutProvider.GetDependency<IFeatureService>() sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation) .IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true); .Returns(true);
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act // Act
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert // Assert
await sutProvider.GetDependency<ICollectionRepository>() await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceive() .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] [Theory, BitAutoData]
public async Task OnSaveSideEffectsAsync_PolicyBeingDisabled_DoesNothing( public async Task ExecuteSideEffectsAsync_PolicyBeingDisabled_DoesNothing(
[PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate, [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) SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)
{ {
// Arrange // Arrange
currentPolicy.OrganizationId = policyUpdate.OrganizationId; previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
sutProvider.GetDependency<IFeatureService>() sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation) .IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true); .Returns(true);
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act // Act
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert // Assert
await sutProvider.GetDependency<ICollectionRepository>() await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceive() .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] [Theory, BitAutoData]
public async Task OnSaveSideEffectsAsync_WhenNoUsersExist_ShouldLogError( public async Task ExecuteSideEffectsAsync_WhenNoUsersExist_DoNothing(
[PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, [PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate,
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy currentPolicy, [Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy,
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,
OrganizationDataOwnershipPolicyRequirementFactory factory) OrganizationDataOwnershipPolicyRequirementFactory factory)
{ {
// Arrange // Arrange
currentPolicy.OrganizationId = policyUpdate.OrganizationId; postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
policyUpdate.Enabled = true; previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
var policyRepository = ArrangePolicyRepositoryWithOutUsers(); var policyRepository = ArrangePolicyRepository([]);
var collectionRepository = Substitute.For<ICollectionRepository>(); 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 // Act
await sut.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert // Assert
await collectionRepository await collectionRepository
.DidNotReceive() .DidNotReceive()
.UpsertDefaultCollectionsAsync( .UpsertDefaultCollectionsAsync(
Arg.Any<Guid>(), Arg.Any<Guid>(),
Arg.Any<List<Guid>>(), Arg.Any<IEnumerable<Guid>>(),
Arg.Any<string>()); Arg.Any<string>());
const string expectedErrorMessage = "No UserOrganizationIds found for"; await policyRepository
.Received(1)
logger.Received(1).Log( .GetPolicyDetailsByOrganizationIdAsync(
LogLevel.Error, policyUpdate.OrganizationId,
Arg.Any<EventId>(), PolicyType.OrganizationDataOwnership);
Arg.Is<object>(o => (o.ToString() ?? "").Contains(expectedErrorMessage)),
Arg.Any<Exception>(),
Arg.Any<Func<object, Exception?, string>>());
} }
public static IEnumerable<object?[]> ShouldUpsertDefaultCollectionsTestCases() public static IEnumerable<object?[]> ShouldUpsertDefaultCollectionsTestCases()
@@ -133,13 +140,13 @@ public class OrganizationDataOwnershipPolicyValidatorTests
object?[] WithExistingPolicy() object?[] WithExistingPolicy()
{ {
var organizationId = Guid.NewGuid(); var organizationId = Guid.NewGuid();
var policyUpdate = new PolicyUpdate var postUpdatedPolicy = new Policy
{ {
OrganizationId = organizationId, OrganizationId = organizationId,
Type = PolicyType.OrganizationDataOwnership, Type = PolicyType.OrganizationDataOwnership,
Enabled = true Enabled = true
}; };
var currentPolicy = new Policy var previousPolicyState = new Policy
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
OrganizationId = organizationId, OrganizationId = organizationId,
@@ -149,51 +156,53 @@ public class OrganizationDataOwnershipPolicyValidatorTests
return new object?[] return new object?[]
{ {
policyUpdate, postUpdatedPolicy,
currentPolicy previousPolicyState
}; };
} }
object?[] WithNoExistingPolicy() object?[] WithNoExistingPolicy()
{ {
var policyUpdate = new PolicyUpdate var postUpdatedPolicy = new Policy
{ {
OrganizationId = new Guid(), OrganizationId = new Guid(),
Type = PolicyType.OrganizationDataOwnership, Type = PolicyType.OrganizationDataOwnership,
Enabled = true Enabled = true
}; };
const Policy currentPolicy = null; const Policy previousPolicyState = null;
return new object?[] return new object?[]
{ {
policyUpdate, postUpdatedPolicy,
currentPolicy previousPolicyState
}; };
} }
} }
[Theory, BitAutoData] [Theory]
[BitMemberAutoData(nameof(ShouldUpsertDefaultCollectionsTestCases))] [BitMemberAutoData(nameof(ShouldUpsertDefaultCollectionsTestCases))]
public async Task OnSaveSideEffectsAsync_WithRequirements_ShouldUpsertDefaultCollections( public async Task ExecuteSideEffectsAsync_WithRequirements_ShouldUpsertDefaultCollections(
Policy postUpdatedPolicy,
Policy? previousPolicyState,
[PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate,
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy? currentPolicy,
[OrganizationPolicyDetails(PolicyType.OrganizationDataOwnership)] IEnumerable<OrganizationPolicyDetails> orgPolicyDetails, [OrganizationPolicyDetails(PolicyType.OrganizationDataOwnership)] IEnumerable<OrganizationPolicyDetails> orgPolicyDetails,
OrganizationDataOwnershipPolicyRequirementFactory factory) OrganizationDataOwnershipPolicyRequirementFactory factory)
{ {
// Arrange // Arrange
foreach (var policyDetail in orgPolicyDetails) var orgPolicyDetailsList = orgPolicyDetails.ToList();
foreach (var policyDetail in orgPolicyDetailsList)
{ {
policyDetail.OrganizationId = policyUpdate.OrganizationId; policyDetail.OrganizationId = policyUpdate.OrganizationId;
} }
var policyRepository = ArrangePolicyRepository(orgPolicyDetails); var policyRepository = ArrangePolicyRepository(orgPolicyDetailsList);
var collectionRepository = Substitute.For<ICollectionRepository>(); 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 // Act
await sut.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert // Assert
await collectionRepository await collectionRepository
@@ -204,9 +213,40 @@ public class OrganizationDataOwnershipPolicyValidatorTests
_defaultUserCollectionName); _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) private static IPolicyRepository ArrangePolicyRepository(IEnumerable<OrganizationPolicyDetails> policyDetails)
@@ -222,17 +262,15 @@ public class OrganizationDataOwnershipPolicyValidatorTests
private static OrganizationDataOwnershipPolicyValidator ArrangeSut( private static OrganizationDataOwnershipPolicyValidator ArrangeSut(
OrganizationDataOwnershipPolicyRequirementFactory factory, OrganizationDataOwnershipPolicyRequirementFactory factory,
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
ICollectionRepository collectionRepository, ICollectionRepository collectionRepository)
ILogger<OrganizationDataOwnershipPolicyValidator> logger = null!)
{ {
logger ??= Substitute.For<ILogger<OrganizationDataOwnershipPolicyValidator>>();
var featureService = Substitute.For<IFeatureService>(); var featureService = Substitute.For<IFeatureService>();
featureService featureService
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation) .IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true); .Returns(true);
var sut = new OrganizationDataOwnershipPolicyValidator(policyRepository, collectionRepository, [factory], featureService, logger); var sut = new OrganizationDataOwnershipPolicyValidator(policyRepository, collectionRepository, [factory], featureService);
return sut; return sut;
} }

View File

@@ -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.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
using Bit.Core.AdminConsole.Repositories; 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) public async Task<IEnumerable<T>> TestGetUserPolicyRequirementsByOrganizationIdAsync<T>(Guid organizationId, PolicyType policyType)
where T : IPolicyRequirement where T : IPolicyRequirement
{ {

View File

@@ -94,8 +94,8 @@ public class SavePolicyCommandTests
Substitute.For<IEventService>(), Substitute.For<IEventService>(),
Substitute.For<IPolicyRepository>(), Substitute.For<IPolicyRepository>(),
[new FakeSingleOrgPolicyValidator(), new FakeSingleOrgPolicyValidator()], [new FakeSingleOrgPolicyValidator(), new FakeSingleOrgPolicyValidator()],
Substitute.For<TimeProvider>() Substitute.For<TimeProvider>(),
)); Substitute.For<IPostSavePolicySideEffect>()));
Assert.Contains("Duplicate PolicyValidator for SingleOrg policy", exception.Message); Assert.Contains("Duplicate PolicyValidator for SingleOrg policy", exception.Message);
} }
@@ -281,6 +281,85 @@ public class SavePolicyCommandTests
await AssertPolicyNotSavedAsync(sutProvider); 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> /// <summary>
/// Returns a new SutProvider with the PolicyValidators registered in the Sut. /// Returns a new SutProvider with the PolicyValidators registered in the Sut.
/// </summary> /// </summary>
@@ -289,6 +368,7 @@ public class SavePolicyCommandTests
return new SutProvider<SavePolicyCommand>() return new SutProvider<SavePolicyCommand>()
.WithFakeTimeProvider() .WithFakeTimeProvider()
.SetDependency(policyValidators ?? []) .SetDependency(policyValidators ?? [])
.SetDependency(Substitute.For<IPostSavePolicySideEffect>())
.Create(); .Create();
} }