1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-22 12:04:27 -05:00

Refactor UserService to integrate IPolicyRequirementQuery for two-factor authentication policy checks

This commit is contained in:
Rui Tome 2025-05-21 15:49:32 +01:00
parent 73bf74dac4
commit 102c8adf61
No known key found for this signature in database
GPG Key ID: 526239D96A8EC066
2 changed files with 165 additions and 15 deletions

View File

@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums;
@ -81,6 +82,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IDistributedCache _distributedCache;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
public UserService(
IUserRepository userRepository,
@ -119,7 +121,8 @@ public class UserService : UserManager<User>, IUserService, IDisposable
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IDistributedCache distributedCache)
IDistributedCache distributedCache,
IPolicyRequirementQuery policyRequirementQuery)
: base(
store,
optionsAccessor,
@ -164,6 +167,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_distributedCache = distributedCache;
_policyRequirementQuery = policyRequirementQuery;
}
public Guid? GetProperUserId(ClaimsPrincipal principal)
@ -1394,20 +1398,40 @@ public class UserService : UserManager<User>, IUserService, IDisposable
private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user)
{
var twoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication);
var removeOrgUserTasks = twoFactorPolicies.Select(async p =>
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
{
var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId);
await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
new RevokeOrganizationUsersRequest(
p.OrganizationId,
[new OrganizationUserUserDetails { Id = p.OrganizationUserId, OrganizationId = p.OrganizationId }],
new SystemUser(EventSystemUser.TwoFactorDisabled)));
await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email);
}).ToArray();
var requirement = await _policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id);
await Task.WhenAll(removeOrgUserTasks);
var removeOrgUserTasks = requirement.TwoFactorPoliciesForActiveMemberships.Select(async p =>
{
var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId);
await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
new RevokeOrganizationUsersRequest(
p.OrganizationId,
[new OrganizationUserUserDetails { Id = p.OrganizationUserId, OrganizationId = p.OrganizationId }],
new SystemUser(EventSystemUser.TwoFactorDisabled)));
await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email);
}).ToArray();
await Task.WhenAll(removeOrgUserTasks);
}
else
{
var twoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication);
var removeOrgUserTasks = twoFactorPolicies.Select(async p =>
{
var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId);
await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
new RevokeOrganizationUsersRequest(
p.OrganizationId,
[new OrganizationUserUserDetails { Id = p.OrganizationUserId, OrganizationId = p.OrganizationId }],
new SystemUser(EventSystemUser.TwoFactorDisabled)));
await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email);
}).ToArray();
await Task.WhenAll(removeOrgUserTasks);
}
}
public override async Task<IdentityResult> ConfirmEmailAsync(User user, string token)

View File

@ -2,8 +2,10 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums;
@ -326,7 +328,8 @@ public class UserServiceTests
sutProvider.GetDependency<IRemoveOrganizationUserCommand>(),
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>(),
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>(),
sutProvider.GetDependency<IDistributedCache>()
sutProvider.GetDependency<IDistributedCache>(),
sutProvider.GetDependency<IPolicyRequirementQuery>()
);
var actualIsVerified = await sut.VerifySecretAsync(user, secret);
@ -462,6 +465,81 @@ public class UserServiceTests
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization2.DisplayName(), user.Email);
}
[Theory, BitAutoData]
public async Task DisableTwoFactorProviderAsync_WithPolicyRequirementsEnabled_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RevokesUserAndSendsEmail(
SutProvider<UserService> sutProvider, User user,
Organization organization1, Guid organizationUserId1,
Organization organization2, Guid organizationUserId2)
{
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Email] = new() { Enabled = true }
});
organization1.Enabled = organization2.Enabled = true;
organization1.UseSso = organization2.UseSso = true;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
.Returns(true);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(new RequireTwoFactorPolicyRequirement(
[
new PolicyDetails
{
OrganizationId = organization1.Id,
OrganizationUserId = organizationUserId1,
OrganizationUserStatus = OrganizationUserStatusType.Accepted,
PolicyType = PolicyType.TwoFactorAuthentication
},
new PolicyDetails
{
OrganizationId = organization2.Id,
OrganizationUserId = organizationUserId2,
OrganizationUserStatus = OrganizationUserStatusType.Confirmed,
PolicyType = PolicyType.TwoFactorAuthentication
}
]));
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization1.Id)
.Returns(organization1);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization2.Id)
.Returns(organization2);
var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>(), JsonHelpers.LegacyEnumKeyResolver);
await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
await sutProvider.GetDependency<IUserRepository>()
.Received(1)
.ReplaceAsync(Arg.Is<User>(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders));
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogUserEventAsync(user.Id, EventType.User_Disabled2fa);
// Revoke the user from the first organization
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.Received(1)
.RevokeNonCompliantOrganizationUsersAsync(
Arg.Is<RevokeOrganizationUsersRequest>(r => r.OrganizationId == organization1.Id &&
r.OrganizationUsers.First().Id == organizationUserId1 &&
r.OrganizationUsers.First().OrganizationId == organization1.Id));
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization1.DisplayName(), user.Email);
// Remove the user from the second organization
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.Received(1)
.RevokeNonCompliantOrganizationUsersAsync(
Arg.Is<RevokeOrganizationUsersRequest>(r => r.OrganizationId == organization2.Id &&
r.OrganizationUsers.First().Id == organizationUserId2 &&
r.OrganizationUsers.First().OrganizationId == organization2.Id));
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization2.DisplayName(), user.Email);
}
[Theory, BitAutoData]
public async Task DisableTwoFactorProviderAsync_UserHasOneProviderEnabled_DoesNotRevokeUserFromOrganization(
SutProvider<UserService> sutProvider, User user, Organization organization)
@ -509,6 +587,53 @@ public class UserServiceTests
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(default, default);
}
[Theory, BitAutoData]
public async Task DisableTwoFactorProviderAsync_WithPolicyRequirementsEnabled_UserHasOneProviderEnabled_DoesNotRevokeUserFromOrganization(
SutProvider<UserService> sutProvider, User user, Organization organization)
{
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Email] = new() { Enabled = true },
[TwoFactorProviderType.Remember] = new() { Enabled = true }
});
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
.Returns(true);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(new RequireTwoFactorPolicyRequirement(
[
new PolicyDetails
{
OrganizationId = organization.Id,
OrganizationUserStatus = OrganizationUserStatusType.Accepted,
PolicyType = PolicyType.TwoFactorAuthentication
}
]));
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(user)
.Returns(true);
var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Remember] = new() { Enabled = true }
}, JsonHelpers.LegacyEnumKeyResolver);
await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
await sutProvider.GetDependency<IUserRepository>()
.Received(1)
.ReplaceAsync(Arg.Is<User>(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders));
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.DidNotReceiveWithAnyArgs()
.RevokeNonCompliantOrganizationUsersAsync(default);
await sutProvider.GetDependency<IMailService>()
.DidNotReceiveWithAnyArgs()
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(default, default);
}
[Theory, BitAutoData]
public async Task ResendNewDeviceVerificationEmail_UserNull_SendTwoFactorEmailAsyncNotCalled(
SutProvider<UserService> sutProvider, string email, string secret)
@ -800,7 +925,8 @@ public class UserServiceTests
sutProvider.GetDependency<IRemoveOrganizationUserCommand>(),
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>(),
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>(),
sutProvider.GetDependency<IDistributedCache>()
sutProvider.GetDependency<IDistributedCache>(),
sutProvider.GetDependency<IPolicyRequirementQuery>()
);
}
}