1
0
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:
Oscar Hinton 2021-05-25 19:23:47 +02:00 committed by GitHub
parent 93fd1c9c9a
commit d4cf6d929a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 669 additions and 102 deletions

View File

@ -138,7 +138,7 @@ namespace Bit.Api.Controllers
} }
[HttpPost("reinvite")] [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); var orgGuidId = new Guid(orgId);
if (!_currentContext.ManageUsers(orgGuidId)) if (!_currentContext.ManageUsers(orgGuidId))
@ -147,7 +147,9 @@ namespace Bit.Api.Controllers
} }
var userId = _userService.GetProperUserId(User); 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")] [HttpPost("{id}/reinvite")]
@ -189,6 +191,38 @@ namespace Bit.Api.Controllers
_userService); _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}")] [HttpPut("{id}")]
[HttpPost("{id}")] [HttpPost("{id}")]
public async Task Put(string orgId, string id, [FromBody]OrganizationUserUpdateRequestModel model) public async Task Put(string orgId, string id, [FromBody]OrganizationUserUpdateRequestModel model)
@ -287,7 +321,7 @@ namespace Bit.Api.Controllers
[HttpDelete("")] [HttpDelete("")]
[HttpPost("delete")] [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); var orgGuidId = new Guid(orgId);
if (!_currentContext.ManageUsers(orgGuidId)) if (!_currentContext.ManageUsers(orgGuidId))
@ -296,7 +330,9 @@ namespace Bit.Api.Controllers
} }
var userId = _userService.GetProperUserId(User); 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)));
} }
} }
} }

View File

@ -60,6 +60,25 @@ namespace Bit.Core.Models.Api
public string Key { get; set; } 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 public class OrganizationUserUpdateRequestModel
{ {
[Required] [Required]

View File

@ -108,4 +108,29 @@ namespace Bit.Core.Models.Api
public string ResetPasswordKey { get; set; } public string ResetPasswordKey { get; set; }
public string EncryptedPrivateKey { 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; }
}
} }

View 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; }
}
}

View File

@ -128,5 +128,10 @@ namespace Bit.Core.Repositories.EntityFramework
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task<IEnumerable<User>> GetManyAsync(IEnumerable<Guid> ids)
{
throw new NotImplementedException();
}
} }
} }

View File

@ -36,5 +36,6 @@ namespace Bit.Core.Repositories
Task<ICollection<OrganizationUser>> GetManyAsync(IEnumerable<Guid> Ids); Task<ICollection<OrganizationUser>> GetManyAsync(IEnumerable<Guid> Ids);
Task DeleteManyAsync(IEnumerable<Guid> userIds); Task DeleteManyAsync(IEnumerable<Guid> userIds);
Task<OrganizationUser> GetByOrganizationEmailAsync(Guid organizationId, string email); Task<OrganizationUser> GetByOrganizationEmailAsync(Guid organizationId, string email);
Task<IEnumerable<OrganizationUserPublicKey>> GetManyPublicKeysByOrganizationUserAsync(Guid organizationId, IEnumerable<Guid> Ids);
} }
} }

View File

@ -17,5 +17,6 @@ namespace Bit.Core.Repositories
Task<DateTime> GetAccountRevisionDateAsync(Guid id); Task<DateTime> GetAccountRevisionDateAsync(Guid id);
Task UpdateStorageAsync(Guid id); Task UpdateStorageAsync(Guid id);
Task UpdateRenewalReminderDateAsync(Guid id, DateTime renewalReminderDate); Task UpdateRenewalReminderDateAsync(Guid id, DateTime renewalReminderDate);
Task<IEnumerable<User>> GetManyAsync(IEnumerable<Guid> ids);
} }
} }

View File

@ -161,5 +161,10 @@ namespace Bit.Core.Repositories.PostgreSql
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task<IEnumerable<User>> GetManyAsync(IEnumerable<Guid> ids)
{
throw new NotImplementedException();
}
} }
} }

View File

@ -365,5 +365,19 @@ namespace Bit.Core.Repositories.SqlServer
commandType: CommandType.StoredProcedure); 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();
}
}
} }
} }

View File

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities;
using Dapper; using Dapper;
namespace Bit.Core.Repositories.SqlServer namespace Bit.Core.Repositories.SqlServer
@ -157,5 +158,18 @@ namespace Bit.Core.Repositories.SqlServer
commandType: CommandType.StoredProcedure); 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();
}
}
} }
} }

View File

@ -34,17 +34,20 @@ namespace Bit.Core.Services
Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email, Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<SelectionReadOnly> collections); OrganizationUserType type, bool accessAll, string externalId, IEnumerable<SelectionReadOnly> collections);
Task<List<OrganizationUser>> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string externalId, OrganizationUserInvite orgUserInvite); 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 ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId);
Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token, Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token,
IUserService userService); IUserService userService);
Task<OrganizationUser> AcceptUserAsync(string orgIdentifier, User user, IUserService userService); Task<OrganizationUser> AcceptUserAsync(string orgIdentifier, User user, IUserService userService);
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
Guid confirmingUserId, IUserService userService); 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 SaveUserAsync(OrganizationUser user, Guid? savingUserId, IEnumerable<SelectionReadOnly> collections);
Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId); Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);
Task DeleteUserAsync(Guid organizationId, Guid userId); 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 UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds, Guid? loggedInUserId);
Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid organizationUserId, string resetPasswordKey, Guid? callingUserId); Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid organizationUserId, string resetPasswordKey, Guid? callingUserId);
Task<OrganizationLicense> GenerateLicenseAsync(Guid organizationId, Guid installationId); Task<OrganizationLicense> GenerateLicenseAsync(Guid organizationId, Guid installationId);

View File

@ -1205,23 +1205,26 @@ namespace Bit.Core.Services
return orgUsers; 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) IEnumerable<Guid> organizationUsersId)
{ {
var orgUsers = await _organizationUserRepository.GetManyAsync(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); 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); await SendInviteAsync(orgUser, org);
result.Add(Tuple.Create(orgUser, ""));
} }
return result;
} }
public async Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId) 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, public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
Guid confirmingUserId, IUserService userService) Guid confirmingUserId, IUserService userService)
{ {
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); var result = await ConfirmUsersAsync(organizationId, new Dictionary<Guid, string>() {{organizationUserId, key}},
if (orgUser == null || orgUser.Status != OrganizationUserStatusType.Accepted || confirmingUserId, userService);
orgUser.OrganizationId != organizationId)
if (!result.Any())
{ {
throw new BadRequestException("User not valid."); throw new BadRequestException("User not valid.");
} }
var org = await GetOrgById(organizationId); var (orgUser, error) = result[0];
if (org.PlanType == PlanType.Free && if (error != "")
(orgUser.Type == OrganizationUserType.Admin || orgUser.Type == OrganizationUserType.Owner))
{ {
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync( throw new BadRequestException(error);
orgUser.UserId.Value); }
if (adminCount > 0) 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); await _organizationUserRepository.ReplaceManyAsync(succeededUsers);
var policies = await _policyRepository.GetManyByOrganizationIdAsync(organizationId);
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); 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."); 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); var usingSingleOrgPolicy = policies.Any(p => p.Type == PolicyType.SingleOrg && p.Enabled);
if (usingSingleOrgPolicy) if (usingSingleOrgPolicy)
{ {
var userOrgs = await _organizationUserRepository.GetManyByUserAsync(user.Id);
if (userOrgs.Any(ou => ou.OrganizationId != organizationId && ou.Status != OrganizationUserStatusType.Invited)) if (userOrgs.Any(ou => ou.OrganizationId != organizationId && ou.Status != OrganizationUserStatusType.Invited))
{ {
throw new BadRequestException("User is a member of another organization."); 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, 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) Guid? deletingUserId)
{ {
var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId); var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId);
@ -1533,34 +1593,52 @@ namespace Bit.Core.Services
{ {
throw new BadRequestException("Users invalid."); 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)) if (!await HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId))
{ {
throw new BadRequestException("Organization must have at least one confirmed owner."); 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) foreach (var orgUser in filteredUsers)
{ {
// TODO: We should replace this call with `DeleteManyAsync`. try
await _organizationUserRepository.DeleteAsync(orgUser);
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed);
if (orgUser.UserId.HasValue)
{ {
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) private async Task<bool> HasConfirmedOwnersExceptAsync(Guid organizationId, IEnumerable<Guid> organizationUsersId)

View File

@ -138,6 +138,7 @@
<Build Include="dbo\Stored Procedures\User_BumpAccountRevisionDateByOrganizationUserIds.sql" /> <Build Include="dbo\Stored Procedures\User_BumpAccountRevisionDateByOrganizationUserIds.sql" />
<Build Include="dbo\Stored Procedures\Cipher_Delete.sql" /> <Build Include="dbo\Stored Procedures\Cipher_Delete.sql" />
<Build Include="dbo\Stored Procedures\User_ReadPublicKeyById.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_Move.sql" />
<Build Include="dbo\Stored Procedures\Cipher_UpdatePartial.sql" /> <Build Include="dbo\Stored Procedures\Cipher_UpdatePartial.sql" />
<Build Include="dbo\Stored Procedures\Device_ClearPushTokenById.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_ReadCanEditByIdUserId.sql" />
<Build Include="dbo\Stored Procedures\Cipher_Create.sql" /> <Build Include="dbo\Stored Procedures\Cipher_Create.sql" />
<Build Include="dbo\Stored Procedures\Cipher_DeleteById.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_ReadById.sql" />
<Build Include="dbo\Stored Procedures\Cipher_Update.sql" /> <Build Include="dbo\Stored Procedures\Cipher_Update.sql" />
<Build Include="dbo\Stored Procedures\Device_Create.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\User_ReadByEmail.sql" />
<Build Include="dbo\Stored Procedures\Collection_UpdateWithGroups.sql" /> <Build Include="dbo\Stored Procedures\Collection_UpdateWithGroups.sql" />
<Build Include="dbo\Stored Procedures\User_ReadById.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\CollectionUser_Delete.sql" />
<Build Include="dbo\Stored Procedures\User_Update.sql" /> <Build Include="dbo\Stored Procedures\User_Update.sql" />
<Build Include="dbo\Stored Procedures\CollectionUser_ReadByCollectionId.sql" /> <Build Include="dbo\Stored Procedures\CollectionUser_ReadByCollectionId.sql" />

View 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

View File

@ -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

View 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);
}
}
}

View 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);
}
}
}

View File

@ -15,8 +15,10 @@ using Bit.Core.Enums;
using Bit.Core.Test.AutoFixture.Attributes; using Bit.Core.Test.AutoFixture.Attributes;
using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Test.AutoFixture.OrganizationFixtures;
using System.Text.Json; using System.Text.Json;
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
using Organization = Bit.Core.Models.Table.Organization; 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 namespace Bit.Core.Test.Services
{ {
@ -28,6 +30,7 @@ namespace Bit.Core.Test.Services
Organization org, List<OrganizationUserUserDetails> existingUsers, List<ImportedOrganizationUser> newUsers) Organization org, List<OrganizationUserUserDetails> existingUsers, List<ImportedOrganizationUser> newUsers)
{ {
org.UseDirectory = true; org.UseDirectory = true;
org.Seats = 10;
newUsers.Add(new ImportedOrganizationUser newUsers.Add(new ImportedOrganizationUser
{ {
Email = existingUsers.First().Email, Email = existingUsers.First().Email,
@ -335,15 +338,18 @@ namespace Bit.Core.Test.Services
} }
[Theory, CustomAutoData(typeof(SutProviderCustomization))] [Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task SaveUser_Passes(OrganizationUser oldUserData, OrganizationUser newUserData, public async Task SaveUser_Passes(
IEnumerable<SelectionReadOnly> collections, OrganizationUser savingUser, SutProvider<OrganizationService> sutProvider) OrganizationUser oldUserData,
OrganizationUser newUserData,
IEnumerable<SelectionReadOnly> collections,
[OrganizationUser(type: OrganizationUserType.Owner)]OrganizationUser savingUser,
SutProvider<OrganizationService> sutProvider)
{ {
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
newUserData.Id = oldUserData.Id; newUserData.Id = oldUserData.Id;
newUserData.UserId = oldUserData.UserId; newUserData.UserId = oldUserData.UserId;
newUserData.OrganizationId = savingUser.OrganizationId = oldUserData.OrganizationId; newUserData.OrganizationId = savingUser.OrganizationId = oldUserData.OrganizationId;
savingUser.Type = OrganizationUserType.Owner;
organizationUserRepository.GetByIdAsync(oldUserData.Id).Returns(oldUserData); organizationUserRepository.GetByIdAsync(oldUserData.Id).Returns(oldUserData);
organizationUserRepository.GetManyByOrganizationAsync(savingUser.OrganizationId, OrganizationUserType.Owner) organizationUserRepository.GetManyByOrganizationAsync(savingUser.OrganizationId, OrganizationUserType.Owner)
.Returns(new List<OrganizationUser> { savingUser }); .Returns(new List<OrganizationUser> { savingUser });
@ -378,13 +384,14 @@ namespace Bit.Core.Test.Services
} }
[Theory, CustomAutoData(typeof(SutProviderCustomization))] [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) SutProvider<OrganizationService> sutProvider)
{ {
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
organizationUser.OrganizationId = deletingUser.OrganizationId; organizationUser.OrganizationId = deletingUser.OrganizationId;
organizationUser.Type = OrganizationUserType.Owner;
organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser);
organizationUserRepository.GetManyByUserAsync(deletingUser.UserId.Value).Returns(new[] { deletingUser }); organizationUserRepository.GetManyByUserAsync(deletingUser.UserId.Value).Returns(new[] { deletingUser });
@ -394,13 +401,14 @@ namespace Bit.Core.Test.Services
} }
[Theory, CustomAutoData(typeof(SutProviderCustomization))] [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) SutProvider<OrganizationService> sutProvider)
{ {
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
organizationUser.OrganizationId = deletingUser.OrganizationId; organizationUser.OrganizationId = deletingUser.OrganizationId;
organizationUser.Type = OrganizationUserType.Owner;
organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser);
organizationUserRepository.GetManyByOrganizationAsync(deletingUser.OrganizationId, OrganizationUserType.Owner) organizationUserRepository.GetManyByOrganizationAsync(deletingUser.OrganizationId, OrganizationUserType.Owner)
.Returns(new[] { organizationUser }); .Returns(new[] { organizationUser });
@ -411,13 +419,13 @@ namespace Bit.Core.Test.Services
} }
[Theory, CustomAutoData(typeof(SutProviderCustomization))] [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) SutProvider<OrganizationService> sutProvider)
{ {
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
deletingUser.Type = OrganizationUserType.Owner;
deletingUser.Status = OrganizationUserStatusType.Confirmed;
organizationUser.OrganizationId = deletingUser.OrganizationId; organizationUser.OrganizationId = deletingUser.OrganizationId;
organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser);
organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser);
@ -435,7 +443,7 @@ namespace Bit.Core.Test.Services
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var organizationUsers = new[] { organizationUser }; var organizationUsers = new[] { organizationUser };
var organizationUserIds = organizationUsers.Select(u => u.Id); var organizationUserIds = organizationUsers.Select(u => u.Id);
organizationUserRepository.GetManyAsync(organizationUserIds).Returns(organizationUsers); organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers);
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.DeleteUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId)); () => sutProvider.Sut.DeleteUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId));
@ -443,46 +451,50 @@ namespace Bit.Core.Test.Services
} }
[Theory, CustomAutoData(typeof(SutProviderCustomization))] [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 organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var organizationUsers = new[] { deletingUser }; var organizationUsers = new[] { deletingUser };
var organizationUserIds = organizationUsers.Select(u => u.Id); 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>( var result = await sutProvider.Sut.DeleteUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId);
() => sutProvider.Sut.DeleteUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId)); Assert.Contains("You cannot remove yourself.", result[0].Item2);
Assert.Contains("You cannot remove yourself.", exception.Message);
} }
[Theory, CustomAutoData(typeof(SutProviderCustomization))] [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) SutProvider<OrganizationService> sutProvider)
{ {
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); 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 organizationUsers = new[] { orgUser };
var organizationUserIds = organizationUsers.Select(u => u.Id); 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); organizationUserRepository.GetManyByOrganizationAsync(orgUser.OrganizationId, OrganizationUserType.Owner).Returns(organizationUsers);
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
@ -491,18 +503,17 @@ namespace Bit.Core.Test.Services
} }
[Theory, CustomAutoData(typeof(SutProviderCustomization))] [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) SutProvider<OrganizationService> sutProvider)
{ {
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
deletingUser.Type = OrganizationUserType.Owner;
deletingUser.Status = OrganizationUserStatusType.Confirmed;
orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId;
orgUser1.Type = OrganizationUserType.Owner;
var organizationUsers = new[] { orgUser1, orgUser2 }; var organizationUsers = new[] { orgUser1, orgUser2 };
var organizationUserIds = organizationUsers.Select(u => u.Id); var organizationUserIds = organizationUsers.Select(u => u.Id);
organizationUserRepository.GetManyAsync(organizationUserIds).Returns(organizationUsers); organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers);
organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser);
organizationUserRepository.GetManyByUserAsync(deletingUser.UserId.Value).Returns(new[] { deletingUser }); organizationUserRepository.GetManyByUserAsync(deletingUser.UserId.Value).Returns(new[] { deletingUser });
organizationUserRepository.GetManyByOrganizationAsync(deletingUser.OrganizationId, OrganizationUserType.Owner) organizationUserRepository.GetManyByOrganizationAsync(deletingUser.OrganizationId, OrganizationUserType.Owner)
@ -510,5 +521,175 @@ namespace Bit.Core.Test.Services
await sutProvider.Sut.DeleteUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); 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);
}
} }
} }

View 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