mirror of
https://github.com/bitwarden/server.git
synced 2025-05-21 11:34:31 -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:
parent
19dc7c339b
commit
8d69bb0aaa
@ -2,6 +2,9 @@
|
|||||||
using Bit.Admin.Models;
|
using Bit.Admin.Models;
|
||||||
using Bit.Admin.Services;
|
using Bit.Admin.Services;
|
||||||
using Bit.Admin.Utilities;
|
using Bit.Admin.Utilities;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -21,19 +24,28 @@ public class UsersController : Controller
|
|||||||
private readonly IPaymentService _paymentService;
|
private readonly IPaymentService _paymentService;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly IAccessControlService _accessControlService;
|
private readonly IAccessControlService _accessControlService;
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
|
|
||||||
public UsersController(
|
public UsersController(
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
ICipherRepository cipherRepository,
|
ICipherRepository cipherRepository,
|
||||||
IPaymentService paymentService,
|
IPaymentService paymentService,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IAccessControlService accessControlService)
|
IAccessControlService accessControlService,
|
||||||
|
ICurrentContext currentContext,
|
||||||
|
IFeatureService featureService,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
||||||
{
|
{
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_cipherRepository = cipherRepository;
|
_cipherRepository = cipherRepository;
|
||||||
_paymentService = paymentService;
|
_paymentService = paymentService;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_accessControlService = accessControlService;
|
_accessControlService = accessControlService;
|
||||||
|
_currentContext = currentContext;
|
||||||
|
_featureService = featureService;
|
||||||
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequirePermission(Permission.User_List_View)]
|
[RequirePermission(Permission.User_List_View)]
|
||||||
@ -51,6 +63,12 @@ public class UsersController : Controller
|
|||||||
|
|
||||||
var skip = (page - 1) * count;
|
var skip = (page - 1) * count;
|
||||||
var users = await _userRepository.SearchAsync(email, skip, count);
|
var users = await _userRepository.SearchAsync(email, skip, count);
|
||||||
|
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization))
|
||||||
|
{
|
||||||
|
TempData["UsersTwoFactorIsEnabled"] = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(users.Select(u => u.Id));
|
||||||
|
}
|
||||||
|
|
||||||
return View(new UsersModel
|
return View(new UsersModel
|
||||||
{
|
{
|
||||||
Items = users as List<User>,
|
Items = users as List<User>,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
@model UsersModel
|
@model UsersModel
|
||||||
@inject Bit.Core.Services.IUserService userService
|
@inject Bit.Core.Services.IUserService userService
|
||||||
|
@inject Bit.Core.Services.IFeatureService featureService
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Users";
|
ViewData["Title"] = "Users";
|
||||||
}
|
}
|
||||||
@ -69,6 +70,20 @@
|
|||||||
{
|
{
|
||||||
<i class="fa fa-times-circle-o fa-lg fa-fw text-muted" title="Email Not Verified"></i>
|
<i class="fa fa-times-circle-o fa-lg fa-fw text-muted" title="Email Not Verified"></i>
|
||||||
}
|
}
|
||||||
|
@if (featureService.IsEnabled(Bit.Core.FeatureFlagKeys.MembersTwoFAQueryOptimization))
|
||||||
|
{
|
||||||
|
var usersTwoFactorIsEnabled = TempData["UsersTwoFactorIsEnabled"] as IEnumerable<(Guid userId, bool twoFactorIsEnabled)>;
|
||||||
|
@if(usersTwoFactorIsEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled)
|
||||||
|
{
|
||||||
|
<i class="fa fa-lock fa-lg fa-fw" title="2FA Enabled"></i>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<i class="fa fa-unlock fa-lg fa-fw text-muted" title="2FA Not Enabled"></i>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
@if(await userService.TwoFactorIsEnabledAsync(user))
|
@if(await userService.TwoFactorIsEnabledAsync(user))
|
||||||
{
|
{
|
||||||
<i class="fa fa-lock fa-lg fa-fw" title="2FA Enabled"></i>
|
<i class="fa fa-lock fa-lg fa-fw" title="2FA Enabled"></i>
|
||||||
@ -77,6 +92,7 @@
|
|||||||
{
|
{
|
||||||
<i class="fa fa-unlock fa-lg fa-fw text-muted" title="2FA Not Enabled"></i>
|
<i class="fa fa-unlock fa-lg fa-fw text-muted" title="2FA Not Enabled"></i>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -48,6 +49,7 @@ public class OrganizationUsersController : Controller
|
|||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||||
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
|
|
||||||
public OrganizationUsersController(
|
public OrganizationUsersController(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@ -66,7 +68,8 @@ public class OrganizationUsersController : Controller
|
|||||||
IAuthorizationService authorizationService,
|
IAuthorizationService authorizationService,
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
ISsoConfigRepository ssoConfigRepository)
|
ISsoConfigRepository ssoConfigRepository,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
@ -85,6 +88,7 @@ public class OrganizationUsersController : Controller
|
|||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_ssoConfigRepository = ssoConfigRepository;
|
_ssoConfigRepository = ssoConfigRepository;
|
||||||
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
@ -126,8 +130,12 @@ public class OrganizationUsersController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var organizationUsers = await _organizationUserRepository
|
if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization))
|
||||||
.GetManyDetailsByOrganizationAsync(orgId, includeGroups, includeCollections);
|
{
|
||||||
|
return await Get_vNext(orgId, includeGroups, includeCollections);
|
||||||
|
}
|
||||||
|
|
||||||
|
var organizationUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(orgId, includeGroups, includeCollections);
|
||||||
var responseTasks = organizationUsers
|
var responseTasks = organizationUsers
|
||||||
.Select(async o =>
|
.Select(async o =>
|
||||||
{
|
{
|
||||||
@ -332,7 +340,9 @@ public class OrganizationUsersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var userId = _userService.GetProperUserId(User);
|
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);
|
_userService);
|
||||||
|
|
||||||
return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r =>
|
return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r =>
|
||||||
@ -681,4 +691,32 @@ public class OrganizationUsersController : Controller
|
|||||||
|
|
||||||
return type;
|
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.Request;
|
||||||
using Bit.Api.AdminConsole.Public.Models.Response;
|
using Bit.Api.AdminConsole.Public.Models.Response;
|
||||||
using Bit.Api.Models.Public.Response;
|
using Bit.Api.Models.Public.Response;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@ -26,6 +29,8 @@ public class MembersController : Controller
|
|||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
private readonly IPaymentService _paymentService;
|
private readonly IPaymentService _paymentService;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
|
|
||||||
public MembersController(
|
public MembersController(
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
@ -37,7 +42,9 @@ public class MembersController : Controller
|
|||||||
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand,
|
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand,
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
IPaymentService paymentService,
|
IPaymentService paymentService,
|
||||||
IOrganizationRepository organizationRepository)
|
IOrganizationRepository organizationRepository,
|
||||||
|
IFeatureService featureService,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
||||||
{
|
{
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_groupRepository = groupRepository;
|
_groupRepository = groupRepository;
|
||||||
@ -49,6 +56,8 @@ public class MembersController : Controller
|
|||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
_paymentService = paymentService;
|
_paymentService = paymentService;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
|
_featureService = featureService;
|
||||||
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -108,11 +117,18 @@ public class MembersController : Controller
|
|||||||
[ProducesResponseType(typeof(ListResponseModel<MemberResponseModel>), (int)HttpStatusCode.OK)]
|
[ProducesResponseType(typeof(ListResponseModel<MemberResponseModel>), (int)HttpStatusCode.OK)]
|
||||||
public async Task<IActionResult> List()
|
public async Task<IActionResult> List()
|
||||||
{
|
{
|
||||||
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(
|
var organizationUserUserDetails = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(_currentContext.OrganizationId.Value);
|
||||||
_currentContext.OrganizationId.Value);
|
|
||||||
// TODO: Get all CollectionUser associations for the organization and marry them up here for the response.
|
// 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 memberResponses = await Task.WhenAll(memberResponsesTasks);
|
||||||
var response = new ListResponseModel<MemberResponseModel>(memberResponses);
|
var response = new ListResponseModel<MemberResponseModel>(memberResponses);
|
||||||
return new JsonResult(response);
|
return new JsonResult(response);
|
||||||
@ -252,4 +268,15 @@ public class MembersController : Controller
|
|||||||
await _organizationService.ResendInviteAsync(_currentContext.OrganizationId.Value, null, id);
|
await _organizationService.ResendInviteAsync(_currentContext.OrganizationId.Value, null, id);
|
||||||
return new OkResult();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,8 @@ public interface IOrganizationService
|
|||||||
Guid confirmingUserId, IUserService userService);
|
Guid confirmingUserId, IUserService userService);
|
||||||
Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,
|
Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,
|
||||||
Guid confirmingUserId, IUserService userService);
|
Guid confirmingUserId, IUserService userService);
|
||||||
|
Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync_vNext(Guid organizationId, Dictionary<Guid, string> keys,
|
||||||
|
Guid confirmingUserId);
|
||||||
[Obsolete("IDeleteOrganizationUserCommand should be used instead. To be removed by EC-607.")]
|
[Obsolete("IDeleteOrganizationUserCommand should be used instead. To be removed by EC-607.")]
|
||||||
Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);
|
Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);
|
||||||
[Obsolete("IDeleteOrganizationUserCommand should be used instead. To be removed by EC-607.")]
|
[Obsolete("IDeleteOrganizationUserCommand should be used instead. To be removed by EC-607.")]
|
||||||
|
@ -13,6 +13,7 @@ using Bit.Core.Auth.Enums;
|
|||||||
using Bit.Core.Auth.Models.Business;
|
using Bit.Core.Auth.Models.Business;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
@ -67,6 +68,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory;
|
private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory;
|
||||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
|
|
||||||
public OrganizationService(
|
public OrganizationService(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@ -99,7 +101,8 @@ public class OrganizationService : IOrganizationService
|
|||||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
||||||
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
|
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
|
||||||
IProviderRepository providerRepository,
|
IProviderRepository providerRepository,
|
||||||
IFeatureService featureService)
|
IFeatureService featureService,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
@ -132,6 +135,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
_orgUserInviteTokenableFactory = orgUserInviteTokenableFactory;
|
_orgUserInviteTokenableFactory = orgUserInviteTokenableFactory;
|
||||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
|
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
|
||||||
@ -1291,7 +1295,10 @@ public class OrganizationService : IOrganizationService
|
|||||||
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
||||||
Guid confirmingUserId, IUserService userService)
|
Guid confirmingUserId, IUserService userService)
|
||||||
{
|
{
|
||||||
var result = await ConfirmUsersAsync(organizationId, new Dictionary<Guid, string>() { { organizationUserId, key } },
|
var result = _featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)
|
||||||
|
? await ConfirmUsersAsync_vNext(organizationId, new Dictionary<Guid, string>() { { organizationUserId, key } },
|
||||||
|
confirmingUserId)
|
||||||
|
: await ConfirmUsersAsync(organizationId, new Dictionary<Guid, string>() { { organizationUserId, key } },
|
||||||
confirmingUserId, userService);
|
confirmingUserId, userService);
|
||||||
|
|
||||||
if (!result.Any())
|
if (!result.Any())
|
||||||
@ -1376,6 +1383,77 @@ public class OrganizationService : IOrganizationService
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync_vNext(Guid organizationId, Dictionary<Guid, string> keys,
|
||||||
|
Guid confirmingUserId)
|
||||||
|
{
|
||||||
|
var selectedOrganizationUsers = await _organizationUserRepository.GetManyAsync(keys.Keys);
|
||||||
|
var validSelectedOrganizationUsers = selectedOrganizationUsers
|
||||||
|
.Where(u => u.Status == OrganizationUserStatusType.Accepted && u.OrganizationId == organizationId && u.UserId != null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (!validSelectedOrganizationUsers.Any())
|
||||||
|
{
|
||||||
|
return new List<Tuple<OrganizationUser, string>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var validSelectedUserIds = validSelectedOrganizationUsers.Select(u => u.UserId.Value).ToList();
|
||||||
|
|
||||||
|
var organization = await GetOrgById(organizationId);
|
||||||
|
var allUsersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validSelectedUserIds);
|
||||||
|
var users = await _userRepository.GetManyWithCalculatedPremiumAsync(validSelectedUserIds);
|
||||||
|
var usersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(validSelectedUserIds);
|
||||||
|
|
||||||
|
var keyedFilteredUsers = validSelectedOrganizationUsers.ToDictionary(u => u.UserId.Value, u => u);
|
||||||
|
var keyedOrganizationUsers = allUsersOrgs.GroupBy(u => u.UserId.Value)
|
||||||
|
.ToDictionary(u => u.Key, u => u.ToList());
|
||||||
|
|
||||||
|
var succeededUsers = new List<OrganizationUser>();
|
||||||
|
var result = new List<Tuple<OrganizationUser, string>>();
|
||||||
|
|
||||||
|
foreach (var user in users)
|
||||||
|
{
|
||||||
|
if (!keyedFilteredUsers.ContainsKey(user.Id))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var orgUser = keyedFilteredUsers[user.Id];
|
||||||
|
var orgUsers = keyedOrganizationUsers.GetValueOrDefault(user.Id, new List<OrganizationUser>());
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (organization.PlanType == PlanType.Free && (orgUser.Type == OrganizationUserType.Admin
|
||||||
|
|| orgUser.Type == OrganizationUserType.Owner))
|
||||||
|
{
|
||||||
|
// Since free organizations only supports a few users there is not much point in avoiding N+1 queries for this.
|
||||||
|
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(user.Id);
|
||||||
|
if (adminCount > 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("User can only be an admin of one free organization.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var twoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled;
|
||||||
|
await CheckPolicies_vNext(organizationId, user, orgUsers, twoFactorEnabled);
|
||||||
|
orgUser.Status = OrganizationUserStatusType.Confirmed;
|
||||||
|
orgUser.Key = keys[orgUser.Id];
|
||||||
|
orgUser.Email = null;
|
||||||
|
|
||||||
|
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
||||||
|
await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), user.Email, orgUser.AccessSecretsManager);
|
||||||
|
await DeleteAndPushUserRegistrationAsync(organizationId, user.Id);
|
||||||
|
succeededUsers.Add(orgUser);
|
||||||
|
result.Add(Tuple.Create(orgUser, ""));
|
||||||
|
}
|
||||||
|
catch (BadRequestException e)
|
||||||
|
{
|
||||||
|
result.Add(Tuple.Create(orgUser, e.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _organizationUserRepository.ReplaceManyAsync(succeededUsers);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
internal async Task<(bool canScale, string failureReason)> CanScaleAsync(
|
internal async Task<(bool canScale, string failureReason)> CanScaleAsync(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
int seatsToAdd)
|
int seatsToAdd)
|
||||||
@ -1485,6 +1563,33 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CheckPolicies_vNext(Guid organizationId, UserWithCalculatedPremium user,
|
||||||
|
ICollection<OrganizationUser> userOrgs, bool twoFactorEnabled)
|
||||||
|
{
|
||||||
|
// Enforce Two Factor Authentication Policy for this organization
|
||||||
|
var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication))
|
||||||
|
.Any(p => p.OrganizationId == organizationId);
|
||||||
|
if (orgRequiresTwoFactor && !twoFactorEnabled)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("User does not have two-step login enabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId);
|
||||||
|
var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg);
|
||||||
|
var otherSingleOrgPolicies =
|
||||||
|
singleOrgPolicies.Where(p => p.OrganizationId != organizationId);
|
||||||
|
// Enforce Single Organization Policy for this organization
|
||||||
|
if (hasOtherOrgs && singleOrgPolicies.Any(p => p.OrganizationId == organizationId))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cannot confirm this member to the organization until they leave or remove all other organizations.");
|
||||||
|
}
|
||||||
|
// Enforce Single Organization Policy of other organizations user is a member of
|
||||||
|
if (otherSingleOrgPolicies.Any())
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cannot confirm this member to the organization because they are in another organization which forbids it.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Obsolete("IDeleteOrganizationUserCommand should be used instead. To be removed by EC-607.")]
|
[Obsolete("IDeleteOrganizationUserCommand should be used instead. To be removed by EC-607.")]
|
||||||
public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
|
public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
|
||||||
{
|
{
|
||||||
@ -2319,7 +2424,21 @@ public class OrganizationService : IOrganizationService
|
|||||||
await AutoAddSeatsAsync(organization, 1);
|
await AutoAddSeatsAsync(organization, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization))
|
||||||
|
{
|
||||||
|
var userTwoFactorIsEnabled = false;
|
||||||
|
// Only check Two Factor Authentication status if the user is linked to a user account
|
||||||
|
if (organizationUser.UserId.HasValue)
|
||||||
|
{
|
||||||
|
userTwoFactorIsEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(new[] { organizationUser.UserId.Value })).FirstOrDefault().twoFactorIsEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
await CheckPoliciesBeforeRestoreAsync_vNext(organizationUser, userTwoFactorIsEnabled);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
await CheckPoliciesBeforeRestoreAsync(organizationUser, userService);
|
await CheckPoliciesBeforeRestoreAsync(organizationUser, userService);
|
||||||
|
}
|
||||||
|
|
||||||
var status = GetPriorActiveOrganizationUserStatusType(organizationUser);
|
var status = GetPriorActiveOrganizationUserStatusType(organizationUser);
|
||||||
|
|
||||||
@ -2351,6 +2470,14 @@ public class OrganizationService : IOrganizationService
|
|||||||
deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId);
|
deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Query Two Factor Authentication status for all users in the organization
|
||||||
|
// This is an optimization to avoid querying the Two Factor Authentication status for each user individually
|
||||||
|
IEnumerable<(Guid userId, bool twoFactorIsEnabled)> organizationUsersTwoFactorEnabled = null;
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization))
|
||||||
|
{
|
||||||
|
organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(filteredUsers.Select(ou => ou.UserId.Value));
|
||||||
|
}
|
||||||
|
|
||||||
var result = new List<Tuple<OrganizationUser, string>>();
|
var result = new List<Tuple<OrganizationUser, string>>();
|
||||||
|
|
||||||
foreach (var organizationUser in filteredUsers)
|
foreach (var organizationUser in filteredUsers)
|
||||||
@ -2372,7 +2499,15 @@ public class OrganizationService : IOrganizationService
|
|||||||
throw new BadRequestException("Only owners can restore other owners.");
|
throw new BadRequestException("Only owners can restore other owners.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization))
|
||||||
|
{
|
||||||
|
var twoFactorIsEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(ou => ou.userId == organizationUser.UserId.Value).twoFactorIsEnabled;
|
||||||
|
await CheckPoliciesBeforeRestoreAsync_vNext(organizationUser, twoFactorIsEnabled);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
await CheckPoliciesBeforeRestoreAsync(organizationUser, userService);
|
await CheckPoliciesBeforeRestoreAsync(organizationUser, userService);
|
||||||
|
}
|
||||||
|
|
||||||
var status = GetPriorActiveOrganizationUserStatusType(organizationUser);
|
var status = GetPriorActiveOrganizationUserStatusType(organizationUser);
|
||||||
|
|
||||||
@ -2438,6 +2573,52 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CheckPoliciesBeforeRestoreAsync_vNext(OrganizationUser orgUser, bool userHasTwoFactorEnabled)
|
||||||
|
{
|
||||||
|
// An invited OrganizationUser isn't linked with a user account yet, so these checks are irrelevant
|
||||||
|
// The user will be subject to the same checks when they try to accept the invite
|
||||||
|
if (GetPriorActiveOrganizationUserStatusType(orgUser) == OrganizationUserStatusType.Invited)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId = orgUser.UserId.Value;
|
||||||
|
|
||||||
|
// Enforce Single Organization Policy of organization user is being restored to
|
||||||
|
var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(userId);
|
||||||
|
var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId);
|
||||||
|
var singleOrgPoliciesApplyingToRevokedUsers = await _policyService.GetPoliciesApplicableToUserAsync(userId,
|
||||||
|
PolicyType.SingleOrg, OrganizationUserStatusType.Revoked);
|
||||||
|
var singleOrgPolicyApplies = singleOrgPoliciesApplyingToRevokedUsers.Any(p => p.OrganizationId == orgUser.OrganizationId);
|
||||||
|
|
||||||
|
if (hasOtherOrgs && singleOrgPolicyApplies)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You cannot restore this user until " +
|
||||||
|
"they leave or remove all other organizations.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce Single Organization Policy of other organizations user is a member of
|
||||||
|
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId,
|
||||||
|
PolicyType.SingleOrg);
|
||||||
|
if (anySingleOrgPolicies)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You cannot restore this user because they are a member of " +
|
||||||
|
"another organization which forbids it");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce Two Factor Authentication Policy of organization user is trying to join
|
||||||
|
if (!userHasTwoFactorEnabled)
|
||||||
|
{
|
||||||
|
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId,
|
||||||
|
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
|
||||||
|
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You cannot restore this user until they enable " +
|
||||||
|
"two-step login on their user account.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser)
|
static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser)
|
||||||
{
|
{
|
||||||
// Determine status to revert back to
|
// Determine status to revert back to
|
||||||
|
@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -24,6 +25,8 @@ public class PolicyService : IPolicyService
|
|||||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
|
|
||||||
public PolicyService(
|
public PolicyService(
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
@ -33,7 +36,9 @@ public class PolicyService : IPolicyService
|
|||||||
IPolicyRepository policyRepository,
|
IPolicyRepository policyRepository,
|
||||||
ISsoConfigRepository ssoConfigRepository,
|
ISsoConfigRepository ssoConfigRepository,
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
GlobalSettings globalSettings)
|
GlobalSettings globalSettings,
|
||||||
|
IFeatureService featureService,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
||||||
{
|
{
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
@ -43,6 +48,8 @@ public class PolicyService : IPolicyService
|
|||||||
_ssoConfigRepository = ssoConfigRepository;
|
_ssoConfigRepository = ssoConfigRepository;
|
||||||
_mailService = mailService;
|
_mailService = mailService;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
|
_featureService = featureService;
|
||||||
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveAsync(Policy policy, IUserService userService, IOrganizationService organizationService,
|
public async Task SaveAsync(Policy policy, IUserService userService, IOrganizationService organizationService,
|
||||||
@ -81,6 +88,12 @@ public class PolicyService : IPolicyService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization))
|
||||||
|
{
|
||||||
|
await EnablePolicy_vNext(policy, org, organizationService, savingUserId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await EnablePolicy(policy, org, userService, organizationService, savingUserId);
|
await EnablePolicy(policy, org, userService, organizationService, savingUserId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -261,8 +274,7 @@ public class PolicyService : IPolicyService
|
|||||||
var currentPolicy = await _policyRepository.GetByIdAsync(policy.Id);
|
var currentPolicy = await _policyRepository.GetByIdAsync(policy.Id);
|
||||||
if (!currentPolicy?.Enabled ?? true)
|
if (!currentPolicy?.Enabled ?? true)
|
||||||
{
|
{
|
||||||
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(
|
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(policy.OrganizationId);
|
||||||
policy.OrganizationId);
|
|
||||||
var removableOrgUsers = orgUsers.Where(ou =>
|
var removableOrgUsers = orgUsers.Where(ou =>
|
||||||
ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked &&
|
ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked &&
|
||||||
ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin &&
|
ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin &&
|
||||||
@ -311,4 +323,61 @@ public class PolicyService : IPolicyService
|
|||||||
|
|
||||||
await SetPolicyConfiguration(policy);
|
await SetPolicyConfiguration(policy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task EnablePolicy_vNext(Policy policy, Organization org, IOrganizationService organizationService, Guid? savingUserId)
|
||||||
|
{
|
||||||
|
var currentPolicy = await _policyRepository.GetByIdAsync(policy.Id);
|
||||||
|
if (!currentPolicy?.Enabled ?? true)
|
||||||
|
{
|
||||||
|
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(policy.OrganizationId);
|
||||||
|
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers);
|
||||||
|
var removableOrgUsers = orgUsers.Where(ou =>
|
||||||
|
ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked &&
|
||||||
|
ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin &&
|
||||||
|
ou.UserId != savingUserId);
|
||||||
|
switch (policy.Type)
|
||||||
|
{
|
||||||
|
case PolicyType.TwoFactorAuthentication:
|
||||||
|
// Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled
|
||||||
|
foreach (var orgUser in removableOrgUsers.OrderBy(ou => ou.HasMasterPassword))
|
||||||
|
{
|
||||||
|
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == orgUser.Id).twoFactorIsEnabled;
|
||||||
|
if (!userTwoFactorEnabled)
|
||||||
|
{
|
||||||
|
if (!orgUser.HasMasterPassword)
|
||||||
|
{
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await organizationService.DeleteUserAsync(policy.OrganizationId, orgUser.Id,
|
||||||
|
savingUserId);
|
||||||
|
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
|
||||||
|
org.DisplayName(), orgUser.Email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PolicyType.SingleOrg:
|
||||||
|
var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(
|
||||||
|
removableOrgUsers.Select(ou => ou.UserId.Value));
|
||||||
|
foreach (var orgUser in removableOrgUsers)
|
||||||
|
{
|
||||||
|
if (userOrgs.Any(ou => ou.UserId == orgUser.UserId
|
||||||
|
&& ou.OrganizationId != org.Id
|
||||||
|
&& ou.Status != OrganizationUserStatusType.Invited))
|
||||||
|
{
|
||||||
|
await organizationService.DeleteUserAsync(policy.OrganizationId, orgUser.Id,
|
||||||
|
savingUserId);
|
||||||
|
await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
|
||||||
|
org.DisplayName(), orgUser.Email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await SetPolicyConfiguration(policy);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,8 @@
|
|||||||
using Bit.Core.Auth.UserFeatures.Registration;
|
using Bit.Core.Auth.UserFeatures.Registration;
|
||||||
using Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
using Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
||||||
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
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;
|
||||||
using Bit.Core.Auth.UserFeatures.UserKey.Implementations;
|
using Bit.Core.Auth.UserFeatures.UserKey.Implementations;
|
||||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword;
|
using Bit.Core.Auth.UserFeatures.UserMasterPassword;
|
||||||
@ -24,6 +26,7 @@ public static class UserServiceCollectionExtensions
|
|||||||
services.AddUserRegistrationCommands();
|
services.AddUserRegistrationCommands();
|
||||||
services.AddWebAuthnLoginCommands();
|
services.AddWebAuthnLoginCommands();
|
||||||
services.AddTdeOffboardingPasswordCommands();
|
services.AddTdeOffboardingPasswordCommands();
|
||||||
|
services.AddTwoFactorQueries();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddUserKeyCommands(this IServiceCollection services, IGlobalSettings globalSettings)
|
public static void AddUserKeyCommands(this IServiceCollection services, IGlobalSettings globalSettings)
|
||||||
@ -54,4 +57,9 @@ public static class UserServiceCollectionExtensions
|
|||||||
services.AddScoped<IGetWebAuthnLoginCredentialAssertionOptionsCommand, GetWebAuthnLoginCredentialAssertionOptionsCommand>();
|
services.AddScoped<IGetWebAuthnLoginCredentialAssertionOptionsCommand, GetWebAuthnLoginCredentialAssertionOptionsCommand>();
|
||||||
services.AddScoped<IAssertWebAuthnLoginCredentialCommand, AssertWebAuthnLoginCredentialCommand>();
|
services.AddScoped<IAssertWebAuthnLoginCredentialCommand, AssertWebAuthnLoginCredentialCommand>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AddTwoFactorQueries(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<ITwoFactorIsEnabledQuery, TwoFactorIsEnabledQuery>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -135,6 +135,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh";
|
public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh";
|
||||||
public const string GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor";
|
public const string GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor";
|
||||||
public const string DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2";
|
public const string DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2";
|
||||||
|
public const string MembersTwoFAQueryOptimization = "ac-1698-members-two-fa-query-optimization";
|
||||||
|
|
||||||
public static List<string> GetAllKeys()
|
public static List<string> GetAllKeys()
|
||||||
{
|
{
|
||||||
|
62
src/Core/Models/Data/UserWithCalculatedPremium.cs
Normal file
62
src/Core/Models/Data/UserWithCalculatedPremium.cs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a user with an additional property indicating if the user has premium access.
|
||||||
|
/// </summary>
|
||||||
|
public class UserWithCalculatedPremium : User
|
||||||
|
{
|
||||||
|
public UserWithCalculatedPremium() { }
|
||||||
|
|
||||||
|
public UserWithCalculatedPremium(User user)
|
||||||
|
{
|
||||||
|
Id = user.Id;
|
||||||
|
Name = user.Name;
|
||||||
|
Email = user.Email;
|
||||||
|
EmailVerified = user.EmailVerified;
|
||||||
|
MasterPassword = user.MasterPassword;
|
||||||
|
MasterPasswordHint = user.MasterPasswordHint;
|
||||||
|
Culture = user.Culture;
|
||||||
|
SecurityStamp = user.SecurityStamp;
|
||||||
|
TwoFactorProviders = user.TwoFactorProviders;
|
||||||
|
TwoFactorRecoveryCode = user.TwoFactorRecoveryCode;
|
||||||
|
EquivalentDomains = user.EquivalentDomains;
|
||||||
|
ExcludedGlobalEquivalentDomains = user.ExcludedGlobalEquivalentDomains;
|
||||||
|
AccountRevisionDate = user.AccountRevisionDate;
|
||||||
|
Key = user.Key;
|
||||||
|
PublicKey = user.PublicKey;
|
||||||
|
PrivateKey = user.PrivateKey;
|
||||||
|
Premium = user.Premium;
|
||||||
|
PremiumExpirationDate = user.PremiumExpirationDate;
|
||||||
|
RenewalReminderDate = user.RenewalReminderDate;
|
||||||
|
Storage = user.Storage;
|
||||||
|
MaxStorageGb = user.MaxStorageGb;
|
||||||
|
Gateway = user.Gateway;
|
||||||
|
GatewayCustomerId = user.GatewayCustomerId;
|
||||||
|
GatewaySubscriptionId = user.GatewaySubscriptionId;
|
||||||
|
ReferenceData = user.ReferenceData;
|
||||||
|
LicenseKey = user.LicenseKey;
|
||||||
|
ApiKey = user.ApiKey;
|
||||||
|
Kdf = user.Kdf;
|
||||||
|
KdfIterations = user.KdfIterations;
|
||||||
|
KdfMemory = user.KdfMemory;
|
||||||
|
KdfParallelism = user.KdfParallelism;
|
||||||
|
CreationDate = user.CreationDate;
|
||||||
|
RevisionDate = user.RevisionDate;
|
||||||
|
ForcePasswordReset = user.ForcePasswordReset;
|
||||||
|
UsesKeyConnector = user.UsesKeyConnector;
|
||||||
|
FailedLoginCount = user.FailedLoginCount;
|
||||||
|
LastFailedLoginDate = user.LastFailedLoginDate;
|
||||||
|
AvatarColor = user.AvatarColor;
|
||||||
|
LastPasswordChangeDate = user.LastPasswordChangeDate;
|
||||||
|
LastKdfChangeDate = user.LastKdfChangeDate;
|
||||||
|
LastKeyRotationDate = user.LastKeyRotationDate;
|
||||||
|
LastEmailChangeDate = user.LastEmailChangeDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates if the user has premium access, either individually or through an organization.
|
||||||
|
/// </summary>
|
||||||
|
public bool HasPremiumAccess { get; set; }
|
||||||
|
}
|
@ -20,12 +20,16 @@ public interface IUserRepository : IRepository<User, Guid>
|
|||||||
Task UpdateRenewalReminderDateAsync(Guid id, DateTime renewalReminderDate);
|
Task UpdateRenewalReminderDateAsync(Guid id, DateTime renewalReminderDate);
|
||||||
Task<IEnumerable<User>> GetManyAsync(IEnumerable<Guid> ids);
|
Task<IEnumerable<User>> GetManyAsync(IEnumerable<Guid> ids);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Retrieves the data for the requested user IDs and includes an additional property indicating
|
||||||
|
/// whether the user has premium access directly or through an organization.
|
||||||
|
/// </summary>
|
||||||
|
Task<IEnumerable<UserWithCalculatedPremium>> GetManyWithCalculatedPremiumAsync(IEnumerable<Guid> ids);
|
||||||
|
/// <summary>
|
||||||
/// Sets a new user key and updates all encrypted data.
|
/// Sets a new user key and updates all encrypted data.
|
||||||
/// <para>Warning: Any user key encrypted data not included will be lost.</para>
|
/// <para>Warning: Any user key encrypted data not included will be lost.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user">The user to update</param>
|
/// <param name="user">The user to update</param>
|
||||||
/// <param name="updateDataActions">Registered database calls to update re-encrypted data.</param>
|
/// <param name="updateDataActions">Registered database calls to update re-encrypted data.</param>
|
||||||
[Obsolete("Intended for future improvements to key rotation. Do not use.")]
|
|
||||||
Task UpdateUserKeyAndEncryptedDataAsync(User user,
|
Task UpdateUserKeyAndEncryptedDataAsync(User user,
|
||||||
IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions);
|
IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions);
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,7 @@ public interface IUserService
|
|||||||
Task<bool> CheckPasswordAsync(User user, string password);
|
Task<bool> CheckPasswordAsync(User user, string password);
|
||||||
Task<bool> CanAccessPremium(ITwoFactorProvidersUser user);
|
Task<bool> CanAccessPremium(ITwoFactorProvidersUser user);
|
||||||
Task<bool> HasPremiumFromOrganization(ITwoFactorProvidersUser user);
|
Task<bool> HasPremiumFromOrganization(ITwoFactorProvidersUser user);
|
||||||
|
[Obsolete("Use ITwoFactorIsEnabledQuery instead.")]
|
||||||
Task<bool> TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user);
|
Task<bool> TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user);
|
||||||
Task<bool> TwoFactorProviderIsEnabledAsync(TwoFactorProviderType provider, ITwoFactorProvidersUser user);
|
Task<bool> TwoFactorProviderIsEnabledAsync(TwoFactorProviderType provider, ITwoFactorProvidersUser user);
|
||||||
Task<string> GenerateSignInTokenAsync(User user, string purpose);
|
Task<string> GenerateSignInTokenAsync(User user, string purpose);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Data;
|
using System.Data;
|
||||||
|
using System.Text.Json;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Auth.UserFeatures.UserKey;
|
using Bit.Core.Auth.UserFeatures.UserKey;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
@ -255,6 +256,20 @@ public class UserRepository : Repository<User, Guid>, IUserRepository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<UserWithCalculatedPremium>> GetManyWithCalculatedPremiumAsync(IEnumerable<Guid> ids)
|
||||||
|
{
|
||||||
|
using (var connection = new SqlConnection(ReadOnlyConnectionString))
|
||||||
|
{
|
||||||
|
var results = await connection.QueryAsync<UserWithCalculatedPremium>(
|
||||||
|
$"[{Schema}].[{Table}_ReadByIdsWithCalculatedPremium]",
|
||||||
|
new { Ids = JsonSerializer.Serialize(ids) },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
UnprotectData(results);
|
||||||
|
return results.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ProtectDataAndSaveAsync(User user, Func<Task> saveTask)
|
private async Task ProtectDataAndSaveAsync(User user, Func<Task> saveTask)
|
||||||
{
|
{
|
||||||
if (user == null)
|
if (user == null)
|
||||||
|
@ -204,6 +204,24 @@ public class UserRepository : Repository<Core.Entities.User, User, Guid>, IUserR
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<DataModel.UserWithCalculatedPremium>> GetManyWithCalculatedPremiumAsync(IEnumerable<Guid> ids)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var users = dbContext.Users.Where(x => ids.Contains(x.Id));
|
||||||
|
return await users.Select(e => new DataModel.UserWithCalculatedPremium(e)
|
||||||
|
{
|
||||||
|
HasPremiumAccess = e.Premium || dbContext.OrganizationUsers
|
||||||
|
.Any(ou => ou.UserId == e.Id &&
|
||||||
|
dbContext.Organizations
|
||||||
|
.Any(o => o.Id == ou.OrganizationId &&
|
||||||
|
o.UsersGetPremium == true &&
|
||||||
|
o.Enabled == true))
|
||||||
|
}).ToListAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override async Task DeleteAsync(Core.Entities.User user)
|
public override async Task DeleteAsync(Core.Entities.User user)
|
||||||
{
|
{
|
||||||
using (var scope = ServiceScopeFactory.CreateScope())
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[User_ReadByIdsWithCalculatedPremium]
|
||||||
|
@Ids NVARCHAR(MAX)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
-- Declare a table variable to hold the parsed JSON data
|
||||||
|
DECLARE @ParsedIds TABLE (Id UNIQUEIDENTIFIER);
|
||||||
|
|
||||||
|
-- Parse the JSON input into the table variable
|
||||||
|
INSERT INTO @ParsedIds (Id)
|
||||||
|
SELECT value
|
||||||
|
FROM OPENJSON(@Ids);
|
||||||
|
|
||||||
|
-- Check if the input table is empty
|
||||||
|
IF (SELECT COUNT(1) FROM @ParsedIds) < 1
|
||||||
|
BEGIN
|
||||||
|
RETURN(-1);
|
||||||
|
END
|
||||||
|
|
||||||
|
-- Main query to fetch user details and calculate premium access
|
||||||
|
SELECT
|
||||||
|
U.*,
|
||||||
|
CASE
|
||||||
|
WHEN U.[Premium] = 1
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM [dbo].[OrganizationUser] OU
|
||||||
|
JOIN [dbo].[Organization] O ON OU.[OrganizationId] = O.[Id]
|
||||||
|
WHERE OU.[UserId] = U.[Id]
|
||||||
|
AND O.[UsersGetPremium] = 1
|
||||||
|
AND O.[Enabled] = 1
|
||||||
|
)
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS HasPremiumAccess
|
||||||
|
FROM
|
||||||
|
[dbo].[UserView] U
|
||||||
|
WHERE
|
||||||
|
U.[Id] IN (SELECT [Id] FROM @ParsedIds);
|
||||||
|
END;
|
@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Entities;
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Auth.Models;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -356,7 +357,7 @@ public class SyncControllerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
await userService.ReceivedWithAnyArgs(1)
|
await userService.ReceivedWithAnyArgs(1)
|
||||||
.TwoFactorIsEnabledAsync(default);
|
.TwoFactorIsEnabledAsync(default(ITwoFactorProvidersUser));
|
||||||
await userService.ReceivedWithAnyArgs(1)
|
await userService.ReceivedWithAnyArgs(1)
|
||||||
.HasPremiumFromOrganization(default);
|
.HasPremiumFromOrganization(default);
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,11 @@ using Bit.Core.AdminConsole.Repositories;
|
|||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
|
using Bit.Core.Auth.Models;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
@ -1630,6 +1632,68 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService);
|
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ConfirmUser_vNext_TwoFactorPolicy_NotEnabled_Throws(Organization org, OrganizationUser confirmingUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, UserWithCalculatedPremium user,
|
||||||
|
OrganizationUser orgUserAnotherOrg,
|
||||||
|
[OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy,
|
||||||
|
string key, SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization).Returns(true);
|
||||||
|
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||||
|
var userService = Substitute.For<IUserService>();
|
||||||
|
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
||||||
|
|
||||||
|
org.PlanType = PlanType.EnterpriseAnnually;
|
||||||
|
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||||
|
orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;
|
||||||
|
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||||
|
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
||||||
|
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||||
|
userRepository.GetManyWithCalculatedPremiumAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||||
|
twoFactorPolicy.OrganizationId = org.Id;
|
||||||
|
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });
|
||||||
|
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
|
||||||
|
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) });
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService));
|
||||||
|
Assert.Contains("User does not have two-step login enabled.", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ConfirmUser_vNext_TwoFactorPolicy_Enabled_Success(Organization org, OrganizationUser confirmingUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, UserWithCalculatedPremium user,
|
||||||
|
[OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy,
|
||||||
|
string key, SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization).Returns(true);
|
||||||
|
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||||
|
var userService = Substitute.For<IUserService>();
|
||||||
|
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
||||||
|
|
||||||
|
org.PlanType = PlanType.EnterpriseAnnually;
|
||||||
|
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||||
|
orgUser.UserId = user.Id;
|
||||||
|
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||||
|
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||||
|
userRepository.GetManyWithCalculatedPremiumAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||||
|
twoFactorPolicy.OrganizationId = org.Id;
|
||||||
|
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });
|
||||||
|
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
|
||||||
|
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, true) });
|
||||||
|
|
||||||
|
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task ConfirmUsers_Success(Organization org,
|
public async Task ConfirmUsers_Success(Organization org,
|
||||||
OrganizationUser confirmingUser,
|
OrganizationUser confirmingUser,
|
||||||
@ -1675,6 +1739,56 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", result[2].Item2);
|
Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", result[2].Item2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ConfirmUsers_vNext_Success(Organization org,
|
||||||
|
OrganizationUser confirmingUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser1,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser2,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser3,
|
||||||
|
OrganizationUser anotherOrgUser, UserWithCalculatedPremium user1, UserWithCalculatedPremium user2, UserWithCalculatedPremium user3,
|
||||||
|
[OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy,
|
||||||
|
[OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy,
|
||||||
|
string key, SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||||
|
var userService = Substitute.For<IUserService>();
|
||||||
|
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
||||||
|
|
||||||
|
org.PlanType = PlanType.EnterpriseAnnually;
|
||||||
|
orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||||
|
orgUser1.UserId = user1.Id;
|
||||||
|
orgUser2.UserId = user2.Id;
|
||||||
|
orgUser3.UserId = user3.Id;
|
||||||
|
anotherOrgUser.UserId = user3.Id;
|
||||||
|
var orgUsers = new[] { orgUser1, orgUser2, orgUser3 };
|
||||||
|
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(orgUsers);
|
||||||
|
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||||
|
userRepository.GetManyWithCalculatedPremiumAsync(default).ReturnsForAnyArgs(new[] { user1, user2, user3 });
|
||||||
|
twoFactorPolicy.OrganizationId = org.Id;
|
||||||
|
policyService.GetPoliciesApplicableToUserAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });
|
||||||
|
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user1.Id) && ids.Contains(user2.Id) && ids.Contains(user3.Id)))
|
||||||
|
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>()
|
||||||
|
{
|
||||||
|
(user1.Id, true),
|
||||||
|
(user2.Id, false),
|
||||||
|
(user3.Id, true)
|
||||||
|
});
|
||||||
|
singleOrgPolicy.OrganizationId = org.Id;
|
||||||
|
policyService.GetPoliciesApplicableToUserAsync(user3.Id, PolicyType.SingleOrg)
|
||||||
|
.Returns(new[] { singleOrgPolicy });
|
||||||
|
organizationUserRepository.GetManyByManyUsersAsync(default)
|
||||||
|
.ReturnsForAnyArgs(new[] { orgUser1, orgUser2, orgUser3, anotherOrgUser });
|
||||||
|
|
||||||
|
var keys = orgUsers.ToDictionary(ou => ou.Id, _ => key);
|
||||||
|
var result = await sutProvider.Sut.ConfirmUsersAsync_vNext(confirmingUser.OrganizationId, keys, confirmingUser.Id);
|
||||||
|
Assert.Contains("", result[0].Item2);
|
||||||
|
Assert.Contains("User does not have two-step login enabled.", result[1].Item2);
|
||||||
|
Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", result[2].Item2);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task UpdateOrganizationKeysAsync_WithoutManageResetPassword_Throws(Guid orgId, string publicKey,
|
public async Task UpdateOrganizationKeysAsync_WithoutManageResetPassword_Throws(Guid orgId, string publicKey,
|
||||||
string privateKey, SutProvider<OrganizationService> sutProvider)
|
string privateKey, SutProvider<OrganizationService> sutProvider)
|
||||||
@ -1842,15 +1956,17 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
await applicationCacheService.DidNotReceiveWithAnyArgs().DeleteOrganizationAbilityAsync(default);
|
await applicationCacheService.DidNotReceiveWithAnyArgs().DeleteOrganizationAbilityAsync(default);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RestoreRevokeUser_Setup(Organization organization, OrganizationUser owner, OrganizationUser organizationUser, SutProvider<OrganizationService> sutProvider)
|
private void RestoreRevokeUser_Setup(Organization organization, OrganizationUser restoringUser,
|
||||||
|
OrganizationUser organizationUser, SutProvider<OrganizationService> sutProvider,
|
||||||
|
OrganizationUserType restoringUserType = OrganizationUserType.Owner)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationUser.OrganizationId).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationUser.OrganizationId).Returns(organization);
|
||||||
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
|
||||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);
|
||||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
organizationUserRepository.GetManyByOrganizationAsync(organizationUser.OrganizationId, OrganizationUserType.Owner)
|
organizationUserRepository.GetManyByOrganizationAsync(organizationUser.OrganizationId, restoringUserType)
|
||||||
.Returns(new[] { owner });
|
.Returns(new[] { restoringUser });
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
@ -1915,6 +2031,320 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, eventSystemUser);
|
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, eventSystemUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_RestoreThemselves_Fails(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
organizationUser.UserId = owner.Id;
|
||||||
|
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
var userService = Substitute.For<IUserService>();
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var eventService = sutProvider.GetDependency<IEventService>();
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService));
|
||||||
|
|
||||||
|
Assert.Contains("you cannot restore yourself", exception.Message.ToLowerInvariant());
|
||||||
|
|
||||||
|
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await eventService.DidNotReceiveWithAnyArgs()
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(OrganizationUserType.Custom)]
|
||||||
|
public async Task RestoreUser_AdminRestoreOwner_Fails(OrganizationUserType restoringUserType, Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser restoringUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.Owner)] OrganizationUser organizationUser, SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
restoringUser.Type = restoringUserType;
|
||||||
|
RestoreRevokeUser_Setup(organization, restoringUser, organizationUser, sutProvider, OrganizationUserType.Admin);
|
||||||
|
var userService = Substitute.For<IUserService>();
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var eventService = sutProvider.GetDependency<IEventService>();
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.RestoreUserAsync(organizationUser, restoringUser.Id, userService));
|
||||||
|
|
||||||
|
Assert.Contains("only owners can restore other owners", exception.Message.ToLowerInvariant());
|
||||||
|
|
||||||
|
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await eventService.DidNotReceiveWithAnyArgs()
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Invited)]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Accepted)]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Confirmed)]
|
||||||
|
public async Task RestoreUser_WithStatusOtherThanRevoked_Fails(OrganizationUserStatusType userStatus, Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser] OrganizationUser organizationUser, SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
organizationUser.Status = userStatus;
|
||||||
|
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
var userService = Substitute.For<IUserService>();
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var eventService = sutProvider.GetDependency<IEventService>();
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService));
|
||||||
|
|
||||||
|
Assert.Contains("already active", exception.Message.ToLowerInvariant());
|
||||||
|
|
||||||
|
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await eventService.DidNotReceiveWithAnyArgs()
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_WithSingleOrgPolicyEnabled_Fails(
|
||||||
|
Organization organization,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser,
|
||||||
|
SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||||
|
secondOrganizationUser.UserId = organizationUser.UserId;
|
||||||
|
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
var userService = Substitute.For<IUserService>();
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var eventService = sutProvider.GetDependency<IEventService>();
|
||||||
|
|
||||||
|
organizationUserRepository.GetManyByUserAsync(organizationUser.UserId.Value).Returns(new[] { organizationUser, secondOrganizationUser });
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
|
||||||
|
.Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg } });
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService));
|
||||||
|
|
||||||
|
Assert.Contains("you cannot restore this user until " +
|
||||||
|
"they leave or remove all other organizations.", exception.Message.ToLowerInvariant());
|
||||||
|
|
||||||
|
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await eventService.DidNotReceiveWithAnyArgs()
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_WithOtherOrganizationSingleOrgPolicyEnabled_Fails(
|
||||||
|
Organization organization,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||||
|
SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||||
|
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
var userService = Substitute.For<IUserService>();
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var eventService = sutProvider.GetDependency<IEventService>();
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.AnyPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService));
|
||||||
|
|
||||||
|
Assert.Contains("you cannot restore this user because they are a member of " +
|
||||||
|
"another organization which forbids it", exception.Message.ToLowerInvariant());
|
||||||
|
|
||||||
|
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await eventService.DidNotReceiveWithAnyArgs()
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_With2FAPolicyEnabled_WithoutUser2FAConfigured_Fails(
|
||||||
|
Organization organization,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||||
|
SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
organizationUser.Email = null;
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||||
|
.Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } });
|
||||||
|
|
||||||
|
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
var userService = Substitute.For<IUserService>();
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var eventService = sutProvider.GetDependency<IEventService>();
|
||||||
|
|
||||||
|
|
||||||
|
userService.TwoFactorIsEnabledAsync(Arg.Any<ITwoFactorProvidersUser>()).Returns(false);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService));
|
||||||
|
|
||||||
|
Assert.Contains("you cannot restore this user until they enable " +
|
||||||
|
"two-step login on their user account.", exception.Message.ToLowerInvariant());
|
||||||
|
|
||||||
|
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await eventService.DidNotReceiveWithAnyArgs()
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_With2FAPolicyEnabled_WithUser2FAConfigured_Success(
|
||||||
|
Organization organization,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||||
|
SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||||
|
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
var userService = Substitute.For<IUserService>();
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var eventService = sutProvider.GetDependency<IEventService>();
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||||
|
.Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } });
|
||||||
|
userService.TwoFactorIsEnabledAsync(Arg.Any<ITwoFactorProvidersUser>()).Returns(true);
|
||||||
|
|
||||||
|
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService);
|
||||||
|
|
||||||
|
await organizationUserRepository.Received().RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed);
|
||||||
|
await eventService.Received()
|
||||||
|
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_vNext_WithSingleOrgPolicyEnabled_Fails(
|
||||||
|
Organization organization,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser,
|
||||||
|
SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization).Returns(true);
|
||||||
|
|
||||||
|
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||||
|
secondOrganizationUser.UserId = organizationUser.UserId;
|
||||||
|
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
var userService = Substitute.For<IUserService>();
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var eventService = sutProvider.GetDependency<IEventService>();
|
||||||
|
|
||||||
|
organizationUserRepository.GetManyByUserAsync(organizationUser.UserId.Value).Returns(new[] { organizationUser, secondOrganizationUser });
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
|
||||||
|
.Returns(new[]
|
||||||
|
{
|
||||||
|
new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg, OrganizationUserStatus = OrganizationUserStatusType.Revoked }
|
||||||
|
});
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService));
|
||||||
|
|
||||||
|
Assert.Contains("you cannot restore this user until " +
|
||||||
|
"they leave or remove all other organizations.", exception.Message.ToLowerInvariant());
|
||||||
|
|
||||||
|
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await eventService.DidNotReceiveWithAnyArgs()
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_vNext_WithOtherOrganizationSingleOrgPolicyEnabled_Fails(
|
||||||
|
Organization organization,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser,
|
||||||
|
SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization).Returns(true);
|
||||||
|
|
||||||
|
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||||
|
secondOrganizationUser.UserId = organizationUser.UserId;
|
||||||
|
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
var userService = Substitute.For<IUserService>();
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var eventService = sutProvider.GetDependency<IEventService>();
|
||||||
|
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
||||||
|
|
||||||
|
twoFactorIsEnabledQuery
|
||||||
|
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
|
||||||
|
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.AnyPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService));
|
||||||
|
|
||||||
|
Assert.Contains("you cannot restore this user because they are a member of " +
|
||||||
|
"another organization which forbids it", exception.Message.ToLowerInvariant());
|
||||||
|
|
||||||
|
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await eventService.DidNotReceiveWithAnyArgs()
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithoutUser2FAConfigured_Fails(
|
||||||
|
Organization organization,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||||
|
SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization).Returns(true);
|
||||||
|
|
||||||
|
organizationUser.Email = null;
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||||
|
.Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } });
|
||||||
|
|
||||||
|
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
var userService = Substitute.For<IUserService>();
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var eventService = sutProvider.GetDependency<IEventService>();
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService));
|
||||||
|
|
||||||
|
Assert.Contains("you cannot restore this user until they enable " +
|
||||||
|
"two-step login on their user account.", exception.Message.ToLowerInvariant());
|
||||||
|
|
||||||
|
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await eventService.DidNotReceiveWithAnyArgs()
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithUser2FAConfigured_Success(
|
||||||
|
Organization organization,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||||
|
SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization).Returns(true);
|
||||||
|
|
||||||
|
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||||
|
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
var userService = Substitute.For<IUserService>();
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var eventService = sutProvider.GetDependency<IEventService>();
|
||||||
|
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||||
|
.Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } });
|
||||||
|
|
||||||
|
twoFactorIsEnabledQuery
|
||||||
|
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
|
||||||
|
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) });
|
||||||
|
|
||||||
|
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService);
|
||||||
|
|
||||||
|
await organizationUserRepository.Received().RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed);
|
||||||
|
await eventService.Received()
|
||||||
|
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HasConfirmedOwnersExcept_WithConfirmedOwner_ReturnsTrue(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, SutProvider<OrganizationService> sutProvider)
|
public async Task HasConfirmedOwnersExcept_WithConfirmedOwner_ReturnsTrue(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE [dbo].[User_ReadByIdsWithCalculatedPremium]
|
||||||
|
@Ids NVARCHAR(MAX)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
-- Declare a table variable to hold the parsed JSON data
|
||||||
|
DECLARE @ParsedIds TABLE (Id UNIQUEIDENTIFIER);
|
||||||
|
|
||||||
|
-- Parse the JSON input into the table variable
|
||||||
|
INSERT INTO @ParsedIds (Id)
|
||||||
|
SELECT value
|
||||||
|
FROM OPENJSON(@Ids);
|
||||||
|
|
||||||
|
-- Check if the input table is empty
|
||||||
|
IF (SELECT COUNT(1) FROM @ParsedIds) < 1
|
||||||
|
BEGIN
|
||||||
|
RETURN(-1);
|
||||||
|
END
|
||||||
|
|
||||||
|
-- Main query to fetch user details and calculate premium access
|
||||||
|
SELECT
|
||||||
|
U.*,
|
||||||
|
CASE
|
||||||
|
WHEN U.[Premium] = 1
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM [dbo].[OrganizationUser] OU
|
||||||
|
JOIN [dbo].[Organization] O ON OU.[OrganizationId] = O.[Id]
|
||||||
|
WHERE OU.[UserId] = U.[Id]
|
||||||
|
AND O.[UsersGetPremium] = 1
|
||||||
|
AND O.[Enabled] = 1
|
||||||
|
)
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS HasPremiumAccess
|
||||||
|
FROM
|
||||||
|
[dbo].[UserView] U
|
||||||
|
WHERE
|
||||||
|
U.[Id] IN (SELECT [Id] FROM @ParsedIds);
|
||||||
|
END;
|
Loading…
x
Reference in New Issue
Block a user