mirror of
https://github.com/bitwarden/server.git
synced 2024-12-22 16:57:36 +01:00
Bulk Confirm (#1345)
* Add support for bulk confirm * Add missing sproc to migration * Change ConfirmUserAsync to internally use ConfirmUsersAsync * Refactor to be a bit more readable * Change BulkReinvite and BulkRemove to return a list of errors/success * Refactor * Fix removing owner preventing removing non owners * Add another unit test * Use fixtures for OrganizationUser and Policies * Fix spelling
This commit is contained in:
parent
93fd1c9c9a
commit
d4cf6d929a
@ -138,7 +138,7 @@ namespace Bit.Api.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("reinvite")]
|
||||
public async Task BulkReinvite(string orgId, [FromBody]OrganizationUserBulkRequestModel model)
|
||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkReinvite(string orgId, [FromBody]OrganizationUserBulkRequestModel model)
|
||||
{
|
||||
var orgGuidId = new Guid(orgId);
|
||||
if (!_currentContext.ManageUsers(orgGuidId))
|
||||
@ -147,7 +147,9 @@ namespace Bit.Api.Controllers
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
await _organizationService.ResendInvitesAsync(orgGuidId, userId.Value, model.Ids);
|
||||
var result = await _organizationService.ResendInvitesAsync(orgGuidId, userId.Value, model.Ids);
|
||||
return new ListResponseModel<OrganizationUserBulkResponseModel>(
|
||||
result.Select(t => new OrganizationUserBulkResponseModel(t.Item1.Id, t.Item2)));
|
||||
}
|
||||
|
||||
[HttpPost("{id}/reinvite")]
|
||||
@ -189,6 +191,38 @@ namespace Bit.Api.Controllers
|
||||
_userService);
|
||||
}
|
||||
|
||||
[HttpPost("confirm")]
|
||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkConfirm(string orgId,
|
||||
[FromBody]OrganizationUserBulkConfirmRequestModel model)
|
||||
{
|
||||
var orgGuidId = new Guid(orgId);
|
||||
if (!_currentContext.ManageUsers(orgGuidId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
var results = await _organizationService.ConfirmUsersAsync(orgGuidId, model.ToDictionary(), userId.Value,
|
||||
_userService);
|
||||
|
||||
return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r =>
|
||||
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));
|
||||
}
|
||||
|
||||
[HttpPost("public-keys")]
|
||||
public async Task<ListResponseModel<OrganizationUserPublicKeyResponseModel>> UserPublicKeys(string orgId, [FromBody]OrganizationUserBulkRequestModel model)
|
||||
{
|
||||
var orgGuidId = new Guid(orgId);
|
||||
if (!_currentContext.ManageUsers(orgGuidId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var result = await _organizationUserRepository.GetManyPublicKeysByOrganizationUserAsync(orgGuidId, model.Ids);
|
||||
var responses = result.Select(r => new OrganizationUserPublicKeyResponseModel(r.Id, r.PublicKey)).ToList();
|
||||
return new ListResponseModel<OrganizationUserPublicKeyResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPost("{id}")]
|
||||
public async Task Put(string orgId, string id, [FromBody]OrganizationUserUpdateRequestModel model)
|
||||
@ -287,7 +321,7 @@ namespace Bit.Api.Controllers
|
||||
|
||||
[HttpDelete("")]
|
||||
[HttpPost("delete")]
|
||||
public async Task BulkDelete(string orgId, [FromBody]OrganizationUserBulkRequestModel model)
|
||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDelete(string orgId, [FromBody]OrganizationUserBulkRequestModel model)
|
||||
{
|
||||
var orgGuidId = new Guid(orgId);
|
||||
if (!_currentContext.ManageUsers(orgGuidId))
|
||||
@ -296,7 +330,9 @@ namespace Bit.Api.Controllers
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
await _organizationService.DeleteUsersAsync(orgGuidId, model.Ids, userId.Value);
|
||||
var result = await _organizationService.DeleteUsersAsync(orgGuidId, model.Ids, userId.Value);
|
||||
return new ListResponseModel<OrganizationUserBulkResponseModel>(result.Select(r =>
|
||||
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +60,25 @@ namespace Bit.Core.Models.Api
|
||||
public string Key { get; set; }
|
||||
}
|
||||
|
||||
public class OrganizationUserBulkConfirmRequestModelEntry
|
||||
{
|
||||
[Required]
|
||||
public Guid Id { get; set; }
|
||||
[Required]
|
||||
public string Key { get; set; }
|
||||
}
|
||||
|
||||
public class OrganizationUserBulkConfirmRequestModel
|
||||
{
|
||||
[Required]
|
||||
public IEnumerable<OrganizationUserBulkConfirmRequestModelEntry> Keys { get; set; }
|
||||
|
||||
public Dictionary<Guid, string> ToDictionary()
|
||||
{
|
||||
return Keys.ToDictionary(e => e.Id, e => e.Key);
|
||||
}
|
||||
}
|
||||
|
||||
public class OrganizationUserUpdateRequestModel
|
||||
{
|
||||
[Required]
|
||||
|
@ -108,4 +108,29 @@ namespace Bit.Core.Models.Api
|
||||
public string ResetPasswordKey { get; set; }
|
||||
public string EncryptedPrivateKey { get; set; }
|
||||
}
|
||||
|
||||
public class OrganizationUserPublicKeyResponseModel : ResponseModel
|
||||
{
|
||||
public OrganizationUserPublicKeyResponseModel(Guid id, string key,
|
||||
string obj = "organizationUserPublicKeyResponseModel") : base(obj)
|
||||
{
|
||||
Id = id;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public string Key { get; set; }
|
||||
}
|
||||
|
||||
public class OrganizationUserBulkResponseModel : ResponseModel
|
||||
{
|
||||
public OrganizationUserBulkResponseModel(Guid id, string error,
|
||||
string obj = "OrganizationBulkConfirmResponseModel") : base(obj)
|
||||
{
|
||||
Id = id;
|
||||
Error = error;
|
||||
}
|
||||
public Guid Id { get; set; }
|
||||
public string Error { get; set; }
|
||||
}
|
||||
}
|
||||
|
10
src/Core/Models/Data/OrganizationUserPublicKey.cs
Normal file
10
src/Core/Models/Data/OrganizationUserPublicKey.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace Bit.Core.Models.Data
|
||||
{
|
||||
public class OrganizationUserPublicKey
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string PublicKey { get; set; }
|
||||
}
|
||||
}
|
@ -128,5 +128,10 @@ namespace Bit.Core.Repositories.EntityFramework
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IEnumerable<User>> GetManyAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,5 +36,6 @@ namespace Bit.Core.Repositories
|
||||
Task<ICollection<OrganizationUser>> GetManyAsync(IEnumerable<Guid> Ids);
|
||||
Task DeleteManyAsync(IEnumerable<Guid> userIds);
|
||||
Task<OrganizationUser> GetByOrganizationEmailAsync(Guid organizationId, string email);
|
||||
Task<IEnumerable<OrganizationUserPublicKey>> GetManyPublicKeysByOrganizationUserAsync(Guid organizationId, IEnumerable<Guid> Ids);
|
||||
}
|
||||
}
|
||||
|
@ -17,5 +17,6 @@ namespace Bit.Core.Repositories
|
||||
Task<DateTime> GetAccountRevisionDateAsync(Guid id);
|
||||
Task UpdateStorageAsync(Guid id);
|
||||
Task UpdateRenewalReminderDateAsync(Guid id, DateTime renewalReminderDate);
|
||||
Task<IEnumerable<User>> GetManyAsync(IEnumerable<Guid> ids);
|
||||
}
|
||||
}
|
||||
|
@ -161,5 +161,10 @@ namespace Bit.Core.Repositories.PostgreSql
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IEnumerable<User>> GetManyAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -365,5 +365,19 @@ namespace Bit.Core.Repositories.SqlServer
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<OrganizationUserPublicKey>> GetManyPublicKeysByOrganizationUserAsync(
|
||||
Guid organizationId, IEnumerable<Guid> Ids)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<OrganizationUserPublicKey>(
|
||||
"[dbo].[User_ReadPublicKeysByOrganizationUserIds]",
|
||||
new { OrganizationId = organizationId, OrganizationUserIds = Ids.ToGuidIdArrayTVP() },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Dapper;
|
||||
|
||||
namespace Bit.Core.Repositories.SqlServer
|
||||
@ -157,5 +158,18 @@ namespace Bit.Core.Repositories.SqlServer
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<User>> GetManyAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
using (var connection = new SqlConnection(ReadOnlyConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<User>(
|
||||
$"[{Schema}].[{Table}_ReadByIds]",
|
||||
new { Ids = ids.ToGuidIdArrayTVP() },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,17 +34,20 @@ namespace Bit.Core.Services
|
||||
Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
|
||||
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<SelectionReadOnly> collections);
|
||||
Task<List<OrganizationUser>> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string externalId, OrganizationUserInvite orgUserInvite);
|
||||
Task ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId);
|
||||
Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId);
|
||||
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId);
|
||||
Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token,
|
||||
IUserService userService);
|
||||
Task<OrganizationUser> AcceptUserAsync(string orgIdentifier, User user, IUserService userService);
|
||||
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
||||
Guid confirmingUserId, IUserService userService);
|
||||
Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,
|
||||
Guid confirmingUserId, IUserService userService);
|
||||
Task SaveUserAsync(OrganizationUser user, Guid? savingUserId, IEnumerable<SelectionReadOnly> collections);
|
||||
Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);
|
||||
Task DeleteUserAsync(Guid organizationId, Guid userId);
|
||||
Task DeleteUsersAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, Guid? deleteingUserId);
|
||||
Task<List<Tuple<OrganizationUser, string>>> DeleteUsersAsync(Guid organizationId,
|
||||
IEnumerable<Guid> organizationUserIds, Guid? deletingUserId);
|
||||
Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds, Guid? loggedInUserId);
|
||||
Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid organizationUserId, string resetPasswordKey, Guid? callingUserId);
|
||||
Task<OrganizationLicense> GenerateLicenseAsync(Guid organizationId, Guid installationId);
|
||||
|
@ -1205,23 +1205,26 @@ namespace Bit.Core.Services
|
||||
return orgUsers;
|
||||
}
|
||||
|
||||
public async Task ResendInvitesAsync(Guid organizationId, Guid? invitingUserId,
|
||||
public async Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId,
|
||||
IEnumerable<Guid> organizationUsersId)
|
||||
{
|
||||
var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId);
|
||||
var filteredUsers = orgUsers
|
||||
.Where(u => u.Status == OrganizationUserStatusType.Invited && u.OrganizationId == organizationId);
|
||||
|
||||
if (!filteredUsers.Any())
|
||||
{
|
||||
throw new BadRequestException("Users invalid.");
|
||||
}
|
||||
|
||||
var org = await GetOrgById(organizationId);
|
||||
foreach (var orgUser in filteredUsers)
|
||||
|
||||
var result = new List<Tuple<OrganizationUser, string>>();
|
||||
foreach (var orgUser in orgUsers)
|
||||
{
|
||||
if (orgUser.Status != OrganizationUserStatusType.Invited || orgUser.OrganizationId != organizationId)
|
||||
{
|
||||
result.Add(Tuple.Create(orgUser, "User invalid."));
|
||||
continue;
|
||||
}
|
||||
|
||||
await SendInviteAsync(orgUser, org);
|
||||
result.Add(Tuple.Create(orgUser, ""));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId)
|
||||
@ -1384,29 +1387,97 @@ namespace Bit.Core.Services
|
||||
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
||||
Guid confirmingUserId, IUserService userService)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
||||
if (orgUser == null || orgUser.Status != OrganizationUserStatusType.Accepted ||
|
||||
orgUser.OrganizationId != organizationId)
|
||||
var result = await ConfirmUsersAsync(organizationId, new Dictionary<Guid, string>() {{organizationUserId, key}},
|
||||
confirmingUserId, userService);
|
||||
|
||||
if (!result.Any())
|
||||
{
|
||||
throw new BadRequestException("User not valid.");
|
||||
}
|
||||
|
||||
var org = await GetOrgById(organizationId);
|
||||
if (org.PlanType == PlanType.Free &&
|
||||
(orgUser.Type == OrganizationUserType.Admin || orgUser.Type == OrganizationUserType.Owner))
|
||||
var (orgUser, error) = result[0];
|
||||
if (error != "")
|
||||
{
|
||||
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(
|
||||
orgUser.UserId.Value);
|
||||
if (adminCount > 0)
|
||||
throw new BadRequestException(error);
|
||||
}
|
||||
return orgUser;
|
||||
}
|
||||
|
||||
public async Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,
|
||||
Guid confirmingUserId, IUserService userService)
|
||||
{
|
||||
var organizationUsers = await _organizationUserRepository.GetManyAsync(keys.Keys);
|
||||
var validOrganizationUsers = organizationUsers
|
||||
.Where(u => u.Status == OrganizationUserStatusType.Accepted && u.OrganizationId == organizationId && u.UserId != null)
|
||||
.ToList();
|
||||
|
||||
if (!validOrganizationUsers.Any())
|
||||
{
|
||||
return new List<Tuple<OrganizationUser, string>>();
|
||||
}
|
||||
|
||||
var validOrganizationUserIds = validOrganizationUsers.Select(u => u.UserId.Value).ToList();
|
||||
|
||||
var organization = await GetOrgById(organizationId);
|
||||
var policies = await _policyRepository.GetManyByOrganizationIdAsync(organizationId);
|
||||
var usersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validOrganizationUserIds);
|
||||
var users = await _userRepository.GetManyAsync(validOrganizationUserIds);
|
||||
|
||||
var keyedFilteredUsers = validOrganizationUsers.ToDictionary(u => u.UserId.Value, u => u);
|
||||
var keyedOrganizationUsers = usersOrgs.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))
|
||||
{
|
||||
throw new BadRequestException("User can only be an admin of one free organization.");
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
await CheckPolicies(policies, organizationId, user, orgUsers, userService);
|
||||
orgUser.Status = OrganizationUserStatusType.Confirmed;
|
||||
orgUser.Key = keys[orgUser.Id];
|
||||
orgUser.Email = null;
|
||||
|
||||
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
||||
await _mailService.SendOrganizationConfirmedEmailAsync(organization.Name, user.Email);
|
||||
await DeleteAndPushUserRegistrationAsync(organizationId, user.Id);
|
||||
succeededUsers.Add(orgUser);
|
||||
result.Add(Tuple.Create(orgUser, ""));
|
||||
}
|
||||
catch (BadRequestException e)
|
||||
{
|
||||
result.Add(Tuple.Create(orgUser, e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
var user = await _userRepository.GetByIdAsync(orgUser.UserId.Value);
|
||||
var policies = await _policyRepository.GetManyByOrganizationIdAsync(organizationId);
|
||||
await _organizationUserRepository.ReplaceManyAsync(succeededUsers);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task CheckPolicies(ICollection<Policy> policies, Guid organizationId, User user,
|
||||
ICollection<OrganizationUser> userOrgs, IUserService userService)
|
||||
{
|
||||
var usingTwoFactorPolicy = policies.Any(p => p.Type == PolicyType.TwoFactorAuthentication && p.Enabled);
|
||||
if (usingTwoFactorPolicy && !(await userService.TwoFactorIsEnabledAsync(user)))
|
||||
if (usingTwoFactorPolicy && !await userService.TwoFactorIsEnabledAsync(user))
|
||||
{
|
||||
throw new BadRequestException("User does not have two-step login enabled.");
|
||||
}
|
||||
@ -1414,23 +1485,11 @@ namespace Bit.Core.Services
|
||||
var usingSingleOrgPolicy = policies.Any(p => p.Type == PolicyType.SingleOrg && p.Enabled);
|
||||
if (usingSingleOrgPolicy)
|
||||
{
|
||||
var userOrgs = await _organizationUserRepository.GetManyByUserAsync(user.Id);
|
||||
if (userOrgs.Any(ou => ou.OrganizationId != organizationId && ou.Status != OrganizationUserStatusType.Invited))
|
||||
{
|
||||
throw new BadRequestException("User is a member of another organization.");
|
||||
}
|
||||
}
|
||||
|
||||
orgUser.Status = OrganizationUserStatusType.Confirmed;
|
||||
orgUser.Key = key;
|
||||
orgUser.Email = null;
|
||||
await _organizationUserRepository.ReplaceAsync(orgUser);
|
||||
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
||||
await _mailService.SendOrganizationConfirmedEmailAsync(org.Name, user.Email);
|
||||
|
||||
await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value);
|
||||
|
||||
return orgUser;
|
||||
}
|
||||
|
||||
public async Task SaveUserAsync(OrganizationUser user, Guid? savingUserId,
|
||||
@ -1522,7 +1581,8 @@ namespace Bit.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteUsersAsync(Guid organizationId, IEnumerable<Guid> organizationUsersId,
|
||||
public async Task<List<Tuple<OrganizationUser, string>>> DeleteUsersAsync(Guid organizationId,
|
||||
IEnumerable<Guid> organizationUsersId,
|
||||
Guid? deletingUserId)
|
||||
{
|
||||
var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId);
|
||||
@ -1533,34 +1593,52 @@ namespace Bit.Core.Services
|
||||
{
|
||||
throw new BadRequestException("Users invalid.");
|
||||
}
|
||||
|
||||
if (deletingUserId.HasValue && filteredUsers.Exists(u => u.UserId == deletingUserId.Value))
|
||||
{
|
||||
throw new BadRequestException("You cannot remove yourself.");
|
||||
}
|
||||
|
||||
var owners = filteredUsers.Where(u => u.Type == OrganizationUserType.Owner);
|
||||
if (owners.Any() && deletingUserId.HasValue && !await UserIsOwnerAsync(organizationId, deletingUserId.Value))
|
||||
{
|
||||
throw new BadRequestException("Only owners can delete other owners.");
|
||||
}
|
||||
|
||||
if (!await HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId))
|
||||
{
|
||||
throw new BadRequestException("Organization must have at least one confirmed owner.");
|
||||
}
|
||||
|
||||
var deletingUserIsOwner = false;
|
||||
if (deletingUserId.HasValue)
|
||||
{
|
||||
deletingUserIsOwner = await UserIsOwnerAsync(organizationId, deletingUserId.Value);
|
||||
}
|
||||
|
||||
var result = new List<Tuple<OrganizationUser, string>>();
|
||||
var deletedUserIds = new List<Guid>();
|
||||
foreach (var orgUser in filteredUsers)
|
||||
{
|
||||
// TODO: We should replace this call with `DeleteManyAsync`.
|
||||
await _organizationUserRepository.DeleteAsync(orgUser);
|
||||
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed);
|
||||
|
||||
if (orgUser.UserId.HasValue)
|
||||
try
|
||||
{
|
||||
await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value);
|
||||
if (deletingUserId.HasValue && orgUser.UserId == deletingUserId)
|
||||
{
|
||||
throw new BadRequestException("You cannot remove yourself.");
|
||||
}
|
||||
|
||||
if (orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue && !deletingUserIsOwner)
|
||||
{
|
||||
throw new BadRequestException("Only owners can delete other owners.");
|
||||
}
|
||||
|
||||
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed);
|
||||
|
||||
if (orgUser.UserId.HasValue)
|
||||
{
|
||||
await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value);
|
||||
}
|
||||
result.Add(Tuple.Create(orgUser, ""));
|
||||
deletedUserIds.Add(orgUser.Id);
|
||||
}
|
||||
catch (BadRequestException e)
|
||||
{
|
||||
result.Add(Tuple.Create(orgUser, e.Message));
|
||||
}
|
||||
|
||||
await _organizationUserRepository.DeleteManyAsync(deletedUserIds);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<bool> HasConfirmedOwnersExceptAsync(Guid organizationId, IEnumerable<Guid> organizationUsersId)
|
||||
|
@ -138,6 +138,7 @@
|
||||
<Build Include="dbo\Stored Procedures\User_BumpAccountRevisionDateByOrganizationUserIds.sql" />
|
||||
<Build Include="dbo\Stored Procedures\Cipher_Delete.sql" />
|
||||
<Build Include="dbo\Stored Procedures\User_ReadPublicKeyById.sql" />
|
||||
<Build Include="dbo\Stored Procedures\User_ReadPublicKeysByOrganizationUserIds.sql" />
|
||||
<Build Include="dbo\Stored Procedures\Cipher_Move.sql" />
|
||||
<Build Include="dbo\Stored Procedures\Cipher_UpdatePartial.sql" />
|
||||
<Build Include="dbo\Stored Procedures\Device_ClearPushTokenById.sql" />
|
||||
@ -160,6 +161,7 @@
|
||||
<Build Include="dbo\Stored Procedures\Cipher_ReadCanEditByIdUserId.sql" />
|
||||
<Build Include="dbo\Stored Procedures\Cipher_Create.sql" />
|
||||
<Build Include="dbo\Stored Procedures\Cipher_DeleteById.sql" />
|
||||
<Build Include="dbo\Stored Procedures\Cipher_DeleteDeleted.sql" />
|
||||
<Build Include="dbo\Stored Procedures\Cipher_ReadById.sql" />
|
||||
<Build Include="dbo\Stored Procedures\Cipher_Update.sql" />
|
||||
<Build Include="dbo\Stored Procedures\Device_Create.sql" />
|
||||
@ -174,6 +176,7 @@
|
||||
<Build Include="dbo\Stored Procedures\User_ReadByEmail.sql" />
|
||||
<Build Include="dbo\Stored Procedures\Collection_UpdateWithGroups.sql" />
|
||||
<Build Include="dbo\Stored Procedures\User_ReadById.sql" />
|
||||
<Build Include="dbo\Stored Procedures\User_ReadByIds.sql" />
|
||||
<Build Include="dbo\Stored Procedures\CollectionUser_Delete.sql" />
|
||||
<Build Include="dbo\Stored Procedures\User_Update.sql" />
|
||||
<Build Include="dbo\Stored Procedures\CollectionUser_ReadByCollectionId.sql" />
|
||||
|
18
src/Sql/dbo/Stored Procedures/User_ReadByIds.sql
Normal file
18
src/Sql/dbo/Stored Procedures/User_ReadByIds.sql
Normal file
@ -0,0 +1,18 @@
|
||||
CREATE PROCEDURE [dbo].[User_ReadByIds]
|
||||
@Ids AS [dbo].[GuidIdArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
IF (SELECT COUNT(1) FROM @Ids) < 1
|
||||
BEGIN
|
||||
RETURN(-1)
|
||||
END
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[UserView]
|
||||
WHERE
|
||||
[Id] IN (SELECT [Id] FROM @Ids)
|
||||
END
|
@ -0,0 +1,19 @@
|
||||
CREATE PROCEDURE [dbo].[User_ReadPublicKeysByOrganizationUserIds]
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@OrganizationUserIds [dbo].[GuidIdArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
OU.[Id],
|
||||
U.[PublicKey]
|
||||
FROM
|
||||
@OrganizationUserIds OUIDs
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationUser] OU ON OUIDs.Id = OU.Id AND OU.[Status] = 1 -- Accepted
|
||||
INNER JOIN
|
||||
[dbo].[User] U ON OU.UserId = U.Id
|
||||
WHERE
|
||||
OU.OrganizationId = @OrganizationId
|
||||
END
|
45
test/Core.Test/AutoFixture/OrganizationUserFixtures.cs
Normal file
45
test/Core.Test/AutoFixture/OrganizationUserFixtures.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System.Reflection;
|
||||
using AutoFixture;
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Test.AutoFixture.OrganizationUserFixtures
|
||||
{
|
||||
internal class OrganizationUser : ICustomization
|
||||
{
|
||||
public OrganizationUserStatusType Status { get; set; }
|
||||
public OrganizationUserType Type { get; set; }
|
||||
|
||||
public OrganizationUser(OrganizationUserStatusType status, OrganizationUserType type)
|
||||
{
|
||||
Status = status;
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
fixture.Customize<Core.Models.Table.OrganizationUser>(composer => composer
|
||||
.With(o => o.Type, Type)
|
||||
.With(o => o.Status, Status));
|
||||
}
|
||||
}
|
||||
|
||||
public class OrganizationUserAttribute : CustomizeAttribute
|
||||
{
|
||||
private readonly OrganizationUserStatusType _status;
|
||||
private readonly OrganizationUserType _type;
|
||||
|
||||
public OrganizationUserAttribute(
|
||||
OrganizationUserStatusType status = OrganizationUserStatusType.Confirmed,
|
||||
OrganizationUserType type = OrganizationUserType.User)
|
||||
{
|
||||
_status = status;
|
||||
_type = type;
|
||||
}
|
||||
|
||||
public override ICustomization GetCustomization(ParameterInfo parameter)
|
||||
{
|
||||
return new OrganizationUser(_status, _type);
|
||||
}
|
||||
}
|
||||
}
|
39
test/Core.Test/AutoFixture/PolicyFixtures.cs
Normal file
39
test/Core.Test/AutoFixture/PolicyFixtures.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System.Reflection;
|
||||
using AutoFixture;
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Test.AutoFixture.OrganizationUserFixtures
|
||||
{
|
||||
internal class Policy : ICustomization
|
||||
{
|
||||
public PolicyType Type { get; set; }
|
||||
|
||||
public Policy(PolicyType type)
|
||||
{
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
fixture.Customize<Core.Models.Table.Policy>(composer => composer
|
||||
.With(o => o.Type, Type)
|
||||
.With(o => o.Enabled, true));
|
||||
}
|
||||
}
|
||||
|
||||
public class PolicyAttribute : CustomizeAttribute
|
||||
{
|
||||
private readonly PolicyType _type;
|
||||
|
||||
public PolicyAttribute(PolicyType type)
|
||||
{
|
||||
_type = type;
|
||||
}
|
||||
|
||||
public override ICustomization GetCustomization(ParameterInfo parameter)
|
||||
{
|
||||
return new Policy(_type);
|
||||
}
|
||||
}
|
||||
}
|
@ -15,8 +15,10 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Test.AutoFixture.Attributes;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||
using Organization = Bit.Core.Models.Table.Organization;
|
||||
using System.Linq;
|
||||
using OrganizationUser = Bit.Core.Models.Table.OrganizationUser;
|
||||
using Policy = Bit.Core.Models.Table.Policy;
|
||||
|
||||
namespace Bit.Core.Test.Services
|
||||
{
|
||||
@ -28,6 +30,7 @@ namespace Bit.Core.Test.Services
|
||||
Organization org, List<OrganizationUserUserDetails> existingUsers, List<ImportedOrganizationUser> newUsers)
|
||||
{
|
||||
org.UseDirectory = true;
|
||||
org.Seats = 10;
|
||||
newUsers.Add(new ImportedOrganizationUser
|
||||
{
|
||||
Email = existingUsers.First().Email,
|
||||
@ -335,15 +338,18 @@ namespace Bit.Core.Test.Services
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task SaveUser_Passes(OrganizationUser oldUserData, OrganizationUser newUserData,
|
||||
IEnumerable<SelectionReadOnly> collections, OrganizationUser savingUser, SutProvider<OrganizationService> sutProvider)
|
||||
public async Task SaveUser_Passes(
|
||||
OrganizationUser oldUserData,
|
||||
OrganizationUser newUserData,
|
||||
IEnumerable<SelectionReadOnly> collections,
|
||||
[OrganizationUser(type: OrganizationUserType.Owner)]OrganizationUser savingUser,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
newUserData.Id = oldUserData.Id;
|
||||
newUserData.UserId = oldUserData.UserId;
|
||||
newUserData.OrganizationId = savingUser.OrganizationId = oldUserData.OrganizationId;
|
||||
savingUser.Type = OrganizationUserType.Owner;
|
||||
organizationUserRepository.GetByIdAsync(oldUserData.Id).Returns(oldUserData);
|
||||
organizationUserRepository.GetManyByOrganizationAsync(savingUser.OrganizationId, OrganizationUserType.Owner)
|
||||
.Returns(new List<OrganizationUser> { savingUser });
|
||||
@ -378,13 +384,14 @@ namespace Bit.Core.Test.Services
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task DeleteUser_NonOwnerRemoveOwner(OrganizationUser organizationUser, OrganizationUser deletingUser,
|
||||
public async Task DeleteUser_NonOwnerRemoveOwner(
|
||||
[OrganizationUser(type: OrganizationUserType.Owner)]OrganizationUser organizationUser,
|
||||
[OrganizationUser(type: OrganizationUserType.Admin)]OrganizationUser deletingUser,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
organizationUser.OrganizationId = deletingUser.OrganizationId;
|
||||
organizationUser.Type = OrganizationUserType.Owner;
|
||||
organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser);
|
||||
organizationUserRepository.GetManyByUserAsync(deletingUser.UserId.Value).Returns(new[] { deletingUser });
|
||||
|
||||
@ -394,13 +401,14 @@ namespace Bit.Core.Test.Services
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task DeleteUser_LastOwner(OrganizationUser organizationUser, OrganizationUser deletingUser,
|
||||
public async Task DeleteUser_LastOwner(
|
||||
[OrganizationUser(type: OrganizationUserType.Owner)]OrganizationUser organizationUser,
|
||||
OrganizationUser deletingUser,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
organizationUser.OrganizationId = deletingUser.OrganizationId;
|
||||
organizationUser.Type = OrganizationUserType.Owner;
|
||||
organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser);
|
||||
organizationUserRepository.GetManyByOrganizationAsync(deletingUser.OrganizationId, OrganizationUserType.Owner)
|
||||
.Returns(new[] { organizationUser });
|
||||
@ -411,13 +419,13 @@ namespace Bit.Core.Test.Services
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task DeleteUser_Success(OrganizationUser organizationUser, OrganizationUser deletingUser,
|
||||
public async Task DeleteUser_Success(
|
||||
OrganizationUser organizationUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)]OrganizationUser deletingUser,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
deletingUser.Type = OrganizationUserType.Owner;
|
||||
deletingUser.Status = OrganizationUserStatusType.Confirmed;
|
||||
organizationUser.OrganizationId = deletingUser.OrganizationId;
|
||||
organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser);
|
||||
organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser);
|
||||
@ -435,7 +443,7 @@ namespace Bit.Core.Test.Services
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationUsers = new[] { organizationUser };
|
||||
var organizationUserIds = organizationUsers.Select(u => u.Id);
|
||||
organizationUserRepository.GetManyAsync(organizationUserIds).Returns(organizationUsers);
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.DeleteUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId));
|
||||
@ -443,46 +451,50 @@ namespace Bit.Core.Test.Services
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task DeleteUsers_RemoveYourself(OrganizationUser deletingUser, SutProvider<OrganizationService> sutProvider)
|
||||
public async Task DeleteUsers_RemoveYourself(
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)]OrganizationUser orgUser,
|
||||
OrganizationUser deletingUser,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationUsers = new[] { deletingUser };
|
||||
var organizationUserIds = organizationUsers.Select(u => u.Id);
|
||||
organizationUserRepository.GetManyAsync(organizationUserIds).Returns(organizationUsers);
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers);
|
||||
organizationUserRepository.GetManyByOrganizationAsync(default, default).ReturnsForAnyArgs(new[] {orgUser});
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.DeleteUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId));
|
||||
Assert.Contains("You cannot remove yourself.", exception.Message);
|
||||
var result = await sutProvider.Sut.DeleteUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId);
|
||||
Assert.Contains("You cannot remove yourself.", result[0].Item2);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task DeleteUsers_NonOwnerRemoveOwner(OrganizationUser deletingUser, OrganizationUser orgUser1, OrganizationUser orgUser2,
|
||||
public async Task DeleteUsers_NonOwnerRemoveOwner(
|
||||
[OrganizationUser(type: OrganizationUserType.Admin)]OrganizationUser deletingUser,
|
||||
[OrganizationUser(type: OrganizationUserType.Owner)]OrganizationUser orgUser1,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed)]OrganizationUser orgUser2,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId;
|
||||
var organizationUsers = new[] { orgUser1 };
|
||||
var organizationUserIds = organizationUsers.Select(u => u.Id);
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers);
|
||||
organizationUserRepository.GetManyByOrganizationAsync(default, default).ReturnsForAnyArgs(new[] {orgUser2});
|
||||
|
||||
var result = await sutProvider.Sut.DeleteUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId);
|
||||
Assert.Contains("Only owners can delete other owners.", result[0].Item2);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task DeleteUsers_LastOwner(
|
||||
[OrganizationUser(status: OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)]OrganizationUser orgUser,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
deletingUser.Type = OrganizationUserType.Admin;
|
||||
orgUser1.Type = OrganizationUserType.Owner;
|
||||
orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId;
|
||||
var organizationUsers = new[] { orgUser1, orgUser2 };
|
||||
var organizationUserIds = organizationUsers.Select(u => u.Id);
|
||||
organizationUserRepository.GetManyAsync(organizationUserIds).Returns(organizationUsers);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.DeleteUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId));
|
||||
Assert.Contains("Only owners can delete other owners.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task DeleteUsers_LastOwner(OrganizationUser orgUser, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
orgUser.Type = OrganizationUserType.Owner;
|
||||
orgUser.Status = OrganizationUserStatusType.Confirmed;
|
||||
var organizationUsers = new[] { orgUser };
|
||||
var organizationUserIds = organizationUsers.Select(u => u.Id);
|
||||
organizationUserRepository.GetManyAsync(organizationUserIds).Returns(organizationUsers);
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers);
|
||||
organizationUserRepository.GetManyByOrganizationAsync(orgUser.OrganizationId, OrganizationUserType.Owner).Returns(organizationUsers);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
@ -491,18 +503,17 @@ namespace Bit.Core.Test.Services
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task DeleteUsers_Success(OrganizationUser deletingUser, OrganizationUser orgUser1, OrganizationUser orgUser2,
|
||||
public async Task DeleteUsers_Success(
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)]OrganizationUser deletingUser,
|
||||
[OrganizationUser(type: OrganizationUserType.Owner)]OrganizationUser orgUser1, OrganizationUser orgUser2,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
deletingUser.Type = OrganizationUserType.Owner;
|
||||
deletingUser.Status = OrganizationUserStatusType.Confirmed;
|
||||
orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId;
|
||||
orgUser1.Type = OrganizationUserType.Owner;
|
||||
var organizationUsers = new[] { orgUser1, orgUser2 };
|
||||
var organizationUserIds = organizationUsers.Select(u => u.Id);
|
||||
organizationUserRepository.GetManyAsync(organizationUserIds).Returns(organizationUsers);
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers);
|
||||
organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser);
|
||||
organizationUserRepository.GetManyByUserAsync(deletingUser.UserId.Value).Returns(new[] { deletingUser });
|
||||
organizationUserRepository.GetManyByOrganizationAsync(deletingUser.OrganizationId, OrganizationUserType.Owner)
|
||||
@ -510,5 +521,175 @@ namespace Bit.Core.Test.Services
|
||||
|
||||
await sutProvider.Sut.DeleteUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task ConfirmUser_InvalidStatus(OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Invited)]OrganizationUser orgUser, string key,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var userService = Substitute.For<IUserService>();
|
||||
|
||||
organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService));
|
||||
Assert.Contains("User not valid.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task ConfirmUser_WrongOrganization(OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)]OrganizationUser orgUser, string key,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var userService = Substitute.For<IUserService>();
|
||||
|
||||
organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(confirmingUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService));
|
||||
Assert.Contains("User not valid.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task ConfirmUser_AlreadyAdmin(Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted, OrganizationUserType.Admin)]OrganizationUser orgUser, User user,
|
||||
string key, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var userService = Substitute.For<IUserService>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
|
||||
org.PlanType = PlanType.Free;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = user.Id;
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] {orgUser});
|
||||
organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(orgUser.UserId.Value).Returns(1);
|
||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] {user});
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService));
|
||||
Assert.Contains("User can only be an admin of one free organization.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task ConfirmUser_SingleOrgPolicy(Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)]OrganizationUser orgUser, User user,
|
||||
OrganizationUser orgUserAnotherOrg, [Policy(PolicyType.SingleOrg)]Policy singleOrgPolicy,
|
||||
string key, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
var userService = Substitute.For<IUserService>();
|
||||
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser.Status = OrganizationUserStatusType.Accepted;
|
||||
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.GetManyAsync(default).ReturnsForAnyArgs(new[] {user});
|
||||
policyRepository.GetManyByOrganizationIdAsync(org.Id).Returns(new[] {singleOrgPolicy});
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService));
|
||||
Assert.Contains("User is a member of another organization.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task ConfirmUser_TwoFactorPolicy(Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)]OrganizationUser orgUser, User user,
|
||||
OrganizationUser orgUserAnotherOrg, [Policy(PolicyType.TwoFactorAuthentication)]Policy twoFactorPolicy,
|
||||
string key, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
var userService = Substitute.For<IUserService>();
|
||||
|
||||
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.GetManyAsync(default).ReturnsForAnyArgs(new[] {user});
|
||||
policyRepository.GetManyByOrganizationIdAsync(org.Id).Returns(new[] {twoFactorPolicy});
|
||||
|
||||
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, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task ConfirmUser_Success(Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)]OrganizationUser orgUser, User user,
|
||||
[Policy(PolicyType.TwoFactorAuthentication)]Policy twoFactorPolicy,
|
||||
[Policy(PolicyType.SingleOrg)]Policy singleOrgPolicy, string key, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
var userService = Substitute.For<IUserService>();
|
||||
|
||||
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.GetManyAsync(default).ReturnsForAnyArgs(new[] {user});
|
||||
policyRepository.GetManyByOrganizationIdAsync(org.Id).Returns(new[] {twoFactorPolicy, singleOrgPolicy});
|
||||
userService.TwoFactorIsEnabledAsync(user).Returns(true);
|
||||
|
||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task ConfirmUsers_Success(Organization org,
|
||||
OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)]OrganizationUser orgUser1,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)]OrganizationUser orgUser2,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)]OrganizationUser orgUser3,
|
||||
OrganizationUser anotherOrgUser, User user1, User user2, User user3,
|
||||
[Policy(PolicyType.TwoFactorAuthentication)]Policy twoFactorPolicy,
|
||||
[Policy(PolicyType.SingleOrg)]Policy singleOrgPolicy, string key, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
var userService = Substitute.For<IUserService>();
|
||||
|
||||
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.GetManyAsync(default).ReturnsForAnyArgs(new[] {user1, user2, user3});
|
||||
policyRepository.GetManyByOrganizationIdAsync(org.Id).Returns(new[] {twoFactorPolicy, singleOrgPolicy});
|
||||
userService.TwoFactorIsEnabledAsync(user1).Returns(true);
|
||||
userService.TwoFactorIsEnabledAsync(user2).Returns(false);
|
||||
userService.TwoFactorIsEnabledAsync(user3).Returns(true);
|
||||
organizationUserRepository.GetManyByManyUsersAsync(default)
|
||||
.ReturnsForAnyArgs(new[] {orgUser1, orgUser2, orgUser3, anotherOrgUser});
|
||||
|
||||
var keys = orgUsers.ToDictionary(ou => ou.Id, _ => key);
|
||||
var result = await sutProvider.Sut.ConfirmUsersAsync(confirmingUser.OrganizationId, keys, confirmingUser.Id, userService);
|
||||
Assert.Contains("", result[0].Item2);
|
||||
Assert.Contains("User does not have two-step login enabled.", result[1].Item2);
|
||||
Assert.Contains("User is a member of another organization.", result[2].Item2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
51
util/Migrator/DbScripts/2021-05-18_00_BulkConfirm.sql
Normal file
51
util/Migrator/DbScripts/2021-05-18_00_BulkConfirm.sql
Normal file
@ -0,0 +1,51 @@
|
||||
IF OBJECT_ID('[dbo].[User_ReadByIds]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[User_ReadByIds]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[User_ReadByIds]
|
||||
@Ids AS [dbo].[GuidIdArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
IF (SELECT COUNT(1) FROM @Ids) < 1
|
||||
BEGIN
|
||||
RETURN(-1)
|
||||
END
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[UserView]
|
||||
WHERE
|
||||
[Id] IN (SELECT [Id] FROM @Ids)
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID('[dbo].[User_ReadPublicKeysByOrganizationUserIds]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[User_ReadPublicKeysByOrganizationUserIds]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[User_ReadPublicKeysByOrganizationUserIds]
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@OrganizationUserIds [dbo].[GuidIdArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
OU.[Id],
|
||||
U.[PublicKey]
|
||||
FROM
|
||||
@OrganizationUserIds OUIDs
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationUser] OU ON OUIDs.Id = OU.Id AND OU.[Status] = 1 -- Accepted
|
||||
INNER JOIN
|
||||
[dbo].[User] U ON OU.UserId = U.Id
|
||||
WHERE
|
||||
OU.OrganizationId = @OrganizationId
|
||||
END
|
Loading…
Reference in New Issue
Block a user