Enhance InitPendingOrganizationCommand with policy validation and feature flag support

Updated the ValidatePoliciesAsync method to enforce the Automatic User Confirmation Policy when the feature flag is enabled. Added new unit tests to cover scenarios for automatic user confirmation and single organization policy violations, ensuring comprehensive validation during organization initialization. This improves error handling and maintains compliance with organizational policies.
This commit is contained in:
Rui Tome
2026-01-12 14:53:11 +00:00
parent 5b572e2009
commit 79fe4cf015
2 changed files with 203 additions and 25 deletions

View File

@@ -268,8 +268,19 @@ public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand
private async Task<Error> ValidatePoliciesAsync(User user, Guid organizationId, Organization org, OrganizationUser orgUser)
{
var autoConfirmReq = await _policyRequirementQuery.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id);
if (autoConfirmReq.CannotCreateNewOrganization() || autoConfirmReq.IsEnabledForOrganizationsOtherThan(organizationId))
// Enforce Automatic User Confirmation Policy (when feature flag is enabled)
if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
{
var autoConfirmReq = await _policyRequirementQuery.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id);
if (autoConfirmReq.CannotCreateNewOrganization())
{
return new SingleOrgPolicyViolationError();
}
}
// Enforce Single Organization Policy
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg);
if (anySingleOrgPolicies)
{
return new SingleOrgPolicyViolationError();
}

View File

@@ -1,8 +1,10 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
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.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Enums;
@@ -198,9 +200,11 @@ public class InitPendingOrganizationCommandTests
var autoConfirmReq = new AutomaticUserConfirmationPolicyRequirement(new List<PolicyDetails>());
var twoFactorReq = new RequireTwoFactorPolicyRequirement(new List<PolicyDetails>());
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id).Returns(autoConfirmReq);
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
.Returns(autoConfirmReq);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id).Returns(twoFactorReq);
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(twoFactorReq);
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(
@@ -215,19 +219,46 @@ public class InitPendingOrganizationCommandTests
Assert.Equal(user.Id, orgUser.UserId);
Assert.Equal(userKey, orgUser.Key);
Assert.Null(orgUser.Email);
await sutProvider.GetDependency<IOrganizationService>().Received().UpdateAsync(org);
await sutProvider.GetDependency<IOrganizationUserRepository>().Received().ReplaceAsync(orgUser);
await sutProvider.GetDependency<IEventService>().Received()
await sutProvider.GetDependency<IOrganizationService>()
.Received(1)
.UpdateAsync(org);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.ReplaceAsync(orgUser);
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
}
[Theory, BitAutoData]
public async Task InitPendingOrganizationVNextAsync_WithNullOrgUser_ReturnsOrganizationUserNotFoundError(
User user, Guid orgId, Guid orgUserId, string publicKey, string privateKey, string userKey,
SutProvider<InitPendingOrganizationCommand> sutProvider)
{
// Arrange
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(orgUserId)
.Returns((OrganizationUser)null);
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(
user, orgId, orgUserId, publicKey, privateKey, "", "token", userKey);
// Assert
Assert.True(result.IsError);
Assert.IsType<OrganizationUserNotFoundError>(result.AsError);
}
[Theory, BitAutoData]
public async Task InitPendingOrganizationVNextAsync_WithInvalidToken_ReturnsInvalidTokenError(
User user, Guid orgId, Guid orgUserId, string publicKey, string privateKey, string userKey,
SutProvider<InitPendingOrganizationCommand> sutProvider, OrganizationUser orgUser)
{
// Arrange
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(orgUser);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(orgUserId)
.Returns(orgUser);
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(
@@ -249,7 +280,9 @@ public class InitPendingOrganizationCommandTests
var token = CreateToken(orgUser, orgUserId, sutProvider);
org.Enabled = true;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(org);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(orgId)
.Returns(org);
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(
@@ -272,7 +305,9 @@ public class InitPendingOrganizationCommandTests
org.Enabled = false;
org.Status = OrganizationStatusType.Created;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(org);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(orgId)
.Returns(org);
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(
@@ -296,7 +331,9 @@ public class InitPendingOrganizationCommandTests
org.Status = OrganizationStatusType.Pending;
org.PublicKey = "existing-key";
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(org);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(orgId)
.Returns(org);
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(
@@ -307,6 +344,29 @@ public class InitPendingOrganizationCommandTests
Assert.IsType<OrganizationHasKeysError>(result.AsError);
}
[Theory, BitAutoData]
public async Task InitPendingOrganizationVNextAsync_WithNullOrganization_ReturnsOrganizationNotFoundError(
User user, Guid orgId, Guid orgUserId, string publicKey, string privateKey, string userKey,
SutProvider<InitPendingOrganizationCommand> sutProvider, OrganizationUser orgUser)
{
// Arrange
orgUser.Email = user.Email;
orgUser.OrganizationId = orgId;
var token = CreateToken(orgUser, orgUserId, sutProvider);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(orgId)
.Returns((Organization)null);
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(
user, orgId, orgUserId, publicKey, privateKey, "", token, userKey);
// Assert
Assert.True(result.IsError);
Assert.IsType<OrganizationNotFoundError>(result.AsError);
}
[Theory, BitAutoData]
public async Task InitPendingOrganizationVNextAsync_WithEmailMismatch_ReturnsEmailMismatchError(
User user, Guid orgId, Guid orgUserId, string publicKey, string privateKey, string userKey,
@@ -317,7 +377,9 @@ public class InitPendingOrganizationCommandTests
orgUser.OrganizationId = orgId;
var token = CreateToken(orgUser, orgUserId, sutProvider);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(org);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(orgId)
.Returns(org);
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(
@@ -344,15 +406,21 @@ public class InitPendingOrganizationCommandTests
org.PrivateKey = null;
org.PublicKey = null;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(org);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>().TwoFactorIsEnabledAsync(user).Returns(true);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(orgId)
.Returns(org);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(user)
.Returns(true);
var autoConfirmReq = new AutomaticUserConfirmationPolicyRequirement(new List<PolicyDetails>());
var twoFactorReq = new RequireTwoFactorPolicyRequirement(new List<PolicyDetails>());
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id).Returns(autoConfirmReq);
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
.Returns(autoConfirmReq);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id).Returns(twoFactorReq);
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(twoFactorReq);
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(
@@ -380,17 +448,23 @@ public class InitPendingOrganizationCommandTests
org.PrivateKey = null;
org.PublicKey = null;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(org);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>().TwoFactorIsEnabledAsync(user).Returns(false);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(orgId)
.Returns(org);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(user)
.Returns(false);
var autoConfirmReq = new AutomaticUserConfirmationPolicyRequirement(new List<PolicyDetails>());
var twoFactorReq = new RequireTwoFactorPolicyRequirement(
new List<PolicyDetails> { new PolicyDetails { OrganizationId = orgId } });
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id).Returns(autoConfirmReq);
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
.Returns(autoConfirmReq);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id).Returns(twoFactorReq);
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(twoFactorReq);
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(
@@ -417,16 +491,21 @@ public class InitPendingOrganizationCommandTests
org.PublicKey = null;
org.PlanType = PlanType.Free;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(org);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(orgId)
.Returns(org);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetCountByFreeOrganizationAdminUserAsync(user.Id).Returns(1);
.GetCountByFreeOrganizationAdminUserAsync(user.Id)
.Returns(1);
var autoConfirmReq = new AutomaticUserConfirmationPolicyRequirement(new List<PolicyDetails>());
var twoFactorReq = new RequireTwoFactorPolicyRequirement(new List<PolicyDetails>());
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id).Returns(autoConfirmReq);
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
.Returns(autoConfirmReq);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id).Returns(twoFactorReq);
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(twoFactorReq);
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(
@@ -437,6 +516,92 @@ public class InitPendingOrganizationCommandTests
Assert.IsType<FreeOrgAdminLimitError>(result.AsError);
}
[Theory, BitAutoData]
public async Task InitPendingOrganizationVNextAsync_WithAutomaticUserConfirmationPolicy_ReturnsSingleOrgPolicyViolationError(
User user, Guid orgId, Guid orgUserId, Guid otherOrgId, string publicKey, string privateKey, string userKey,
SutProvider<InitPendingOrganizationCommand> sutProvider, Organization org, OrganizationUser orgUser)
{
// Arrange
orgUser.Email = user.Email;
orgUser.OrganizationId = orgId;
var token = CreateToken(orgUser, orgUserId, sutProvider);
org.Enabled = false;
org.Status = OrganizationStatusType.Pending;
org.PrivateKey = null;
org.PublicKey = null;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(org);
// Enable AutomaticConfirmUsers feature flag
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
// User is subject to AutomaticUserConfirmation policy from another organization
var autoConfirmReq = new AutomaticUserConfirmationPolicyRequirement(
new List<PolicyDetails> { new PolicyDetails { OrganizationId = otherOrgId } });
var twoFactorReq = new RequireTwoFactorPolicyRequirement(new List<PolicyDetails>());
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
.Returns(autoConfirmReq);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(twoFactorReq);
// No legacy SingleOrg policy
sutProvider.GetDependency<IPolicyService>()
.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg).Returns(false);
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(
user, orgId, orgUserId, publicKey, privateKey, "", token, userKey);
// Assert
Assert.True(result.IsError);
Assert.IsType<SingleOrgPolicyViolationError>(result.AsError);
}
[Theory, BitAutoData]
public async Task InitPendingOrganizationVNextAsync_WithSingleOrgPolicy_ReturnsSingleOrgPolicyViolationError(
User user, Guid orgId, Guid orgUserId, string publicKey, string privateKey, string userKey,
SutProvider<InitPendingOrganizationCommand> sutProvider, Organization org, OrganizationUser orgUser)
{
// Arrange
orgUser.Email = user.Email;
orgUser.OrganizationId = orgId;
var token = CreateToken(orgUser, orgUserId, sutProvider);
org.Enabled = false;
org.Status = OrganizationStatusType.Pending;
org.PrivateKey = null;
org.PublicKey = null;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(org);
var autoConfirmReq = new AutomaticUserConfirmationPolicyRequirement(new List<PolicyDetails>());
var twoFactorReq = new RequireTwoFactorPolicyRequirement(new List<PolicyDetails>());
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
.Returns(autoConfirmReq);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(twoFactorReq);
// User is subject to SingleOrg policy from another organization
sutProvider.GetDependency<IPolicyService>()
.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg)
.Returns(true);
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(
user, orgId, orgUserId, publicKey, privateKey, "", token, userKey);
// Assert
Assert.True(result.IsError);
Assert.IsType<SingleOrgPolicyViolationError>(result.AsError);
}
[Theory, BitAutoData]
public async Task InitPendingOrganizationVNextAsync_WithMismatchedOrganizationId_ReturnsOrganizationMismatchError(
User user, Guid orgId, Guid differentOrgId, Guid orgUserId, string publicKey, string privateKey, string userKey,
@@ -453,7 +618,9 @@ public class InitPendingOrganizationCommandTests
org.PrivateKey = null;
org.PublicKey = null;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(org);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(orgId)
.Returns(org);
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(