mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42: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:
@ -0,0 +1,337 @@
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Auth.UserFeatures.TwoFactorAuth;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class TwoFactorIsEnabledQueryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData(TwoFactorProviderType.Authenticator)]
|
||||
[BitAutoData(TwoFactorProviderType.Email)]
|
||||
[BitAutoData(TwoFactorProviderType.Remember)]
|
||||
[BitAutoData(TwoFactorProviderType.OrganizationDuo)]
|
||||
[BitAutoData(TwoFactorProviderType.WebAuthn)]
|
||||
public async Task TwoFactorIsEnabledQuery_WithProviderTypeNotRequiringPremium_ReturnsAllTwoFactorEnabled(
|
||||
TwoFactorProviderType freeProviderType,
|
||||
SutProvider<TwoFactorIsEnabledQuery> sutProvider,
|
||||
List<UserWithCalculatedPremium> usersWithCalculatedPremium)
|
||||
{
|
||||
// Arrange
|
||||
var userIds = usersWithCalculatedPremium.Select(u => u.Id).ToList();
|
||||
var twoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
{ freeProviderType, new TwoFactorProvider { Enabled = true } } // Does not require premium
|
||||
};
|
||||
|
||||
foreach (var user in usersWithCalculatedPremium)
|
||||
{
|
||||
user.HasPremiumAccess = false;
|
||||
user.SetTwoFactorProviders(twoFactorProviders);
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetManyWithCalculatedPremiumAsync(Arg.Is<IEnumerable<Guid>>(i => i.All(userIds.Contains)))
|
||||
.Returns(usersWithCalculatedPremium);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds);
|
||||
|
||||
// Assert
|
||||
foreach (var userDetail in usersWithCalculatedPremium)
|
||||
{
|
||||
Assert.Contains(result, res => res.userId == userDetail.Id && res.twoFactorIsEnabled == true);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task TwoFactorIsEnabledQuery_WithNoTwoFactorEnabled_ReturnsAllTwoFactorDisabled(
|
||||
SutProvider<TwoFactorIsEnabledQuery> sutProvider,
|
||||
List<UserWithCalculatedPremium> usersWithCalculatedPremium)
|
||||
{
|
||||
// Arrange
|
||||
var userIds = usersWithCalculatedPremium.Select(u => u.Id).ToList();
|
||||
var twoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
{ TwoFactorProviderType.Email, new TwoFactorProvider { Enabled = false } }
|
||||
};
|
||||
|
||||
foreach (var user in usersWithCalculatedPremium)
|
||||
{
|
||||
user.SetTwoFactorProviders(twoFactorProviders);
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetManyWithCalculatedPremiumAsync(Arg.Is<IEnumerable<Guid>>(i => i.All(userIds.Contains)))
|
||||
.Returns(usersWithCalculatedPremium);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds);
|
||||
|
||||
// Assert
|
||||
foreach (var userDetail in usersWithCalculatedPremium)
|
||||
{
|
||||
Assert.Contains(result, res => res.userId == userDetail.Id && res.twoFactorIsEnabled == false);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(TwoFactorProviderType.Duo)]
|
||||
[BitAutoData(TwoFactorProviderType.YubiKey)]
|
||||
public async Task TwoFactorIsEnabledQuery_WithProviderTypeRequiringPremium_ReturnsMixedResults(
|
||||
TwoFactorProviderType premiumProviderType,
|
||||
SutProvider<TwoFactorIsEnabledQuery> sutProvider,
|
||||
List<UserWithCalculatedPremium> usersWithCalculatedPremium)
|
||||
{
|
||||
// Arrange
|
||||
var userIds = usersWithCalculatedPremium.Select(u => u.Id).ToList();
|
||||
var twoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
{ TwoFactorProviderType.Email, new TwoFactorProvider { Enabled = false } },
|
||||
{ premiumProviderType, new TwoFactorProvider { Enabled = true } }
|
||||
};
|
||||
|
||||
foreach (var user in usersWithCalculatedPremium)
|
||||
{
|
||||
user.HasPremiumAccess = usersWithCalculatedPremium.IndexOf(user) == 0; // Only the first user has premium access
|
||||
user.SetTwoFactorProviders(twoFactorProviders);
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetManyWithCalculatedPremiumAsync(Arg.Is<IEnumerable<Guid>>(i => i.All(userIds.Contains)))
|
||||
.Returns(usersWithCalculatedPremium);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds);
|
||||
|
||||
// Assert
|
||||
foreach (var userDetail in usersWithCalculatedPremium)
|
||||
{
|
||||
Assert.Contains(result, res => res.userId == userDetail.Id && res.twoFactorIsEnabled == userDetail.HasPremiumAccess);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task TwoFactorIsEnabledQuery_WithNullTwoFactorProviders_ReturnsAllTwoFactorDisabled(
|
||||
SutProvider<TwoFactorIsEnabledQuery> sutProvider,
|
||||
List<UserWithCalculatedPremium> usersWithCalculatedPremium)
|
||||
{
|
||||
// Arrange
|
||||
var userIds = usersWithCalculatedPremium.Select(u => u.Id).ToList();
|
||||
|
||||
foreach (var user in usersWithCalculatedPremium)
|
||||
{
|
||||
user.TwoFactorProviders = null; // No two-factor providers configured
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetManyWithCalculatedPremiumAsync(Arg.Is<IEnumerable<Guid>>(i => i.All(userIds.Contains)))
|
||||
.Returns(usersWithCalculatedPremium);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds);
|
||||
|
||||
// Assert
|
||||
foreach (var userDetail in usersWithCalculatedPremium)
|
||||
{
|
||||
Assert.Contains(result, res => res.userId == userDetail.Id && res.twoFactorIsEnabled == false);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task TwoFactorIsEnabledQuery_WithNoUserIds_ReturnsAllTwoFactorDisabled(
|
||||
SutProvider<TwoFactorIsEnabledQuery> sutProvider,
|
||||
List<OrganizationUserUserDetails> users)
|
||||
{
|
||||
// Arrange
|
||||
foreach (var user in users)
|
||||
{
|
||||
user.UserId = null;
|
||||
}
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(users);
|
||||
|
||||
// Assert
|
||||
foreach (var user in users)
|
||||
{
|
||||
Assert.Contains(result, res => res.user.Equals(user) && res.twoFactorIsEnabled == false);
|
||||
}
|
||||
|
||||
// No UserIds were supplied so no calls to the UserRepository should have been made
|
||||
await sutProvider.GetDependency<IUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.GetManyWithCalculatedPremiumAsync(default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(TwoFactorProviderType.Authenticator)]
|
||||
[BitAutoData(TwoFactorProviderType.Email)]
|
||||
[BitAutoData(TwoFactorProviderType.Remember)]
|
||||
[BitAutoData(TwoFactorProviderType.OrganizationDuo)]
|
||||
[BitAutoData(TwoFactorProviderType.WebAuthn)]
|
||||
public async Task TwoFactorIsEnabledQuery_WithProviderTypeNotRequiringPremium_ReturnsTrue(
|
||||
TwoFactorProviderType freeProviderType,
|
||||
SutProvider<TwoFactorIsEnabledQuery> sutProvider,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
var twoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
{ freeProviderType, new TwoFactorProvider { Enabled = true } }
|
||||
};
|
||||
|
||||
user.Premium = false;
|
||||
user.SetTwoFactorProviders(twoFactorProviders);
|
||||
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
|
||||
await sutProvider.GetDependency<IUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.GetManyWithCalculatedPremiumAsync(default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task TwoFactorIsEnabledQuery_WithNoTwoFactorEnabled_ReturnsFalse(
|
||||
SutProvider<TwoFactorIsEnabledQuery> sutProvider,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
var twoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
{ TwoFactorProviderType.Email, new TwoFactorProvider { Enabled = false } }
|
||||
};
|
||||
|
||||
user.SetTwoFactorProviders(twoFactorProviders);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
|
||||
await sutProvider.GetDependency<IUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.GetManyWithCalculatedPremiumAsync(default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(TwoFactorProviderType.Duo)]
|
||||
[BitAutoData(TwoFactorProviderType.YubiKey)]
|
||||
public async Task TwoFactorIsEnabledQuery_WithProviderTypeRequiringPremium_WithoutPremium_ReturnsFalse(
|
||||
TwoFactorProviderType premiumProviderType,
|
||||
SutProvider<TwoFactorIsEnabledQuery> sutProvider,
|
||||
UserWithCalculatedPremium user)
|
||||
{
|
||||
// Arrange
|
||||
var twoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
{ premiumProviderType, new TwoFactorProvider { Enabled = true } }
|
||||
};
|
||||
|
||||
user.Premium = false;
|
||||
user.HasPremiumAccess = false;
|
||||
user.SetTwoFactorProviders(twoFactorProviders);
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetManyWithCalculatedPremiumAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(user.Id)))
|
||||
.Returns(new List<UserWithCalculatedPremium> { user });
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(TwoFactorProviderType.Duo)]
|
||||
[BitAutoData(TwoFactorProviderType.YubiKey)]
|
||||
public async Task TwoFactorIsEnabledQuery_WithProviderTypeRequiringPremium_WithUserPremium_ReturnsTrue(
|
||||
TwoFactorProviderType premiumProviderType,
|
||||
SutProvider<TwoFactorIsEnabledQuery> sutProvider,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
var twoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
{ premiumProviderType, new TwoFactorProvider { Enabled = true } }
|
||||
};
|
||||
|
||||
user.Premium = true;
|
||||
user.SetTwoFactorProviders(twoFactorProviders);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
|
||||
await sutProvider.GetDependency<IUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.GetManyWithCalculatedPremiumAsync(default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(TwoFactorProviderType.Duo)]
|
||||
[BitAutoData(TwoFactorProviderType.YubiKey)]
|
||||
public async Task TwoFactorIsEnabledQuery_WithProviderTypeRequiringPremium_WithOrgPremium_ReturnsTrue(
|
||||
TwoFactorProviderType premiumProviderType,
|
||||
SutProvider<TwoFactorIsEnabledQuery> sutProvider,
|
||||
UserWithCalculatedPremium user)
|
||||
{
|
||||
// Arrange
|
||||
var twoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
{ premiumProviderType, new TwoFactorProvider { Enabled = true } }
|
||||
};
|
||||
|
||||
user.Premium = false;
|
||||
user.HasPremiumAccess = true;
|
||||
user.SetTwoFactorProviders(twoFactorProviders);
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetManyWithCalculatedPremiumAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(user.Id)))
|
||||
.Returns(new List<UserWithCalculatedPremium> { user });
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task TwoFactorIsEnabledQuery_WithNullTwoFactorProviders_ReturnsFalse(
|
||||
SutProvider<TwoFactorIsEnabledQuery> sutProvider,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
user.TwoFactorProviders = null; // No two-factor providers configured
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user