1
0
mirror of https://github.com/bitwarden/server synced 2025-12-26 05:03:18 +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
#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);
}
}

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 PolicyResponseModel() : base("policy")
{
}
public PolicyResponseModel(Policy policy, string obj = "policy")
: 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
{
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.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);
}
}

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.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)

View File

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

View File

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