1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-07 02:52:50 -05:00

[PM-14439] Add PolicyRequirementQuery for enforcement logic (#5336)

* Add PolicyRequirementQuery, helpers and models in preparation for migrating domain code

Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com>
This commit is contained in:
Thomas Rittson
2025-02-14 21:05:49 +10:00
committed by GitHub
parent 54d59b3b92
commit f4341b2f3b
14 changed files with 795 additions and 0 deletions

View File

@ -0,0 +1,39 @@
#nullable enable
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
/// <summary>
/// Represents an OrganizationUser and a Policy which *may* be enforced against them.
/// You may assume that the Policy is enabled and that the organization's plan supports policies.
/// This is consumed by <see cref="IPolicyRequirement"/> to create requirements for specific policy types.
/// </summary>
public class PolicyDetails
{
public Guid OrganizationUserId { get; set; }
public Guid OrganizationId { get; set; }
public PolicyType PolicyType { get; set; }
public string? PolicyData { get; set; }
public OrganizationUserType OrganizationUserType { get; set; }
public OrganizationUserStatusType OrganizationUserStatus { get; set; }
/// <summary>
/// Custom permissions for the organization user, if any. Use <see cref="GetOrganizationUserCustomPermissions"/>
/// to deserialize.
/// </summary>
public string? OrganizationUserPermissionsData { get; set; }
/// <summary>
/// True if the user is also a ProviderUser for the organization, false otherwise.
/// </summary>
public bool IsProvider { get; set; }
public T GetDataModel<T>() where T : IPolicyDataModel, new()
=> CoreHelpers.LoadClassFromJsonData<T>(PolicyData);
public Permissions GetOrganizationUserCustomPermissions()
=> CoreHelpers.LoadClassFromJsonData<Permissions>(OrganizationUserPermissionsData);
}

View File

@ -0,0 +1,18 @@
#nullable enable
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;
public interface IPolicyRequirementQuery
{
/// <summary>
/// Get a policy requirement for a specific user.
/// The policy requirement represents how one or more policy types should be enforced against the user.
/// It will always return a value even if there are no policies that should be enforced.
/// This should be used for all policy checks.
/// </summary>
/// <param name="userId">The user that you need to enforce the policy against.</param>
/// <typeparam name="T">The IPolicyRequirement that corresponds to the policy you want to enforce.</typeparam>
Task<T> GetAsync<T>(Guid userId) where T : IPolicyRequirement;
}

View File

@ -0,0 +1,28 @@
#nullable enable
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
public class PolicyRequirementQuery(
IPolicyRepository policyRepository,
IEnumerable<RequirementFactory<IPolicyRequirement>> factories)
: IPolicyRequirementQuery
{
public async Task<T> GetAsync<T>(Guid userId) where T : IPolicyRequirement
{
var factory = factories.OfType<RequirementFactory<T>>().SingleOrDefault();
if (factory is null)
{
throw new NotImplementedException("No Policy Requirement found for " + typeof(T));
}
return factory(await GetPolicyDetails(userId));
}
private Task<IEnumerable<PolicyDetails>> GetPolicyDetails(Guid userId) =>
policyRepository.GetPolicyDetailsByUserId(userId);
}

View File

@ -0,0 +1,24 @@
#nullable enable
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
/// <summary>
/// Represents the business requirements of how one or more enterprise policies will be enforced against a user.
/// The implementation of this interface will depend on how the policies are enforced in the relevant domain.
/// </summary>
public interface IPolicyRequirement;
/// <summary>
/// A factory function that takes a sequence of <see cref="PolicyDetails"/> and transforms them into a single
/// <see cref="IPolicyRequirement"/> for consumption by the relevant domain. This will receive *all* policy types
/// that may be enforced against a user; when implementing this delegate, you must filter out irrelevant policy types
/// as well as policies that should not be enforced against a user (e.g. due to the user's role or status).
/// </summary>
/// <remarks>
/// See <see cref="PolicyRequirementHelpers"/> for extension methods to handle common requirements when implementing
/// this delegate.
/// </remarks>
public delegate T RequirementFactory<out T>(IEnumerable<PolicyDetails> policyDetails)
where T : IPolicyRequirement;

View File

@ -0,0 +1,41 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.Enums;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
public static class PolicyRequirementHelpers
{
/// <summary>
/// Filters the PolicyDetails by PolicyType. This is generally required to only get the PolicyDetails that your
/// IPolicyRequirement relates to.
/// </summary>
public static IEnumerable<PolicyDetails> GetPolicyType(
this IEnumerable<PolicyDetails> policyDetails,
PolicyType type)
=> policyDetails.Where(x => x.PolicyType == type);
/// <summary>
/// Filters the PolicyDetails to remove the specified user roles. This can be used to exempt
/// owners and admins from policy enforcement.
/// </summary>
public static IEnumerable<PolicyDetails> ExemptRoles(
this IEnumerable<PolicyDetails> policyDetails,
IEnumerable<OrganizationUserType> roles)
=> policyDetails.Where(x => !roles.Contains(x.OrganizationUserType));
/// <summary>
/// Filters the PolicyDetails to remove organization users who are also provider users for the organization.
/// This can be used to exempt provider users from policy enforcement.
/// </summary>
public static IEnumerable<PolicyDetails> ExemptProviders(this IEnumerable<PolicyDetails> policyDetails)
=> policyDetails.Where(x => !x.IsProvider);
/// <summary>
/// Filters the PolicyDetails to remove the specified organization user statuses. For example, this can be used
/// to exempt users in the invited and revoked statuses from policy enforcement.
/// </summary>
public static IEnumerable<PolicyDetails> ExemptStatus(
this IEnumerable<PolicyDetails> policyDetails, IEnumerable<OrganizationUserStatusType> status)
=> policyDetails.Where(x => !status.Contains(x.OrganizationUserStatus));
}

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
using Bit.Core.AdminConsole.Services;
using Bit.Core.AdminConsole.Services.Implementations;
@ -12,7 +13,14 @@ public static class PolicyServiceCollectionExtensions
{
services.AddScoped<IPolicyService, PolicyService>();
services.AddScoped<ISavePolicyCommand, SavePolicyCommand>();
services.AddScoped<IPolicyRequirementQuery, PolicyRequirementQuery>();
services.AddPolicyValidators();
services.AddPolicyRequirements();
}
private static void AddPolicyValidators(this IServiceCollection services)
{
services.AddScoped<IPolicyValidator, TwoFactorAuthenticationPolicyValidator>();
services.AddScoped<IPolicyValidator, SingleOrgPolicyValidator>();
services.AddScoped<IPolicyValidator, RequireSsoPolicyValidator>();
@ -20,4 +28,34 @@ public static class PolicyServiceCollectionExtensions
services.AddScoped<IPolicyValidator, MaximumVaultTimeoutPolicyValidator>();
services.AddScoped<IPolicyValidator, FreeFamiliesForEnterprisePolicyValidator>();
}
private static void AddPolicyRequirements(this IServiceCollection services)
{
// Register policy requirement factories here
}
/// <summary>
/// Used to register simple policy requirements where its factory method implements CreateRequirement.
/// This MUST be used rather than calling AddScoped directly, because it will ensure the factory method has
/// the correct type to be injected and then identified by <see cref="PolicyRequirementQuery"/> at runtime.
/// </summary>
/// <typeparam name="T">The specific PolicyRequirement being registered.</typeparam>
private static void AddPolicyRequirement<T>(this IServiceCollection serviceCollection, RequirementFactory<T> factory)
where T : class, IPolicyRequirement
=> serviceCollection.AddPolicyRequirement(_ => factory);
/// <summary>
/// Used to register policy requirements where you need to access additional dependencies (usually to return a
/// curried factory method).
/// This MUST be used rather than calling AddScoped directly, because it will ensure the factory method has
/// the correct type to be injected and then identified by <see cref="PolicyRequirementQuery"/> at runtime.
/// </summary>
/// <typeparam name="T">
/// A callback that takes IServiceProvider and returns a RequirementFactory for
/// your policy requirement.
/// </typeparam>
private static void AddPolicyRequirement<T>(this IServiceCollection serviceCollection,
Func<IServiceProvider, RequirementFactory<T>> factory)
where T : class, IPolicyRequirement
=> serviceCollection.AddScoped<RequirementFactory<IPolicyRequirement>>(factory);
}

View File

@ -1,5 +1,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.Repositories;
#nullable enable
@ -8,7 +10,25 @@ namespace Bit.Core.AdminConsole.Repositories;
public interface IPolicyRepository : IRepository<Policy, Guid>
{
/// <summary>
/// Gets all policies of a given type for an organization.
/// </summary>
/// <remarks>
/// WARNING: do not use this to enforce policies against a user! It returns raw data and does not take into account
/// various business rules. Use <see cref="IPolicyRequirementQuery"/> instead.
/// </remarks>
Task<Policy?> GetByOrganizationIdTypeAsync(Guid organizationId, PolicyType type);
Task<ICollection<Policy>> GetManyByOrganizationIdAsync(Guid organizationId);
Task<ICollection<Policy>> GetManyByUserIdAsync(Guid userId);
/// <summary>
/// Gets all PolicyDetails for a user for all policy types.
/// </summary>
/// <remarks>
/// Each PolicyDetail represents an OrganizationUser and a Policy which *may* be enforced
/// against them. It only returns PolicyDetails for policies that are enabled and where the organization's plan
/// supports policies. It also excludes "revoked invited" users who are not subject to policy enforcement.
/// This is consumed by <see cref="IPolicyRequirementQuery"/> to create requirements for specific policy types.
/// You probably do not want to call it directly.
/// </remarks>
Task<IEnumerable<PolicyDetails>> GetPolicyDetailsByUserId(Guid userId);
}