1
0
mirror of https://github.com/bitwarden/server synced 2025-12-25 04:33:26 +00:00

[PM-27131] Auto confirm policy requirement (#6649)

* Added Auto confirm policy enforcement requirement. Includes strict single org enforcement along with blocking provider users from joining orgs with auto confirm enabled.
This commit is contained in:
Jared McCannon
2025-12-15 15:40:00 -06:00
committed by GitHub
parent bead4f1d5a
commit e646b91a50
20 changed files with 1488 additions and 238 deletions

View File

@@ -3,6 +3,7 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Models.Business.Tokenables;
@@ -34,6 +35,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
private readonly IFeatureService _featureService;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly IAutomaticUserConfirmationPolicyEnforcementValidator _automaticUserConfirmationPolicyEnforcementValidator;
public AcceptOrgUserCommand(
IDataProtectionProvider dataProtectionProvider,
@@ -46,7 +48,8 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
IFeatureService featureService,
IPolicyRequirementQuery policyRequirementQuery)
IPolicyRequirementQuery policyRequirementQuery,
IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator)
{
// TODO: remove data protector when old token validation removed
_dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose);
@@ -60,6 +63,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
_featureService = featureService;
_policyRequirementQuery = policyRequirementQuery;
_automaticUserConfirmationPolicyEnforcementValidator = automaticUserConfirmationPolicyEnforcementValidator;
}
public async Task<OrganizationUser> AcceptOrgUserByEmailTokenAsync(Guid organizationUserId, User user, string emailToken,
@@ -186,13 +190,19 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
}
}
// Enforce Single Organization Policy of organization user is trying to join
var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(user.Id);
var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId);
if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
{
await ValidateAutomaticUserConfirmationPolicyAsync(orgUser, allOrgUsers, user);
}
// Enforce Single Organization Policy of organization user is trying to join
var invitedSingleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
PolicyType.SingleOrg, OrganizationUserStatusType.Invited);
if (hasOtherOrgs && invitedSingleOrgPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
if (allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId)
&& invitedSingleOrgPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
{
throw new BadRequestException("You may not join this organization until you leave or remove all other organizations.");
}
@@ -255,4 +265,20 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
}
}
}
private async Task ValidateAutomaticUserConfirmationPolicyAsync(OrganizationUser orgUser,
ICollection<OrganizationUser> allOrgUsers, User user)
{
var error = (await _automaticUserConfirmationPolicyEnforcementValidator.IsCompliantAsync(
new AutomaticUserConfirmationPolicyEnforcementRequest(orgUser.OrganizationId, allOrgUsers, user)))
.Match(
error => error.Message,
_ => string.Empty
);
if (!string.IsNullOrEmpty(error))
{
throw new BadRequestException(error);
}
}
}

View File

@@ -1,6 +1,7 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Utilities.v2;
@@ -8,6 +9,7 @@ using Bit.Core.AdminConsole.Utilities.v2.Validation;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Services;
using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
@@ -16,6 +18,8 @@ public class AutomaticallyConfirmOrganizationUsersValidator(
IOrganizationUserRepository organizationUserRepository,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IPolicyRequirementQuery policyRequirementQuery,
IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator,
IUserService userService,
IPolicyRepository policyRepository) : IAutomaticallyConfirmOrganizationUsersValidator
{
public async Task<ValidationResult<AutomaticallyConfirmOrganizationUserValidationRequest>> ValidateAsync(
@@ -61,7 +65,7 @@ public class AutomaticallyConfirmOrganizationUsersValidator(
return Invalid(request, new UserDoesNotHaveTwoFactorEnabled());
}
if (await OrganizationUserConformsToSingleOrgPolicyAsync(request) is { } error)
if (await OrganizationUserConformsToAutomaticUserConfirmationPolicyAsync(request) is { } error)
{
return Invalid(request, error);
}
@@ -69,10 +73,8 @@ public class AutomaticallyConfirmOrganizationUsersValidator(
return Valid(request);
}
private async Task<bool> OrganizationHasAutomaticallyConfirmUsersPolicyEnabledAsync(
AutomaticallyConfirmOrganizationUserValidationRequest request) =>
await policyRepository.GetByOrganizationIdTypeAsync(request.OrganizationId,
PolicyType.AutomaticUserConfirmation) is { Enabled: true }
private async Task<bool> OrganizationHasAutomaticallyConfirmUsersPolicyEnabledAsync(AutomaticallyConfirmOrganizationUserValidationRequest request) =>
await policyRepository.GetByOrganizationIdTypeAsync(request.OrganizationId, PolicyType.AutomaticUserConfirmation) is { Enabled: true }
&& request.Organization is { UseAutomaticUserConfirmation: true };
private async Task<bool> OrganizationUserConformsToTwoFactorRequiredPolicyAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
@@ -87,30 +89,37 @@ public class AutomaticallyConfirmOrganizationUsersValidator(
.IsTwoFactorRequiredForOrganization(request.Organization!.Id);
}
private async Task<Error?> OrganizationUserConformsToSingleOrgPolicyAsync(
/// <summary>
/// Validates whether the specified organization user complies with the automatic user confirmation policy.
/// This includes checks across all organizations the user is associated with to ensure they meet the compliance criteria.
///
/// We are not checking single organization policy compliance here because automatically confirm users policy enforces
/// a stricter version and applies to all users. If you are compliant with Auto Confirm, you'll be in compliance with
/// Single Org.
/// </summary>
/// <param name="request">
/// The request model encapsulates the current organization, the user being validated, and all organization users associated
/// with that user.
/// </param>
/// <returns>
/// An <see cref="Error"/> if the user fails to meet the automatic user confirmation policy, or null if the validation succeeds.
/// </returns>
private async Task<Error?> OrganizationUserConformsToAutomaticUserConfirmationPolicyAsync(
AutomaticallyConfirmOrganizationUserValidationRequest request)
{
var allOrganizationUsersForUser = await organizationUserRepository
.GetManyByUserAsync(request.OrganizationUser!.UserId!.Value);
if (allOrganizationUsersForUser.Count == 1)
{
return null;
}
var user = await userService.GetUserByIdAsync(request.OrganizationUser!.UserId!.Value);
var policyRequirement = await policyRequirementQuery
.GetAsync<SingleOrganizationPolicyRequirement>(request.OrganizationUser!.UserId!.Value);
if (policyRequirement.IsSingleOrgEnabledForThisOrganization(request.Organization!.Id))
{
return new OrganizationEnforcesSingleOrgPolicy();
}
if (policyRequirement.IsSingleOrgEnabledForOrganizationsOtherThan(request.Organization.Id))
{
return new OtherOrganizationEnforcesSingleOrgPolicy();
}
return null;
return (await automaticUserConfirmationPolicyEnforcementValidator.IsCompliantAsync(
new AutomaticUserConfirmationPolicyEnforcementRequest(
request.OrganizationId,
allOrganizationUsersForUser,
user)))
.Match<Error?>(
error => error,
_ => null
);
}
}

View File

@@ -8,6 +8,9 @@ public record UserIsNotUserType() : BadRequestError("Only organization users wit
public record UserIsNotAccepted() : BadRequestError("Cannot confirm user that has not accepted the invitation.");
public record OrganizationUserIdIsInvalid() : BadRequestError("Invalid organization user id.");
public record UserDoesNotHaveTwoFactorEnabled() : BadRequestError("User does not have two-step login enabled.");
public record OrganizationEnforcesSingleOrgPolicy() : BadRequestError("Cannot confirm this member to the organization until they leave or remove all other organizations");
public record OtherOrganizationEnforcesSingleOrgPolicy() : BadRequestError("Cannot confirm this member to the organization because they are in another organization which forbids it.");
public record UserCannotBelongToAnotherOrganization() : BadRequestError("Cannot confirm this member to the organization until they leave or remove all other organizations");
public record OtherOrganizationDoesNotAllowOtherMembership() : BadRequestError("Cannot confirm this member to the organization because they are in another organization which forbids it.");
public record AutomaticallyConfirmUsersPolicyIsNotEnabled() : BadRequestError("Cannot confirm this member because the Automatically Confirm Users policy is not enabled.");
public record ProviderUsersCannotJoin() : BadRequestError("An organization the user is a part of has enabled Automatic User Confirmation policy, and it does not support provider users joining.");
public record UserCannotJoinProvider() : BadRequestError("An organization the user is a part of has enabled Automatic User Confirmation policy, and it does not support the user joining a provider.");
public record CurrentOrganizationUserIsNotPresentInRequest() : BadRequestError("The current organization user does not exist in the request.");

View File

@@ -4,6 +4,7 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
@@ -33,6 +34,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly IFeatureService _featureService;
private readonly ICollectionRepository _collectionRepository;
private readonly IAutomaticUserConfirmationPolicyEnforcementValidator _automaticUserConfirmationPolicyEnforcementValidator;
public ConfirmOrganizationUserCommand(
IOrganizationRepository organizationRepository,
@@ -47,7 +49,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
IDeviceRepository deviceRepository,
IPolicyRequirementQuery policyRequirementQuery,
IFeatureService featureService,
ICollectionRepository collectionRepository)
ICollectionRepository collectionRepository,
IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@@ -62,6 +65,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
_policyRequirementQuery = policyRequirementQuery;
_featureService = featureService;
_collectionRepository = collectionRepository;
_automaticUserConfirmationPolicyEnforcementValidator = automaticUserConfirmationPolicyEnforcementValidator;
}
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
@@ -127,6 +131,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
var organization = await _organizationRepository.GetByIdAsync(organizationId);
var allUsersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validSelectedUserIds);
var users = await _userRepository.GetManyAsync(validSelectedUserIds);
var usersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(validSelectedUserIds);
@@ -188,6 +193,25 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
await ValidateTwoFactorAuthenticationPolicyAsync(user, organizationId, userTwoFactorEnabled);
var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId);
if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
{
var error = (await _automaticUserConfirmationPolicyEnforcementValidator.IsCompliantAsync(
new AutomaticUserConfirmationPolicyEnforcementRequest(
organizationId,
userOrgs,
user)))
.Match(
error => new BadRequestException(error.Message),
_ => null
);
if (error is not null)
{
throw error;
}
}
var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg);
var otherSingleOrgPolicies =
singleOrgPolicies.Where(p => p.OrganizationId != organizationId);
@@ -267,8 +291,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
return;
}
var organizationDataOwnershipPolicy =
await _policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(organizationUser.UserId!.Value);
var organizationDataOwnershipPolicy = await _policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(organizationUser.UserId!.Value);
if (!organizationDataOwnershipPolicy.RequiresDefaultCollectionOnConfirm(organizationUser.OrganizationId))
{
return;
@@ -311,8 +334,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
return;
}
var policyEligibleOrganizationUserIds =
await _policyRequirementQuery.GetManyByOrganizationIdAsync<OrganizationDataOwnershipPolicyRequirement>(organizationId);
var policyEligibleOrganizationUserIds = await _policyRequirementQuery
.GetManyByOrganizationIdAsync<OrganizationDataOwnershipPolicyRequirement>(organizationId);
var eligibleOrganizationUserIds = confirmedOrganizationUsers
.Where(ou => policyEligibleOrganizationUserIds.Contains(ou.Id))

View File

@@ -4,6 +4,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
@@ -29,7 +30,8 @@ public class RestoreOrganizationUserCommand(
IUserRepository userRepository,
IOrganizationService organizationService,
IFeatureService featureService,
IPolicyRequirementQuery policyRequirementQuery) : IRestoreOrganizationUserCommand
IPolicyRequirementQuery policyRequirementQuery,
IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator) : IRestoreOrganizationUserCommand
{
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId)
{
@@ -300,6 +302,25 @@ public class RestoreOrganizationUserCommand(
{
throw new BadRequestException(user.Email + " is not compliant with the two-step login policy");
}
if (featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
{
var validationResult = await automaticUserConfirmationPolicyEnforcementValidator.IsCompliantAsync(
new AutomaticUserConfirmationPolicyEnforcementRequest(orgUser.OrganizationId,
allOrgUsers,
user!));
var badRequestException = validationResult.Match(
error => new BadRequestException(user.Email +
" is not compliant with the automatic user confirmation policy: " +
error.Message),
_ => null);
if (badRequestException is not null)
{
throw badRequestException;
}
}
}
private async Task<bool> IsTwoFactorRequiredForOrganizationAsync(Guid userId, Guid organizationId)

View File

@@ -3,6 +3,8 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Organizations.Models;
@@ -43,7 +45,9 @@ public class CloudOrganizationSignUpCommand(
IPushNotificationService pushNotificationService,
ICollectionRepository collectionRepository,
IDeviceRepository deviceRepository,
IPricingClient pricingClient) : ICloudOrganizationSignUpCommand
IPricingClient pricingClient,
IPolicyRequirementQuery policyRequirementQuery,
IFeatureService featureService) : ICloudOrganizationSignUpCommand
{
public async Task<SignUpOrganizationResponse> SignUpOrganizationAsync(OrganizationSignup signup)
{
@@ -237,6 +241,17 @@ public class CloudOrganizationSignUpCommand(
private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
{
if (featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
{
var requirement = await policyRequirementQuery.GetAsync<AutomaticUserConfirmationPolicyRequirement>(ownerId);
if (requirement.CannotCreateNewOrganization())
{
throw new BadRequestException("You may not create an organization. You belong to an organization " +
"which has a policy that prohibits you from being a member of any other organization.");
}
}
var anySingleOrgPolicies = await policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);
if (anySingleOrgPolicies)
{

View File

@@ -2,6 +2,8 @@
#nullable disable
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Entities;
@@ -28,6 +30,8 @@ public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand
private readonly IGlobalSettings _globalSettings;
private readonly IPolicyService _policyService;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IFeatureService _featureService;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
public InitPendingOrganizationCommand(
IOrganizationService organizationService,
@@ -37,7 +41,9 @@ public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand
IDataProtectionProvider dataProtectionProvider,
IGlobalSettings globalSettings,
IPolicyService policyService,
IOrganizationUserRepository organizationUserRepository
IOrganizationUserRepository organizationUserRepository,
IFeatureService featureService,
IPolicyRequirementQuery policyRequirementQuery
)
{
_organizationService = organizationService;
@@ -48,6 +54,8 @@ public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand
_globalSettings = globalSettings;
_policyService = policyService;
_organizationUserRepository = organizationUserRepository;
_featureService = featureService;
_policyRequirementQuery = policyRequirementQuery;
}
public async Task InitPendingOrganizationAsync(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken)
@@ -113,6 +121,17 @@ public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand
private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
{
if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
{
var requirement = await _policyRequirementQuery.GetAsync<AutomaticUserConfirmationPolicyRequirement>(ownerId);
if (requirement.CannotCreateNewOrganization())
{
throw new BadRequestException("You may not create an organization. You belong to an organization " +
"which has a policy that prohibits you from being a member of any other organization.");
}
}
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);
if (anySingleOrgPolicies)
{

View File

@@ -2,6 +2,8 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Services;
@@ -31,6 +33,8 @@ public class SelfHostedOrganizationSignUpCommand : ISelfHostedOrganizationSignUp
private readonly IPolicyService _policyService;
private readonly IGlobalSettings _globalSettings;
private readonly IStripePaymentService _paymentService;
private readonly IFeatureService _featureService;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
public SelfHostedOrganizationSignUpCommand(
IOrganizationRepository organizationRepository,
@@ -44,7 +48,9 @@ public class SelfHostedOrganizationSignUpCommand : ISelfHostedOrganizationSignUp
ILicensingService licensingService,
IPolicyService policyService,
IGlobalSettings globalSettings,
IStripePaymentService paymentService)
IStripePaymentService paymentService,
IFeatureService featureService,
IPolicyRequirementQuery policyRequirementQuery)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@@ -58,6 +64,8 @@ public class SelfHostedOrganizationSignUpCommand : ISelfHostedOrganizationSignUp
_policyService = policyService;
_globalSettings = globalSettings;
_paymentService = paymentService;
_featureService = featureService;
_policyRequirementQuery = policyRequirementQuery;
}
public async Task<(Organization organization, OrganizationUser? organizationUser)> SignUpAsync(
@@ -103,6 +111,17 @@ public class SelfHostedOrganizationSignUpCommand : ISelfHostedOrganizationSignUp
private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
{
if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
{
var requirement = await _policyRequirementQuery.GetAsync<AutomaticUserConfirmationPolicyRequirement>(ownerId);
if (requirement.CannotCreateNewOrganization())
{
throw new BadRequestException("You may not create an organization. You belong to an organization " +
"which has a policy that prohibits you from being a member of any other organization.");
}
}
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);
if (anySingleOrgPolicies)
{

View File

@@ -0,0 +1,44 @@
using Bit.Core.Entities;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
/// <summary>
/// Request object for <see cref="AutomaticUserConfirmationPolicyEnforcementValidator"/>
/// </summary>
public record AutomaticUserConfirmationPolicyEnforcementRequest
{
/// <summary>
/// Organization to be validated
/// </summary>
public Guid OrganizationId { get; }
/// <summary>
/// All organization users that match the provided user.
/// </summary>
public ICollection<OrganizationUser> AllOrganizationUsers { get; }
/// <summary>
/// User associated with the organization user to be confirmed
/// </summary>
public User User { get; }
/// <summary>
/// Request object for <see cref="AutomaticUserConfirmationPolicyEnforcementValidator"/>.
/// </summary>
/// <remarks>
/// This record is used to encapsulate the data required for handling the automatic confirmation policy enforcement.
/// </remarks>
/// <param name="organizationId">The organization to be validated.</param>
/// <param name="organizationUsers">All organization users that match the provided user.</param>
/// <param name="user">The user entity connecting all org users provided.</param>
public AutomaticUserConfirmationPolicyEnforcementRequest(
Guid organizationId,
IEnumerable<OrganizationUser> organizationUsers,
User user)
{
OrganizationId = organizationId;
AllOrganizationUsers = organizationUsers.ToArray();
User = user;
}
}

View File

@@ -0,0 +1,49 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Utilities.v2.Validation;
using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
public class AutomaticUserConfirmationPolicyEnforcementValidator(
IPolicyRequirementQuery policyRequirementQuery,
IProviderUserRepository providerUserRepository)
: IAutomaticUserConfirmationPolicyEnforcementValidator
{
public async Task<ValidationResult<AutomaticUserConfirmationPolicyEnforcementRequest>> IsCompliantAsync(
AutomaticUserConfirmationPolicyEnforcementRequest request)
{
var automaticUserConfirmationPolicyRequirement = await policyRequirementQuery
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(request.User.Id);
var currentOrganizationUser = request.AllOrganizationUsers
.FirstOrDefault(x => x.OrganizationId == request.OrganizationId
&& x.UserId == request.User.Id);
if (currentOrganizationUser is null)
{
return Invalid(request, new CurrentOrganizationUserIsNotPresentInRequest());
}
if (automaticUserConfirmationPolicyRequirement.IsEnabled(request.OrganizationId))
{
if ((await providerUserRepository.GetManyByUserAsync(request.User.Id)).Count != 0)
{
return Invalid(request, new ProviderUsersCannotJoin());
}
if (request.AllOrganizationUsers.Count > 1)
{
return Invalid(request, new UserCannotBelongToAnotherOrganization());
}
}
if (automaticUserConfirmationPolicyRequirement.IsEnabledForOrganizationsOtherThan(currentOrganizationUser.OrganizationId))
{
return Invalid(request, new OtherOrganizationDoesNotAllowOtherMembership());
}
return Valid(request);
}
}

View File

@@ -0,0 +1,28 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Utilities.v2.Validation;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
/// <summary>
/// Used to enforce the Automatic User Confirmation policy. It uses the <see cref="IPolicyRequirementQuery"/> to retrieve
/// the <see cref="AutomaticUserConfirmationPolicyRequirement"/>. It is used to check to make sure the given user is
/// valid for the Automatic User Confirmation policy. It also validates that the given user is not a provider
/// or a member of another organization regardless of status or type.
/// </summary>
public interface IAutomaticUserConfirmationPolicyEnforcementValidator
{
/// <summary>
/// Checks if the given user is compliant with the Automatic User Confirmation policy.
///
/// To be compliant, a user must
/// - not be a member of a provider
/// - not be a member of another organization
/// </summary>
/// <param name="request"></param>
/// <remarks>
/// This uses the validation result pattern to avoid throwing exceptions.
/// </remarks>
/// <returns>A validation result with the error message if applicable.</returns>
Task<ValidationResult<AutomaticUserConfirmationPolicyEnforcementRequest>> IsCompliantAsync(AutomaticUserConfirmationPolicyEnforcementRequest request);
}

View File

@@ -0,0 +1,48 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.Enums;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
/// <summary>
/// Represents the enforcement status of the Automatic User Confirmation policy.
/// </summary>
/// <remarks>
/// The Automatic User Confirmation policy is enforced against all types of users regardless of status or type.
///
/// Users cannot:
/// <ul>
/// <li>Be a member of another organization (similar to Single Organization Policy)</li>
/// <li>Cannot be a provider</li>
/// </ul>
/// </remarks>
/// <param name="policyDetails">Collection of policy details that apply to this user id</param>
public class AutomaticUserConfirmationPolicyRequirement(IEnumerable<PolicyDetails> policyDetails) : IPolicyRequirement
{
public bool CannotBeGrantedEmergencyAccess() => policyDetails.Any();
public bool CannotJoinProvider() => policyDetails.Any();
public bool CannotCreateProvider() => policyDetails.Any();
public bool CannotCreateNewOrganization() => policyDetails.Any();
public bool IsEnabled(Guid organizationId) => policyDetails.Any(p => p.OrganizationId == organizationId);
public bool IsEnabledForOrganizationsOtherThan(Guid organizationId) =>
policyDetails.Any(p => p.OrganizationId != organizationId);
}
public class AutomaticUserConfirmationPolicyRequirementFactory : BasePolicyRequirementFactory<AutomaticUserConfirmationPolicyRequirement>
{
public override PolicyType PolicyType => PolicyType.AutomaticUserConfirmation;
protected override IEnumerable<OrganizationUserType> ExemptRoles => [];
protected override IEnumerable<OrganizationUserStatusType> ExemptStatuses => [];
protected override bool ExemptProviders => false;
public override AutomaticUserConfirmationPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails) =>
new(policyDetails);
}

View File

@@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
@@ -23,6 +24,8 @@ public static class PolicyServiceCollectionExtensions
services.AddPolicyRequirements();
services.AddPolicySideEffects();
services.AddPolicyUpdateEvents();
services.AddScoped<IAutomaticUserConfirmationPolicyEnforcementValidator, AutomaticUserConfirmationPolicyEnforcementValidator>();
}
[Obsolete("Use AddPolicyUpdateEvents instead.")]
@@ -69,5 +72,6 @@ public static class PolicyServiceCollectionExtensions
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireTwoFactorPolicyRequirementFactory>();
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, MasterPasswordPolicyRequirementFactory>();
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SingleOrganizationPolicyRequirementFactory>();
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, AutomaticUserConfirmationPolicyRequirementFactory>();
}
}