1
0
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:
Jared McCannon
2025-11-13 11:33:24 -06:00
committed by GitHub
parent 59a64af345
commit e7b4837be9
8 changed files with 791 additions and 3 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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