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:
@ -12,6 +12,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -48,6 +49,7 @@ public class OrganizationUsersController : Controller
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
|
||||
public OrganizationUsersController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -66,7 +68,8 @@ public class OrganizationUsersController : Controller
|
||||
IAuthorizationService authorizationService,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IFeatureService featureService,
|
||||
ISsoConfigRepository ssoConfigRepository)
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -85,6 +88,7 @@ public class OrganizationUsersController : Controller
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_featureService = featureService;
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -126,8 +130,12 @@ public class OrganizationUsersController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organizationUsers = await _organizationUserRepository
|
||||
.GetManyDetailsByOrganizationAsync(orgId, includeGroups, includeCollections);
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization))
|
||||
{
|
||||
return await Get_vNext(orgId, includeGroups, includeCollections);
|
||||
}
|
||||
|
||||
var organizationUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(orgId, includeGroups, includeCollections);
|
||||
var responseTasks = organizationUsers
|
||||
.Select(async o =>
|
||||
{
|
||||
@ -332,7 +340,9 @@ public class OrganizationUsersController : Controller
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
var results = await _organizationService.ConfirmUsersAsync(orgGuidId, model.ToDictionary(), userId.Value,
|
||||
var results = _featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)
|
||||
? await _organizationService.ConfirmUsersAsync_vNext(orgGuidId, model.ToDictionary(), userId.Value)
|
||||
: await _organizationService.ConfirmUsersAsync(orgGuidId, model.ToDictionary(), userId.Value,
|
||||
_userService);
|
||||
|
||||
return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r =>
|
||||
@ -681,4 +691,32 @@ public class OrganizationUsersController : Controller
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
private async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get_vNext(Guid orgId,
|
||||
bool includeGroups = false, bool includeCollections = false)
|
||||
{
|
||||
var organizationUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(orgId, includeGroups, includeCollections);
|
||||
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers);
|
||||
var responseTasks = organizationUsers
|
||||
.Select(async o =>
|
||||
{
|
||||
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == o.Id).twoFactorIsEnabled;
|
||||
var orgUser = new OrganizationUserUserDetailsResponseModel(o, userTwoFactorEnabled);
|
||||
|
||||
// Downgrade Custom users with no other permissions than 'Edit/Delete Assigned Collections' to User
|
||||
orgUser.Type = GetFlexibleCollectionsUserType(orgUser.Type, orgUser.Permissions);
|
||||
|
||||
// Set 'Edit/Delete Assigned Collections' custom permissions to false
|
||||
if (orgUser.Permissions is not null)
|
||||
{
|
||||
orgUser.Permissions.EditAssignedCollections = false;
|
||||
orgUser.Permissions.DeleteAssignedCollections = false;
|
||||
}
|
||||
|
||||
return orgUser;
|
||||
});
|
||||
var responses = await Task.WhenAll(responseTasks);
|
||||
|
||||
return new ListResponseModel<OrganizationUserUserDetailsResponseModel>(responses);
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,12 @@
|
||||
using Bit.Api.AdminConsole.Public.Models.Request;
|
||||
using Bit.Api.AdminConsole.Public.Models.Response;
|
||||
using Bit.Api.Models.Public.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -26,6 +29,8 @@ public class MembersController : Controller
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
|
||||
public MembersController(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@ -37,7 +42,9 @@ public class MembersController : Controller
|
||||
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IPaymentService paymentService,
|
||||
IOrganizationRepository organizationRepository)
|
||||
IOrganizationRepository organizationRepository,
|
||||
IFeatureService featureService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_groupRepository = groupRepository;
|
||||
@ -49,6 +56,8 @@ public class MembersController : Controller
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_paymentService = paymentService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_featureService = featureService;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -108,11 +117,18 @@ public class MembersController : Controller
|
||||
[ProducesResponseType(typeof(ListResponseModel<MemberResponseModel>), (int)HttpStatusCode.OK)]
|
||||
public async Task<IActionResult> List()
|
||||
{
|
||||
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(
|
||||
_currentContext.OrganizationId.Value);
|
||||
var organizationUserUserDetails = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(_currentContext.OrganizationId.Value);
|
||||
// TODO: Get all CollectionUser associations for the organization and marry them up here for the response.
|
||||
var memberResponsesTasks = users.Select(async u => new MemberResponseModel(u,
|
||||
await _userService.TwoFactorIsEnabledAsync(u), null));
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization))
|
||||
{
|
||||
return await List_vNext(organizationUserUserDetails);
|
||||
}
|
||||
|
||||
var memberResponsesTasks = organizationUserUserDetails.Select(async u =>
|
||||
{
|
||||
return new MemberResponseModel(u, await _userService.TwoFactorIsEnabledAsync(u), null);
|
||||
});
|
||||
var memberResponses = await Task.WhenAll(memberResponsesTasks);
|
||||
var response = new ListResponseModel<MemberResponseModel>(memberResponses);
|
||||
return new JsonResult(response);
|
||||
@ -252,4 +268,15 @@ public class MembersController : Controller
|
||||
await _organizationService.ResendInviteAsync(_currentContext.OrganizationId.Value, null, id);
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
private async Task<JsonResult> List_vNext(ICollection<OrganizationUserUserDetails> organizationUserUserDetails)
|
||||
{
|
||||
var orgUsersTwoFactorIsEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUserUserDetails);
|
||||
var memberResponses = organizationUserUserDetails.Select(u =>
|
||||
{
|
||||
return new MemberResponseModel(u, orgUsersTwoFactorIsEnabled.FirstOrDefault(tuple => tuple.user == u).twoFactorIsEnabled, null);
|
||||
});
|
||||
var response = new ListResponseModel<MemberResponseModel>(memberResponses);
|
||||
return new JsonResult(response);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user