1
0
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:
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,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);
}
}