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:
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user