1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-05 18:12:48 -05:00

[AC-1698] Check if a user has 2FA enabled more efficiently (#4524)

* feat: Add stored procedure for reading organization user details with premium access by organization ID

The code changes include:
- Addition of a new stored procedure [dbo].[OrganizationUserUserDetailsWithPremiumAccess_ReadByOrganizationId] to read organization user details with premium access by organization ID
- Modification of the IUserService interface to include an optional parameter for checking two-factor authentication with premium access
- Modification of the UserService class to handle the new optional parameter in the TwoFactorIsEnabledAsync method
- Addition of a new method GetManyDetailsWithPremiumAccessByOrganizationAsync in the IOrganizationUserRepository interface to retrieve organization user details with premium access by organization ID
- Addition of a new view [dbo].[OrganizationUserUserDetailsWithPremiumAccessView] to retrieve organization user details with premium access

* Add IUserRepository.SearchDetailsAsync that includes the field HasPremiumAccess

* Check the feature flag on Admin.UsersController to see if the optimization runs

* Modify PolicyService to run query optimization if the feature flag is enabled

* Refactor the parameter check on UserService.TwoFactorIsEnabledAsync

* Run query optimization on public MembersController if feature flag is enabled

* Restore refactor

* Reverted change used for development

* Add unit tests for OrganizationService.RestoreUser

* Separate new CheckPoliciesBeforeRestoreAsync optimization into new method

* Add more unit tests

* Apply refactor to bulk restore

* Add GetManyDetailsAsync method to IUserRepository. Add ConfirmUsersAsync_vNext method to IOrganizationService

* Add unit tests for ConfirmUser_vNext

* Refactor the optimization to use the new TwoFactorIsEnabledAsync method instead of changing the existing one

* Removed unused sql scripts and added migration script

* Remove unnecessary view

* chore: Remove unused SearchDetailsAsync method from IUserRepository and UserRepository

* refactor: Use UserDetails constructor in UserRepository

* Add summary to IUserRepository.GetManyDetailsAsync

* Add summary descriptions to IUserService.TwoFactorIsEnabledAsync

* Remove obsolete annotation from IUserRepository.UpdateUserKeyAndEncryptedDataAsync

* refactor: Rename UserDetails to UserWithCalculatedPremium across the codebase

* Extract IUserService.TwoFactorIsEnabledAsync into a new TwoFactorIsEnabledQuery class

* Add unit tests for TwoFactorIsEnabledQuery

* Update TwoFactorIsEnabledQueryTests to include additional provider types

* Refactor TwoFactorIsEnabledQuery

* Refactor TwoFactorIsEnabledQuery and update tests

* refactor: Update TwoFactorIsEnabledQueryTests to include test for null TwoFactorProviders

* refactor: Improve TwoFactorIsEnabledQuery and update tests

* refactor: Improve TwoFactorIsEnabledQuery and update tests

* Remove empty <returns> from summary

* Update User_ReadByIdsWithCalculatedPremium stored procedure to accept JSON array of IDs
This commit is contained in:
Rui Tomé
2024-08-08 15:43:45 +01:00
committed by GitHub
parent 19dc7c339b
commit 8d69bb0aaa
21 changed files with 1481 additions and 25 deletions

View File

@ -0,0 +1,23 @@
using Bit.Core.Auth.Models;
namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
public interface ITwoFactorIsEnabledQuery
{
/// <summary>
/// Returns a list of user IDs and whether two factor is enabled for each user.
/// </summary>
/// <param name="userIds">The list of user IDs to check.</param>
Task<IEnumerable<(Guid userId, bool twoFactorIsEnabled)>> TwoFactorIsEnabledAsync(IEnumerable<Guid> userIds);
/// <summary>
/// Returns a list of users and whether two factor is enabled for each user.
/// </summary>
/// <param name="users">The list of users to check.</param>
/// <typeparam name="T">The type of user in the list. Must implement <see cref="ITwoFactorProvidersUser"/>.</typeparam>
Task<IEnumerable<(T user, bool twoFactorIsEnabled)>> TwoFactorIsEnabledAsync<T>(IEnumerable<T> users) where T : ITwoFactorProvidersUser;
/// <summary>
/// Returns whether two factor is enabled for the user.
/// </summary>
/// <param name="user">The user to check.</param>
Task<bool> TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user);
}

View File

@ -0,0 +1,123 @@
using Bit.Core.Auth.Models;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Repositories;
namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth;
public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery
{
private readonly IUserRepository _userRepository;
public TwoFactorIsEnabledQuery(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public async Task<IEnumerable<(Guid userId, bool twoFactorIsEnabled)>> TwoFactorIsEnabledAsync(IEnumerable<Guid> userIds)
{
var result = new List<(Guid userId, bool hasTwoFactor)>();
if (userIds == null || !userIds.Any())
{
return result;
}
var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync(userIds.ToList());
foreach (var userDetail in userDetails)
{
var hasTwoFactor = false;
var providers = userDetail.GetTwoFactorProviders();
if (providers != null)
{
// Get all enabled providers
var enabledProviderKeys = from provider in providers
where provider.Value?.Enabled ?? false
select provider.Key;
// Find the first provider that is enabled and passes the premium check
hasTwoFactor = enabledProviderKeys
.Select(type => userDetail.HasPremiumAccess || !TwoFactorProvider.RequiresPremium(type))
.FirstOrDefault();
}
result.Add((userDetail.Id, hasTwoFactor));
}
return result;
}
public async Task<IEnumerable<(T user, bool twoFactorIsEnabled)>> TwoFactorIsEnabledAsync<T>(IEnumerable<T> users) where T : ITwoFactorProvidersUser
{
var userIds = users
.Select(u => u.GetUserId())
.Where(u => u.HasValue)
.Select(u => u.Value)
.ToList();
var twoFactorResults = await TwoFactorIsEnabledAsync(userIds);
var result = new List<(T user, bool twoFactorIsEnabled)>();
foreach (var user in users)
{
var userId = user.GetUserId();
if (userId.HasValue)
{
var hasTwoFactor = twoFactorResults.FirstOrDefault(res => res.userId == userId.Value).twoFactorIsEnabled;
result.Add((user, hasTwoFactor));
}
else
{
result.Add((user, false));
}
}
return result;
}
public async Task<bool> TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user)
{
var userId = user.GetUserId();
if (!userId.HasValue)
{
return false;
}
var providers = user.GetTwoFactorProviders();
if (providers == null || !providers.Any())
{
return false;
}
// Get all enabled providers
var enabledProviderKeys = providers
.Where(provider => provider.Value?.Enabled ?? false)
.Select(provider => provider.Key);
if (!enabledProviderKeys.Any())
{
return false;
}
// Determine if any enabled provider passes the premium check
var hasTwoFactor = enabledProviderKeys
.Select(type => user.GetPremium() || !TwoFactorProvider.RequiresPremium(type))
.FirstOrDefault();
// If no enabled provider passes the check, check the repository for organization premium access
if (!hasTwoFactor)
{
var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync(new List<Guid> { userId.Value });
var userDetail = userDetails.FirstOrDefault();
if (userDetail != null)
{
hasTwoFactor = enabledProviderKeys
.Select(type => userDetail.HasPremiumAccess || !TwoFactorProvider.RequiresPremium(type))
.FirstOrDefault();
}
}
return hasTwoFactor;
}
}

View File

@ -3,6 +3,8 @@
using Bit.Core.Auth.UserFeatures.Registration;
using Bit.Core.Auth.UserFeatures.Registration.Implementations;
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Auth.UserFeatures.UserKey;
using Bit.Core.Auth.UserFeatures.UserKey.Implementations;
using Bit.Core.Auth.UserFeatures.UserMasterPassword;
@ -24,6 +26,7 @@ public static class UserServiceCollectionExtensions
services.AddUserRegistrationCommands();
services.AddWebAuthnLoginCommands();
services.AddTdeOffboardingPasswordCommands();
services.AddTwoFactorQueries();
}
public static void AddUserKeyCommands(this IServiceCollection services, IGlobalSettings globalSettings)
@ -54,4 +57,9 @@ public static class UserServiceCollectionExtensions
services.AddScoped<IGetWebAuthnLoginCredentialAssertionOptionsCommand, GetWebAuthnLoginCredentialAssertionOptionsCommand>();
services.AddScoped<IAssertWebAuthnLoginCredentialCommand, AssertWebAuthnLoginCredentialCommand>();
}
private static void AddTwoFactorQueries(this IServiceCollection services)
{
services.AddScoped<ITwoFactorIsEnabledQuery, TwoFactorIsEnabledQuery>();
}
}