1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-23 20:41:04 -05:00

Enhance AcceptOrgUserCommand to use IPolicyRequirementQuery for two-factor authentication validation

This commit is contained in:
Rui Tome 2025-05-08 15:31:52 +01:00
parent d696bfdc30
commit e8a8d5c8c7
No known key found for this signature in database
GPG Key ID: 526239D96A8EC066
2 changed files with 120 additions and 8 deletions

View File

@ -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<OrgUserInviteTokenable> _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<OrgUserInviteTokenable> orgUserInviteTokenDataFactory)
IDataProtectorTokenFactory<OrgUserInviteTokenable> 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<OrganizationUser> 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<RequireTwoFactorPolicyRequirement>(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.");
}
}
}
}

View File

@ -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<AcceptOrgUserCommand> sutProvider,
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
{
// Arrange
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
sutProvider.GetDependency<IFeatureService>()
.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<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(new RequireTwoFactorPolicyRequirement { RequireTwoFactor = true });
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
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<AcceptOrgUserCommand> sutProvider,
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
{
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
.Returns(true);
// User has 2FA enabled
_userService.TwoFactorIsEnabledAsync(user).Returns(true);
// Organization they are trying to join requires 2FA
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(new RequireTwoFactorPolicyRequirement { RequireTwoFactor = true });
await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);
await sutProvider.GetDependency<IPolicyRequirementQuery>()
.DidNotReceiveWithAnyArgs()
.GetAsync<RequireTwoFactorPolicyRequirement>(default);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.ReplaceAsync(Arg.Is<OrganizationUser>(ou => ou.Status == OrganizationUserStatusType.Accepted));
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserJoiningOrgWithout2FARequirement_Succeeds(
bool userTwoFactorEnabled, SutProvider<AcceptOrgUserCommand> sutProvider,
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
{
// Arrange
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
sutProvider.GetDependency<IFeatureService>()
.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<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(new RequireTwoFactorPolicyRequirement { RequireTwoFactor = false });
// Act
await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);
// Assert
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
.ReplaceAsync(Arg.Is<OrganizationUser>(ou => ou.Status == OrganizationUserStatusType.Accepted));
}
[Theory]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)]