Implement InitPendingOrganizationValidator for improved organization initialization validation

- Introduced IInitPendingOrganizationValidator interface and its implementation to encapsulate validation logic for organization initialization.
- Refactored InitPendingOrganizationCommand to utilize the new validator for token validation, user email matching, organization state checks, and policy enforcement.
- Enhanced dependency injection in OrganizationServiceCollectionExtensions to include the new validator.
- Added comprehensive unit tests for the validator to ensure robust validation logic and error handling.
This commit is contained in:
Rui Tome
2026-01-30 16:28:53 +00:00
parent fd8044ce2a
commit dc18834aed
5 changed files with 730 additions and 174 deletions

View File

@@ -7,7 +7,6 @@ using Bit.Core.AdminConsole.Services;
using Bit.Core.AdminConsole.Utilities.v2.Results;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@@ -21,7 +20,6 @@ using Bit.Core.Tokens;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.DataProtection;
using OneOf.Types;
using Error = Bit.Core.AdminConsole.Utilities.v2.Error;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
@@ -44,6 +42,7 @@ public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand
private readonly IPushNotificationService _pushNotificationService;
private readonly IPushRegistrationService _pushRegistrationService;
private readonly IDeviceRepository _deviceRepository;
private readonly IInitPendingOrganizationValidator _validator;
public InitPendingOrganizationCommand(
IOrganizationService organizationService,
@@ -62,7 +61,8 @@ public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand
IUserRepository userRepository,
IPushNotificationService pushNotificationService,
IPushRegistrationService pushRegistrationService,
IDeviceRepository deviceRepository
IDeviceRepository deviceRepository,
IInitPendingOrganizationValidator validator
)
{
_organizationService = organizationService;
@@ -82,6 +82,7 @@ public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand
_pushNotificationService = pushNotificationService;
_pushRegistrationService = pushRegistrationService;
_deviceRepository = deviceRepository;
_validator = validator;
}
public async Task InitPendingOrganizationAsync(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken)
@@ -94,7 +95,7 @@ public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand
throw new BadRequestException("User invalid.");
}
var tokenValid = ValidateInviteToken(orgUser, user, emailToken);
var tokenValid = _validator.ValidateInviteToken(orgUser, user, emailToken);
if (!tokenValid)
{
@@ -170,14 +171,6 @@ public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand
}
}
private bool ValidateInviteToken(OrganizationUser orgUser, User user, string emailToken)
{
var tokenValid = OrgUserInviteTokenable.ValidateOrgUserInviteStringToken(
_orgUserInviteTokenDataFactory, emailToken, orgUser);
return tokenValid;
}
public async Task<CommandResult> InitPendingOrganizationVNextAsync(InitPendingOrganizationRequest request)
{
var orgUser = await _organizationUserRepository.GetByIdAsync(request.OrganizationUserId);
@@ -186,12 +179,12 @@ public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand
return new OrganizationUserNotFoundError();
}
if (!ValidateInviteToken(orgUser, request.User, request.EmailToken))
if (!_validator.ValidateInviteToken(orgUser, request.User, request.EmailToken))
{
return new InvalidTokenError();
}
var validationError = ValidateUserEmail(orgUser, request.User);
var validationError = _validator.ValidateUserEmail(orgUser, request.User);
if (validationError != null)
{
return validationError;
@@ -203,18 +196,25 @@ public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand
return new OrganizationNotFoundError();
}
if (orgUser.OrganizationId != request.OrganizationId)
{
return new OrganizationMismatchError();
}
validationError = ValidateOrganizationState(org);
validationError = _validator.ValidateOrganizationMatch(orgUser, request.OrganizationId);
if (validationError != null)
{
return validationError;
}
validationError = await ValidatePoliciesAsync(request.User, request.OrganizationId, org, orgUser);
validationError = _validator.ValidateOrganizationState(org);
if (validationError != null)
{
return validationError;
}
validationError = await _validator.ValidatePoliciesAsync(request.User, request.OrganizationId);
if (validationError != null)
{
return validationError;
}
validationError = await _validator.ValidateBusinessRulesAsync(request.User, org, orgUser);
if (validationError != null)
{
return validationError;
@@ -224,7 +224,6 @@ public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand
PrepareOrganizationUserForConfirmation(orgUser, request);
var updateActions = BuildDatabaseUpdateActions(org, orgUser, request);
await _organizationRepository.ExecuteOrganizationInitializationUpdatesAsync(updateActions);
await SendNotificationsAsync(org, orgUser, request.User, request.OrganizationId);
@@ -232,76 +231,6 @@ public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand
return new None();
}
private static Error? ValidateUserEmail(OrganizationUser orgUser, User user)
{
if (string.IsNullOrWhiteSpace(orgUser.Email) ||
!orgUser.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase))
{
return new EmailMismatchError();
}
return null;
}
private static Error? ValidateOrganizationState(Organization org)
{
if (org.Enabled)
{
return new OrganizationAlreadyEnabledError();
}
if (org.Status != OrganizationStatusType.Pending)
{
return new OrganizationNotPendingError();
}
if (!string.IsNullOrEmpty(org.PublicKey) || !string.IsNullOrEmpty(org.PrivateKey))
{
return new OrganizationHasKeysError();
}
return null;
}
private async Task<Error?> ValidatePoliciesAsync(User user, Guid organizationId, Organization org, OrganizationUser orgUser)
{
// 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();
}
var twoFactorReq = await _policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id);
if (twoFactorReq.IsTwoFactorRequiredForOrganization(organizationId) &&
!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
{
return new TwoFactorRequiredError();
}
if (org.PlanType == PlanType.Free &&
(orgUser.Type == OrganizationUserType.Owner || orgUser.Type == OrganizationUserType.Admin))
{
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(user.Id);
if (adminCount > 0)
{
return new FreeOrgAdminLimitError();
}
}
return null;
}
private static void PrepareOrganizationForInitialization(Organization org, InitPendingOrganizationRequest request)
{
org.Enabled = true;

View File

@@ -0,0 +1,164 @@
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.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tokens;
using Error = Bit.Core.AdminConsole.Utilities.v2.Error;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
public interface IInitPendingOrganizationValidator
{
/// <summary>
/// Validates the invite token for an organization user.
/// </summary>
bool ValidateInviteToken(OrganizationUser orgUser, User user, string emailToken);
/// <summary>
/// Validates that the user's email matches the organization user's email.
/// </summary>
Error? ValidateUserEmail(OrganizationUser orgUser, User user);
/// <summary>
/// Validates that the organization is in the correct state for initialization.
/// </summary>
Error? ValidateOrganizationState(Organization org);
/// <summary>
/// Validates that the organization user's organization ID matches the expected organization ID.
/// </summary>
Error? ValidateOrganizationMatch(OrganizationUser orgUser, Guid organizationId);
/// <summary>
/// Validates policy requirements for the user joining the organization.
/// </summary>
Task<Error?> ValidatePoliciesAsync(User user, Guid organizationId);
/// <summary>
/// Validates business rules for the user joining the organization (e.g., free org admin limits).
/// </summary>
Task<Error?> ValidateBusinessRulesAsync(User user, Organization org, OrganizationUser orgUser);
}
public class InitPendingOrganizationValidator : IInitPendingOrganizationValidator
{
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
private readonly IFeatureService _featureService;
private readonly IPolicyService _policyService;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IOrganizationUserRepository _organizationUserRepository;
public InitPendingOrganizationValidator(
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
IFeatureService featureService,
IPolicyService policyService,
IPolicyRequirementQuery policyRequirementQuery,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IOrganizationUserRepository organizationUserRepository)
{
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
_featureService = featureService;
_policyService = policyService;
_policyRequirementQuery = policyRequirementQuery;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_organizationUserRepository = organizationUserRepository;
}
public bool ValidateInviteToken(OrganizationUser orgUser, User user, string emailToken)
{
return OrgUserInviteTokenable.ValidateOrgUserInviteStringToken(
_orgUserInviteTokenDataFactory, emailToken, orgUser);
}
public Error? ValidateUserEmail(OrganizationUser orgUser, User user)
{
if (string.IsNullOrWhiteSpace(orgUser.Email) ||
!orgUser.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase))
{
return new EmailMismatchError();
}
return null;
}
public Error? ValidateOrganizationState(Organization org)
{
if (org.Enabled)
{
return new OrganizationAlreadyEnabledError();
}
if (org.Status != OrganizationStatusType.Pending)
{
return new OrganizationNotPendingError();
}
if (!string.IsNullOrEmpty(org.PublicKey) || !string.IsNullOrEmpty(org.PrivateKey))
{
return new OrganizationHasKeysError();
}
return null;
}
public Error? ValidateOrganizationMatch(OrganizationUser orgUser, Guid organizationId)
{
if (orgUser.OrganizationId != organizationId)
{
return new OrganizationMismatchError();
}
return null;
}
public async Task<Error?> ValidatePoliciesAsync(User user, Guid organizationId)
{
if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
{
var autoConfirmReq = await _policyRequirementQuery.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id);
if (autoConfirmReq.CannotCreateNewOrganization())
{
return new SingleOrgPolicyViolationError();
}
}
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg);
if (anySingleOrgPolicies)
{
return new SingleOrgPolicyViolationError();
}
var twoFactorReq = await _policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id);
if (twoFactorReq.IsTwoFactorRequiredForOrganization(organizationId) &&
!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
{
return new TwoFactorRequiredError();
}
return null;
}
public async Task<Error?> ValidateBusinessRulesAsync(User user, Organization org, OrganizationUser orgUser)
{
if (org.PlanType == PlanType.Free &&
(orgUser.Type == OrganizationUserType.Owner || orgUser.Type == OrganizationUserType.Admin))
{
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(user.Id);
if (adminCount > 0)
{
return new FreeOrgAdminLimitError();
}
}
return null;
}
}

View File

@@ -214,6 +214,7 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped<IInviteUsersOrganizationValidator, InviteUsersOrganizationValidator>();
services.AddScoped<IInviteUsersPasswordManagerValidator, InviteUsersPasswordManagerValidator>();
services.AddScoped<IInviteUsersEnvironmentValidator, InviteUsersEnvironmentValidator>();
services.AddScoped<IInitPendingOrganizationValidator, InitPendingOrganizationValidator>();
services.AddScoped<IInitPendingOrganizationCommand, InitPendingOrganizationCommand>();
services.AddScoped<IImportOrganizationUsersAndGroupsCommand, ImportOrganizationUsersAndGroupsCommand>();
}

View File

@@ -1,13 +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.Organizations;
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;
using Bit.Core.Entities;
using Bit.Core.Enums;
@@ -161,6 +155,11 @@ public class InitPendingOrganizationCommandTests
var protectedToken = _orgUserInviteTokenDataFactory.Protect(orgUserInviteTokenable);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(orgUser);
// Setup validator to accept the token for old InitPendingOrganizationAsync method
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateInviteToken(orgUser, Arg.Any<User>(), protectedToken)
.Returns(true);
return protectedToken;
}
@@ -248,13 +247,14 @@ public class InitPendingOrganizationCommandTests
orgUser.Email = user.Email;
var requestWithInvalidToken = request with { User = user, EmailToken = "invalid-token" };
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
sutProvider.Create();
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(requestWithInvalidToken.OrganizationUserId)
.Returns(orgUser);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateInviteToken(orgUser, user, "invalid-token")
.Returns(false);
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(requestWithInvalidToken);
@@ -274,13 +274,20 @@ public class InitPendingOrganizationCommandTests
orgUser.Email = "different@email.com";
user.Email = "user@email.com";
var token = CreateToken(orgUser, request.OrganizationUserId, sutProvider);
var requestWithUser = request with { User = user, EmailToken = token };
var requestWithUser = request with { User = user, EmailToken = "valid-token" };
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(requestWithUser.OrganizationUserId)
.Returns(orgUser);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateInviteToken(orgUser, user, Arg.Any<string>())
.Returns(true);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateUserEmail(orgUser, user)
.Returns(new EmailMismatchError());
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(requestWithUser);
@@ -299,13 +306,20 @@ public class InitPendingOrganizationCommandTests
// Arrange
orgUser.Email = user.Email;
var token = CreateToken(orgUser, request.OrganizationUserId, sutProvider);
var requestWithUser = request with { User = user, EmailToken = token };
var requestWithUser = request with { User = user, EmailToken = "valid-token" };
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(requestWithUser.OrganizationUserId)
.Returns(orgUser);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateInviteToken(orgUser, user, Arg.Any<string>())
.Returns(true);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateUserEmail(orgUser, user)
.Returns((Bit.Core.AdminConsole.Utilities.v2.Error)null);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(requestWithUser.OrganizationId)
.Returns((Organization)null);
@@ -330,17 +344,28 @@ public class InitPendingOrganizationCommandTests
orgUser.Email = user.Email;
orgUser.OrganizationId = Guid.NewGuid(); // Different from request
var token = CreateToken(orgUser, request.OrganizationUserId, sutProvider);
var requestWithUser = request with { User = user, EmailToken = token };
var requestWithUser = request with { User = user, EmailToken = "valid-token" };
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(requestWithUser.OrganizationUserId)
.Returns(orgUser);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateInviteToken(orgUser, user, Arg.Any<string>())
.Returns(true);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateUserEmail(orgUser, user)
.Returns((Bit.Core.AdminConsole.Utilities.v2.Error)null);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(requestWithUser.OrganizationId)
.Returns(org);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateOrganizationMatch(orgUser, requestWithUser.OrganizationId)
.Returns(new OrganizationMismatchError());
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(requestWithUser);
@@ -370,6 +395,14 @@ public class InitPendingOrganizationCommandTests
.GetByIdAsync(updatedRequest.OrganizationId)
.Returns(org);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateOrganizationMatch(orgUser, updatedRequest.OrganizationId)
.Returns((Bit.Core.AdminConsole.Utilities.v2.Error)null);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateOrganizationState(org)
.Returns(new OrganizationAlreadyEnabledError());
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(updatedRequest);
@@ -399,6 +432,14 @@ public class InitPendingOrganizationCommandTests
.GetByIdAsync(updatedRequest.OrganizationId)
.Returns(org);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateOrganizationMatch(orgUser, updatedRequest.OrganizationId)
.Returns((Bit.Core.AdminConsole.Utilities.v2.Error)null);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateOrganizationState(org)
.Returns(new OrganizationNotPendingError());
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(updatedRequest);
@@ -428,6 +469,14 @@ public class InitPendingOrganizationCommandTests
.GetByIdAsync(updatedRequest.OrganizationId)
.Returns(org);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateOrganizationMatch(orgUser, updatedRequest.OrganizationId)
.Returns((Bit.Core.AdminConsole.Utilities.v2.Error)null);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateOrganizationState(org)
.Returns(new OrganizationHasKeysError());
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(updatedRequest);
@@ -457,6 +506,14 @@ public class InitPendingOrganizationCommandTests
.GetByIdAsync(updatedRequest.OrganizationId)
.Returns(org);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateOrganizationMatch(orgUser, updatedRequest.OrganizationId)
.Returns((Bit.Core.AdminConsole.Utilities.v2.Error)null);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateOrganizationState(org)
.Returns(new OrganizationHasKeysError());
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(updatedRequest);
@@ -476,9 +533,9 @@ public class InitPendingOrganizationCommandTests
// Arrange
var updatedRequest = SetupValidOrgAndOrgUser(user, org, orgUser, request, sutProvider);
sutProvider.GetDependency<IPolicyService>()
.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg)
.Returns(true);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidatePoliciesAsync(user, updatedRequest.OrganizationId)
.Returns(new SingleOrgPolicyViolationError());
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(updatedRequest);
@@ -499,33 +556,9 @@ public class InitPendingOrganizationCommandTests
// Arrange
var updatedRequest = SetupValidOrgAndOrgUser(user, org, orgUser, request, sutProvider);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IPolicyService>()
.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg)
.Returns(false);
// Create a PolicyDetails that requires 2FA for this organization
var policyDetails = new PolicyDetails
{
OrganizationId = updatedRequest.OrganizationId,
OrganizationUserId = updatedRequest.OrganizationUserId,
PolicyType = PolicyType.TwoFactorAuthentication,
OrganizationUserType = OrganizationUserType.Owner,
OrganizationUserStatus = OrganizationUserStatusType.Invited
};
var twoFactorReq = new RequireTwoFactorPolicyRequirement(new[] { policyDetails });
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(twoFactorReq);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(user)
.Returns(false);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidatePoliciesAsync(user, updatedRequest.OrganizationId)
.Returns(new TwoFactorRequiredError());
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(updatedRequest);
@@ -549,24 +582,13 @@ public class InitPendingOrganizationCommandTests
org.PlanType = PlanType.Free;
orgUser.Type = OrganizationUserType.Owner;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidatePoliciesAsync(user, updatedRequest.OrganizationId)
.Returns((Bit.Core.AdminConsole.Utilities.v2.Error)null);
sutProvider.GetDependency<IPolicyService>()
.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg)
.Returns(false);
// Create a RequireTwoFactorPolicyRequirement with no policies (2FA not required)
var twoFactorReq = new RequireTwoFactorPolicyRequirement(Enumerable.Empty<PolicyDetails>());
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(twoFactorReq);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetCountByFreeOrganizationAdminUserAsync(user.Id)
.Returns(1);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateBusinessRulesAsync(user, org, orgUser)
.Returns(new FreeOrgAdminLimitError());
// Act
var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(updatedRequest);
@@ -585,25 +607,14 @@ public class InitPendingOrganizationCommandTests
{
var updatedRequest = SetupValidOrgAndOrgUser(user, org, orgUser, request, sutProvider);
// Setup policy checks to pass
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Any<string>())
.Returns(false);
// Setup validator to pass all policy and business rule checks
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidatePoliciesAsync(user, updatedRequest.OrganizationId)
.Returns((Bit.Core.AdminConsole.Utilities.v2.Error)null);
sutProvider.GetDependency<IPolicyService>()
.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg)
.Returns(false);
// Create a RequireTwoFactorPolicyRequirement with no policies (2FA not required)
var twoFactorReq = new RequireTwoFactorPolicyRequirement(Enumerable.Empty<PolicyDetails>());
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(twoFactorReq);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetCountByFreeOrganizationAdminUserAsync(user.Id)
.Returns(0);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateBusinessRulesAsync(user, org, orgUser)
.Returns((Bit.Core.AdminConsole.Utilities.v2.Error)null);
// Setup repositories to return update delegates
sutProvider.GetDependency<IOrganizationRepository>()
@@ -649,6 +660,15 @@ public class InitPendingOrganizationCommandTests
.GetByIdAsync(updatedRequest.OrganizationId)
.Returns(org);
// Setup validator for organization match and state
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateOrganizationMatch(orgUser, updatedRequest.OrganizationId)
.Returns((Bit.Core.AdminConsole.Utilities.v2.Error)null);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateOrganizationState(org)
.Returns((Bit.Core.AdminConsole.Utilities.v2.Error)null);
return updatedRequest;
}
@@ -660,13 +680,20 @@ public class InitPendingOrganizationCommandTests
{
orgUser.Email = user.Email;
var token = CreateToken(orgUser, request.OrganizationUserId, sutProvider);
var updatedRequest = request with { User = user, EmailToken = token };
var updatedRequest = request with { User = user, EmailToken = "valid-token" };
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(updatedRequest.OrganizationUserId)
.Returns(orgUser);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateInviteToken(orgUser, user, Arg.Any<string>())
.Returns(true);
sutProvider.GetDependency<IInitPendingOrganizationValidator>()
.ValidateUserEmail(orgUser, user)
.Returns((Bit.Core.AdminConsole.Utilities.v2.Error)null);
return updatedRequest;
}

View File

@@ -0,0 +1,435 @@
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;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tokens;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Fakes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;
[SutProviderCustomize]
public class InitPendingOrganizationValidatorTests
{
private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory = Substitute.For<IOrgUserInviteTokenableFactory>();
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>();
[Theory, BitAutoData]
public void ValidateInviteToken_ValidToken_ReturnsTrue(
User user,
OrganizationUser orgUser,
SutProvider<InitPendingOrganizationValidator> sutProvider)
{
// Arrange
orgUser.Email = user.Email;
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
sutProvider.Create();
_orgUserInviteTokenableFactory.CreateToken(orgUser).Returns(new OrgUserInviteTokenable(orgUser)
{
ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))
});
var orgUserInviteTokenable = _orgUserInviteTokenableFactory.CreateToken(orgUser);
var protectedToken = _orgUserInviteTokenDataFactory.Protect(orgUserInviteTokenable);
// Act
var result = sutProvider.Sut.ValidateInviteToken(orgUser, user, protectedToken);
// Assert
Assert.True(result);
}
[Theory, BitAutoData]
public void ValidateInviteToken_InvalidToken_ReturnsFalse(
User user,
OrganizationUser orgUser,
SutProvider<InitPendingOrganizationValidator> sutProvider)
{
// Arrange
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
sutProvider.Create();
// Act
var result = sutProvider.Sut.ValidateInviteToken(orgUser, user, "invalid-token");
// Assert
Assert.False(result);
}
[Theory, BitAutoData]
public void ValidateUserEmail_MatchingEmail_ReturnsNull(
User user,
OrganizationUser orgUser,
SutProvider<InitPendingOrganizationValidator> sutProvider)
{
// Arrange
orgUser.Email = user.Email;
// Act
var result = sutProvider.Sut.ValidateUserEmail(orgUser, user);
// Assert
Assert.Null(result);
}
[Theory, BitAutoData]
public void ValidateUserEmail_MismatchedEmail_ReturnsError(
User user,
OrganizationUser orgUser,
SutProvider<InitPendingOrganizationValidator> sutProvider)
{
// Arrange
orgUser.Email = "different@example.com";
user.Email = "user@example.com";
// Act
var result = sutProvider.Sut.ValidateUserEmail(orgUser, user);
// Assert
Assert.NotNull(result);
Assert.IsType<EmailMismatchError>(result);
}
[Theory, BitAutoData]
public void ValidateUserEmail_NullOrgUserEmail_ReturnsError(
User user,
OrganizationUser orgUser,
SutProvider<InitPendingOrganizationValidator> sutProvider)
{
// Arrange
orgUser.Email = null;
// Act
var result = sutProvider.Sut.ValidateUserEmail(orgUser, user);
// Assert
Assert.NotNull(result);
Assert.IsType<EmailMismatchError>(result);
}
[Theory, BitAutoData]
public void ValidateOrganizationState_ValidState_ReturnsNull(
Organization org,
SutProvider<InitPendingOrganizationValidator> sutProvider)
{
// Arrange
org.Enabled = false;
org.Status = OrganizationStatusType.Pending;
org.PublicKey = null;
org.PrivateKey = null;
// Act
var result = sutProvider.Sut.ValidateOrganizationState(org);
// Assert
Assert.Null(result);
}
[Theory, BitAutoData]
public void ValidateOrganizationState_OrgEnabled_ReturnsError(
Organization org,
SutProvider<InitPendingOrganizationValidator> sutProvider)
{
// Arrange
org.Enabled = true;
org.Status = OrganizationStatusType.Pending;
org.PublicKey = null;
org.PrivateKey = null;
// Act
var result = sutProvider.Sut.ValidateOrganizationState(org);
// Assert
Assert.NotNull(result);
Assert.IsType<OrganizationAlreadyEnabledError>(result);
}
[Theory, BitAutoData]
public void ValidateOrganizationState_OrgNotPending_ReturnsError(
Organization org,
SutProvider<InitPendingOrganizationValidator> sutProvider)
{
// Arrange
org.Enabled = false;
org.Status = OrganizationStatusType.Created;
org.PublicKey = null;
org.PrivateKey = null;
// Act
var result = sutProvider.Sut.ValidateOrganizationState(org);
// Assert
Assert.NotNull(result);
Assert.IsType<OrganizationNotPendingError>(result);
}
[Theory, BitAutoData]
public void ValidateOrganizationState_OrgHasKeys_ReturnsError(
Organization org,
SutProvider<InitPendingOrganizationValidator> sutProvider)
{
// Arrange
org.Enabled = false;
org.Status = OrganizationStatusType.Pending;
org.PublicKey = "existing-public-key";
org.PrivateKey = "existing-private-key";
// Act
var result = sutProvider.Sut.ValidateOrganizationState(org);
// Assert
Assert.NotNull(result);
Assert.IsType<OrganizationHasKeysError>(result);
}
[Theory, BitAutoData]
public void ValidateOrganizationMatch_Matching_ReturnsNull(
OrganizationUser orgUser,
Guid organizationId,
SutProvider<InitPendingOrganizationValidator> sutProvider)
{
// Arrange
orgUser.OrganizationId = organizationId;
// Act
var result = sutProvider.Sut.ValidateOrganizationMatch(orgUser, organizationId);
// Assert
Assert.Null(result);
}
[Theory, BitAutoData]
public void ValidateOrganizationMatch_NotMatching_ReturnsError(
OrganizationUser orgUser,
Guid organizationId,
SutProvider<InitPendingOrganizationValidator> sutProvider)
{
// Arrange
orgUser.OrganizationId = Guid.NewGuid();
// Act
var result = sutProvider.Sut.ValidateOrganizationMatch(orgUser, organizationId);
// Assert
Assert.NotNull(result);
Assert.IsType<OrganizationMismatchError>(result);
}
[Theory, BitAutoData]
public async Task ValidatePoliciesAsync_AllPoliciesPass_ReturnsNull(
User user,
Guid organizationId,
SutProvider<InitPendingOrganizationValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IPolicyService>()
.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg)
.Returns(false);
var twoFactorReq = new RequireTwoFactorPolicyRequirement(Enumerable.Empty<PolicyDetails>());
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(twoFactorReq);
// Act
var result = await sutProvider.Sut.ValidatePoliciesAsync(user, organizationId);
// Assert
Assert.Null(result);
}
[Theory, BitAutoData]
public async Task ValidatePoliciesAsync_SingleOrgPolicyViolation_ReturnsError(
User user,
Guid organizationId,
SutProvider<InitPendingOrganizationValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IPolicyService>()
.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg)
.Returns(true);
// Act
var result = await sutProvider.Sut.ValidatePoliciesAsync(user, organizationId);
// Assert
Assert.NotNull(result);
Assert.IsType<SingleOrgPolicyViolationError>(result);
}
[Theory, BitAutoData]
public async Task ValidatePoliciesAsync_TwoFactorRequired_UserDoesNotHave2FA_ReturnsError(
User user,
Guid organizationId,
SutProvider<InitPendingOrganizationValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IPolicyService>()
.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg)
.Returns(false);
var policyDetails = new PolicyDetails
{
OrganizationId = organizationId,
PolicyType = PolicyType.TwoFactorAuthentication,
OrganizationUserType = OrganizationUserType.Owner,
OrganizationUserStatus = OrganizationUserStatusType.Invited
};
var twoFactorReq = new RequireTwoFactorPolicyRequirement(new[] { policyDetails });
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(twoFactorReq);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(user)
.Returns(false);
// Act
var result = await sutProvider.Sut.ValidatePoliciesAsync(user, organizationId);
// Assert
Assert.NotNull(result);
Assert.IsType<TwoFactorRequiredError>(result);
}
[Theory, BitAutoData]
public async Task ValidateBusinessRulesAsync_PaidOrg_ReturnsNull(
User user,
Organization org,
OrganizationUser orgUser,
SutProvider<InitPendingOrganizationValidator> sutProvider)
{
// Arrange
org.PlanType = PlanType.EnterpriseAnnually;
orgUser.Type = OrganizationUserType.Owner;
// Act
var result = await sutProvider.Sut.ValidateBusinessRulesAsync(user, org, orgUser);
// Assert
Assert.Null(result);
}
[Theory, BitAutoData]
public async Task ValidateBusinessRulesAsync_FreeOrgNonAdmin_ReturnsNull(
User user,
Organization org,
OrganizationUser orgUser,
SutProvider<InitPendingOrganizationValidator> sutProvider)
{
// Arrange
org.PlanType = PlanType.Free;
orgUser.Type = OrganizationUserType.User;
// Act
var result = await sutProvider.Sut.ValidateBusinessRulesAsync(user, org, orgUser);
// Assert
Assert.Null(result);
}
[Theory, BitAutoData]
public async Task ValidateBusinessRulesAsync_FreeOrgAdminNoExisting_ReturnsNull(
User user,
Organization org,
OrganizationUser orgUser,
SutProvider<InitPendingOrganizationValidator> sutProvider)
{
// Arrange
org.PlanType = PlanType.Free;
orgUser.Type = OrganizationUserType.Owner;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetCountByFreeOrganizationAdminUserAsync(user.Id)
.Returns(0);
// Act
var result = await sutProvider.Sut.ValidateBusinessRulesAsync(user, org, orgUser);
// Assert
Assert.Null(result);
}
[Theory, BitAutoData]
public async Task ValidateBusinessRulesAsync_FreeOrgAdminLimitExceeded_ReturnsError(
User user,
Organization org,
OrganizationUser orgUser,
SutProvider<InitPendingOrganizationValidator> sutProvider)
{
// Arrange
org.PlanType = PlanType.Free;
orgUser.Type = OrganizationUserType.Owner;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetCountByFreeOrganizationAdminUserAsync(user.Id)
.Returns(1);
// Act
var result = await sutProvider.Sut.ValidateBusinessRulesAsync(user, org, orgUser);
// Assert
Assert.NotNull(result);
Assert.IsType<FreeOrgAdminLimitError>(result);
}
[Theory, BitAutoData]
public async Task ValidatePoliciesAsync_AutoConfirmPolicyViolation_ReturnsError(
User user,
Guid organizationId,
SutProvider<InitPendingOrganizationValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
var policyDetails = new PolicyDetails
{
OrganizationId = organizationId,
PolicyType = PolicyType.AutomaticUserConfirmation,
OrganizationUserType = OrganizationUserType.Owner,
OrganizationUserStatus = OrganizationUserStatusType.Invited
};
var autoConfirmReq = new AutomaticUserConfirmationPolicyRequirement(new[] { policyDetails });
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
.Returns(autoConfirmReq);
// Act
var result = await sutProvider.Sut.ValidatePoliciesAsync(user, organizationId);
// Assert
Assert.NotNull(result);
Assert.IsType<SingleOrgPolicyViolationError>(result);
}
}