mirror of
https://github.com/bitwarden/server
synced 2025-12-10 05:13:48 +00:00
[PM-26377] Add Auto Confirm Policy (#6552)
* First pass at adding Automatic User Confirmation Policy. * Adding edge case tests. Adding side effect of updating organization feature. Removing account recovery restriction from validation. * Added implementation for the vnext save * Added documentation to different event types with remarks. Updated IPolicyValidator xml docs.
This commit is contained in:
@@ -9,6 +9,10 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
/// <summary>
|
||||
/// Defines behavior and functionality for a given PolicyType.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// All methods defined in this interface are for the PolicyService#SavePolicy method. This needs to be supported until
|
||||
/// we successfully refactor policy validators over to policy validation handlers
|
||||
/// </remarks>
|
||||
public interface IPolicyValidator
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -53,6 +53,7 @@ public static class PolicyServiceCollectionExtensions
|
||||
services.AddScoped<IPolicyUpdateEvent, FreeFamiliesForEnterprisePolicyValidator>();
|
||||
services.AddScoped<IPolicyUpdateEvent, OrganizationDataOwnershipPolicyValidator>();
|
||||
services.AddScoped<IPolicyUpdateEvent, UriMatchDefaultPolicyValidator>();
|
||||
services.AddScoped<IPolicyUpdateEvent, AutomaticUserConfirmationPolicyEventHandler>();
|
||||
}
|
||||
|
||||
private static void AddPolicyRequirements(this IServiceCollection services)
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Represents all policies required to be enabled before the given policy can be enabled.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This interface is intended for policy event handlers that mandate the activation of other policies
|
||||
/// as prerequisites for enabling the associated policy.
|
||||
/// </remarks>
|
||||
public interface IEnforceDependentPoliciesEvent : IPolicyUpdateEvent
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -3,6 +3,12 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Represents all side effects that should be executed before a policy is upserted.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This should be added to policy handlers that need to perform side effects before policy upserts.
|
||||
/// </remarks>
|
||||
public interface IOnPolicyPreUpdateEvent : IPolicyUpdateEvent
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the policy to be upserted.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used for the VNextSavePolicyCommand. All policy handlers should implement this interface.
|
||||
/// </remarks>
|
||||
public interface IPolicyUpdateEvent
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -3,12 +3,17 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Represents all validations that need to be run to enable or disable the given policy.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used for the VNextSavePolicyCommand. This optional but should be implemented for all policies that have
|
||||
/// certain requirements for the given organization.
|
||||
/// </remarks>
|
||||
public interface IPolicyValidationEvent : IPolicyUpdateEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs side effects after a policy is validated but before it is saved.
|
||||
/// For example, this can be used to remove non-compliant users from the organization.
|
||||
/// Implementation is optional; by default, it will not perform any side effects.
|
||||
/// Performs any validations required to enable or disable the policy.
|
||||
/// </summary>
|
||||
/// <param name="policyRequest">The policy save request containing the policy update and metadata</param>
|
||||
/// <param name="currentPolicy">The current policy, if any</param>
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an event handler for the Automatic User Confirmation policy.
|
||||
///
|
||||
/// This class validates that the following conditions are met:
|
||||
/// <ul>
|
||||
/// <li>The Single organization policy is enabled</li>
|
||||
/// <li>All organization users are compliant with the Single organization policy</li>
|
||||
/// <li>No provider users exist</li>
|
||||
/// </ul>
|
||||
///
|
||||
/// This class also performs side effects when the policy is being enabled or disabled. They are:
|
||||
/// <ul>
|
||||
/// <li>Sets the UseAutomaticUserConfirmation organization feature to match the policy update</li>
|
||||
/// </ul>
|
||||
/// </summary>
|
||||
public class AutomaticUserConfirmationPolicyEventHandler(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IPolicyRepository policyRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
TimeProvider timeProvider)
|
||||
: IPolicyValidator, IPolicyValidationEvent, IOnPolicyPreUpdateEvent, IEnforceDependentPoliciesEvent
|
||||
{
|
||||
public PolicyType Type => PolicyType.AutomaticUserConfirmation;
|
||||
public async Task ExecutePreUpsertSideEffectAsync(SavePolicyModel policyRequest, Policy? currentPolicy) =>
|
||||
await OnSaveSideEffectsAsync(policyRequest.PolicyUpdate, currentPolicy);
|
||||
|
||||
private const string _singleOrgPolicyNotEnabledErrorMessage =
|
||||
"The Single organization policy must be enabled before enabling the Automatically confirm invited users policy.";
|
||||
|
||||
private const string _usersNotCompliantWithSingleOrgErrorMessage =
|
||||
"All organization users must be compliant with the Single organization policy before enabling the Automatically confirm invited users policy. Please remove users who are members of multiple organizations.";
|
||||
|
||||
private const string _providerUsersExistErrorMessage =
|
||||
"The organization has users with the Provider user type. Please remove provider users before enabling the Automatically confirm invited users policy.";
|
||||
|
||||
public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];
|
||||
|
||||
public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||
{
|
||||
var isNotEnablingPolicy = policyUpdate is not { Enabled: true };
|
||||
var policyAlreadyEnabled = currentPolicy is { Enabled: true };
|
||||
if (isNotEnablingPolicy || policyAlreadyEnabled)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return await ValidateEnablingPolicyAsync(policyUpdate.OrganizationId);
|
||||
}
|
||||
|
||||
public async Task<string> ValidateAsync(SavePolicyModel savePolicyModel, Policy? currentPolicy) =>
|
||||
await ValidateAsync(savePolicyModel.PolicyUpdate, currentPolicy);
|
||||
|
||||
public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||
{
|
||||
var organization = await organizationRepository.GetByIdAsync(policyUpdate.OrganizationId);
|
||||
|
||||
if (organization is not null)
|
||||
{
|
||||
organization.UseAutomaticUserConfirmation = policyUpdate.Enabled;
|
||||
organization.RevisionDate = timeProvider.GetUtcNow().UtcDateTime;
|
||||
await organizationRepository.UpsertAsync(organization);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> ValidateEnablingPolicyAsync(Guid organizationId)
|
||||
{
|
||||
var singleOrgValidationError = await ValidateSingleOrgPolicyComplianceAsync(organizationId);
|
||||
if (!string.IsNullOrWhiteSpace(singleOrgValidationError))
|
||||
{
|
||||
return singleOrgValidationError;
|
||||
}
|
||||
|
||||
var providerValidationError = await ValidateNoProviderUsersAsync(organizationId);
|
||||
if (!string.IsNullOrWhiteSpace(providerValidationError))
|
||||
{
|
||||
return providerValidationError;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private async Task<string> ValidateSingleOrgPolicyComplianceAsync(Guid organizationId)
|
||||
{
|
||||
var singleOrgPolicy = await policyRepository.GetByOrganizationIdTypeAsync(organizationId, PolicyType.SingleOrg);
|
||||
if (singleOrgPolicy is not { Enabled: true })
|
||||
{
|
||||
return _singleOrgPolicyNotEnabledErrorMessage;
|
||||
}
|
||||
|
||||
return await ValidateUserComplianceWithSingleOrgAsync(organizationId);
|
||||
}
|
||||
|
||||
private async Task<string> ValidateUserComplianceWithSingleOrgAsync(Guid organizationId)
|
||||
{
|
||||
var organizationUsers = (await organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId))
|
||||
.Where(ou => ou.Status != OrganizationUserStatusType.Invited &&
|
||||
ou.Status != OrganizationUserStatusType.Revoked &&
|
||||
ou.UserId.HasValue)
|
||||
.ToList();
|
||||
|
||||
if (organizationUsers.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var hasNonCompliantUser = (await organizationUserRepository.GetManyByManyUsersAsync(
|
||||
organizationUsers.Select(ou => ou.UserId!.Value)))
|
||||
.Any(uo => uo.OrganizationId != organizationId &&
|
||||
uo.Status != OrganizationUserStatusType.Invited);
|
||||
|
||||
return hasNonCompliantUser ? _usersNotCompliantWithSingleOrgErrorMessage : string.Empty;
|
||||
}
|
||||
|
||||
private async Task<string> ValidateNoProviderUsersAsync(Guid organizationId)
|
||||
{
|
||||
var providerUsers = await providerUserRepository.GetManyByOrganizationAsync(organizationId);
|
||||
|
||||
return providerUsers.Count > 0 ? _providerUsersExistErrorMessage : string.Empty;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user