diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs index 756bd2ae46..8c9edb4dc4 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Billing.Enums; @@ -25,6 +26,8 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand private readonly IMailService _mailService; private readonly IUserRepository _userRepository; private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; + private readonly IFeatureService _featureService; + private readonly IPolicyRequirementQuery _policyRequirementQuery; public AcceptOrgUserCommand( IDataProtectionProvider dataProtectionProvider, @@ -34,9 +37,10 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand IPolicyService policyService, IMailService mailService, IUserRepository userRepository, - IDataProtectorTokenFactory orgUserInviteTokenDataFactory) + IDataProtectorTokenFactory orgUserInviteTokenDataFactory, + IFeatureService featureService, + IPolicyRequirementQuery policyRequirementQuery) { - // TODO: remove data protector when old token validation removed _dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose); _globalSettings = globalSettings; @@ -46,6 +50,8 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand _mailService = mailService; _userRepository = userRepository; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; + _featureService = featureService; + _policyRequirementQuery = policyRequirementQuery; } public async Task AcceptOrgUserByEmailTokenAsync(Guid organizationUserId, User user, string emailToken, @@ -194,12 +200,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand // Enforce Two Factor Authentication Policy of organization user is trying to join if (!await userService.TwoFactorIsEnabledAsync(user)) { - var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, - PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited); - if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) - { - throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account."); - } + await ValidateTwoFactorAuthenticationPolicy(user, orgUser.OrganizationId); } orgUser.Status = OrganizationUserStatusType.Accepted; @@ -220,4 +221,24 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand return orgUser; } + private async Task ValidateTwoFactorAuthenticationPolicy(User user, Guid organizationId) + { + if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) + { + var requireTwoFactorPolicyRequirement = await _policyRequirementQuery.GetAsync(user.Id); + if (requireTwoFactorPolicyRequirement.RequireTwoFactor) + { + throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account."); + } + } + else + { + var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, + PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited); + if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == organizationId)) + { + throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account."); + } + } + } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs index 2dda23481a..ae7e6ff094 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Billing.Enums; @@ -183,6 +184,96 @@ public class AcceptOrgUserCommandTests exception.Message); } + [Theory] + [BitAutoData] + public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserWithout2FAJoining2FARequiredOrg_ThrowsBadRequest( + SutProvider sutProvider, + User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) + { + // Arrange + SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + // User doesn't have 2FA enabled + _userService.TwoFactorIsEnabledAsync(user).Returns(false); + + // Organization they are trying to join requires 2FA + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new RequireTwoFactorPolicyRequirement { RequireTwoFactor = true }); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService)); + + Assert.Equal("You cannot join this organization until you enable two-step login on your user account.", + exception.Message); + } + + [Theory] + [BitAutoData] + public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserWith2FAJoining2FARequiredOrg_Succeeds( + SutProvider sutProvider, + User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) + { + SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + // User has 2FA enabled + _userService.TwoFactorIsEnabledAsync(user).Returns(true); + + // Organization they are trying to join requires 2FA + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new RequireTwoFactorPolicyRequirement { RequireTwoFactor = true }); + + await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetAsync(default); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(ou => ou.Status == OrganizationUserStatusType.Accepted)); + } + + [Theory] + [BitAutoData(true)] + [BitAutoData(false)] + public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserJoiningOrgWithout2FARequirement_Succeeds( + bool userTwoFactorEnabled, SutProvider sutProvider, + User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) + { + // Arrange + SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + // User doesn't have 2FA enabled + _userService.TwoFactorIsEnabledAsync(user).Returns(userTwoFactorEnabled); + + // Organization they are trying to join doesn't require 2FA + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new RequireTwoFactorPolicyRequirement { RequireTwoFactor = false }); + + // Act + await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService); + + // Assert + await sutProvider.GetDependency().Received(1) + .ReplaceAsync(Arg.Is(ou => ou.Status == OrganizationUserStatusType.Accepted)); + } + [Theory] [BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Owner)]