From 670b548b2279081287c69449c1cfdfea2a8f9f20 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 15 May 2017 14:41:20 -0400 Subject: [PATCH] updated format of import data --- .../Controllers/OrganizationsController.cs | 4 +- src/Api/Startup.cs | 2 +- .../ImportOrganizationUsersRequestModel.cs | 21 ++- src/Core/Repositories/IGroupRepository.cs | 1 + .../Repositories/SqlServer/GroupRepository.cs | 11 ++ src/Core/Services/IOrganizationService.cs | 4 +- .../Implementations/OrganizationService.cs | 161 +++++++++--------- src/Sql/Sql.sqlproj | 1 + .../GroupUser_UpdateUsers.sql | 46 +++++ 9 files changed, 156 insertions(+), 95 deletions(-) create mode 100644 src/Sql/dbo/Stored Procedures/GroupUser_UpdateUsers.sql diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index 6435976d1d..c63f09796c 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -239,8 +239,8 @@ namespace Bit.Api.Controllers } var userId = _userService.GetProperUserId(User); - await _organizationService.ImportAsync(orgIdGuid, userId.Value, model.Groups.Select(g => g.ToGroup(orgIdGuid)), - model.Users.Select(u => u.ToKvp())); + await _organizationService.ImportAsync(orgIdGuid, userId.Value, model.Groups.Select(g => g.ToGroupTuple(orgIdGuid)), + model.NewUsers.Select(u => u.Email), model.RemoveUsers.Select(u => u.Email)); } } } diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index a0389dc595..3b9b2ebb01 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -95,7 +95,7 @@ namespace Bit.Api // Services services.AddBaseServices(); - services.AddDefaultServices(); + services.AddNoopServices(); // Cors services.AddCors(config => diff --git a/src/Core/Models/Api/Request/Organizations/ImportOrganizationUsersRequestModel.cs b/src/Core/Models/Api/Request/Organizations/ImportOrganizationUsersRequestModel.cs index bdf79cbbd7..93b243dd7b 100644 --- a/src/Core/Models/Api/Request/Organizations/ImportOrganizationUsersRequestModel.cs +++ b/src/Core/Models/Api/Request/Organizations/ImportOrganizationUsersRequestModel.cs @@ -1,4 +1,5 @@ -using System; +using Bit.Core.Models.Table; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -7,7 +8,8 @@ namespace Bit.Core.Models.Api public class ImportOrganizationUsersRequestModel { public Group[] Groups { get; set; } - public User[] Users { get; set; } + public User[] NewUsers { get; set; } + public User[] RemoveUsers { get; set; } public class Group { @@ -15,15 +17,18 @@ namespace Bit.Core.Models.Api public string Name { get; set; } [Required] public string ExternalId { get; set; } - - public Table.Group ToGroup(Guid organizationId) + public IEnumerable Users { get; set; } + + public Tuple> ToGroupTuple(Guid organizationId) { - return new Table.Group + var group = new Table.Group { OrganizationId = organizationId, Name = Name, ExternalId = ExternalId }; + + return new Tuple>(group, new HashSet(Users)); } } @@ -32,12 +37,6 @@ namespace Bit.Core.Models.Api [Required] [EmailAddress] public string Email { get; set; } - public IEnumerable ExternalGroupIds { get; set; } - - public KeyValuePair> ToKvp() - { - return new KeyValuePair>(Email, ExternalGroupIds); - } } } } diff --git a/src/Core/Repositories/IGroupRepository.cs b/src/Core/Repositories/IGroupRepository.cs index 837c4522d0..0f4199e960 100644 --- a/src/Core/Repositories/IGroupRepository.cs +++ b/src/Core/Repositories/IGroupRepository.cs @@ -16,5 +16,6 @@ namespace Bit.Core.Repositories Task CreateAsync(Group obj, IEnumerable collections); Task ReplaceAsync(Group obj, IEnumerable collections); Task DeleteUserAsync(Guid groupId, Guid organizationUserId); + Task UpdateUsersAsync(Guid groupId, IEnumerable organizationUserIds); } } diff --git a/src/Core/Repositories/SqlServer/GroupRepository.cs b/src/Core/Repositories/SqlServer/GroupRepository.cs index 73e65871db..30657bea70 100644 --- a/src/Core/Repositories/SqlServer/GroupRepository.cs +++ b/src/Core/Repositories/SqlServer/GroupRepository.cs @@ -130,6 +130,17 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task UpdateUsersAsync(Guid groupId, IEnumerable organizationUserIds) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteAsync( + "[dbo].[GroupUser_UpdateUsers]", + new { GroupId = groupId, OrganizationUserIds = organizationUserIds.ToGuidIdArrayTVP() }, + commandType: CommandType.StoredProcedure); + } + } + public class GroupWithCollections : Group { public DataTable Collections { get; set; } diff --git a/src/Core/Services/IOrganizationService.cs b/src/Core/Services/IOrganizationService.cs index 1df0fad82d..6f459267e5 100644 --- a/src/Core/Services/IOrganizationService.cs +++ b/src/Core/Services/IOrganizationService.cs @@ -29,7 +29,7 @@ namespace Bit.Core.Services Task SaveUserAsync(OrganizationUser user, Guid savingUserId, IEnumerable collections); Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid deletingUserId); Task DeleteUserAsync(Guid organizationId, Guid userId); - Task ImportAsync(Guid organizationId, Guid importingUserId, IEnumerable groups, - IEnumerable>> users); + Task ImportAsync(Guid organizationId, Guid importingUserId, IEnumerable>> groups, + IEnumerable newUsers, IEnumerable removeUsers); } } diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 006a3a977c..ea4e972c12 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -813,7 +813,7 @@ namespace Bit.Core.Services Guid confirmingUserId) { var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); - if(orgUser == null || orgUser.Status != Enums.OrganizationUserStatusType.Accepted || + if(orgUser == null || orgUser.Status != OrganizationUserStatusType.Accepted || orgUser.OrganizationId != organizationId) { throw new BadRequestException("User not valid."); @@ -898,8 +898,11 @@ namespace Bit.Core.Services await _organizationUserRepository.DeleteAsync(orgUser); } - public async Task ImportAsync(Guid organizationId, Guid importingUserId, IEnumerable groups, - IEnumerable>> users) + public async Task ImportAsync(Guid organizationId, + Guid importingUserId, + IEnumerable>> groups, + IEnumerable newUsers, + IEnumerable removeUsers) { var organization = await _organizationRepository.GetByIdAsync(organizationId); if(organization == null) @@ -912,100 +915,100 @@ namespace Bit.Core.Services throw new BadRequestException("Organization cannot use groups."); } - // Groups - var existingGroups = (await _groupRepository.GetManyByOrganizationIdAsync(organizationId)).ToList(); - var existingGroupsDict = existingGroups.ToDictionary(g => g.ExternalId); + var newUsersSet = new HashSet(newUsers); + var existingUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId); + var existingUsersIdDict = existingUsers.ToDictionary(u => u.Email, u => u.Id); + // Users + // Remove Users + if(removeUsers.Any()) + { + var removeUsersSet = new HashSet(removeUsers); + var existingUsersDict = existingUsers.ToDictionary(u => u.Email); + + var usersToRemove = removeUsersSet + .Except(newUsersSet) + .Where(ru => existingUsersDict.ContainsKey(ru)) + .Select(ru => existingUsersDict[ru]); + + foreach(var user in usersToRemove) + { + await _organizationUserRepository.DeleteAsync(new OrganizationUser { Id = user.Id }); + existingUsersIdDict.Remove(user.Email); + } + } + + // Add new users + if(newUsers.Any()) + { + var existingUsersSet = new HashSet(existingUsers.Select(u => u.Email)); + var usersToAdd = newUsersSet.Except(existingUsersSet).ToList(); + + var seatsAvailable = int.MaxValue; + if(organization.Seats.HasValue) + { + var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organizationId); + seatsAvailable = organization.Seats.Value - userCount; + if(seatsAvailable < usersToAdd.Count) + { + // throw exception? + return; + } + } + + foreach(var user in usersToAdd) + { + try + { + var newUser = await InviteUserAsync(organizationId, importingUserId, user, OrganizationUserType.User, + false, new List()); + existingUsersIdDict.Add(newUser.Email, newUser.Id); + } + catch(BadRequestException) + { + continue; + } + } + } + + // Groups if(groups?.Any() ?? false) { - var newGroups = groups.Where(g => !existingGroupsDict.ContainsKey(g.ExternalId)); - var updateGroups = existingGroups.Where(eg => groups.Any(g => g.ExternalId == eg.ExternalId && g.Name != eg.Name)); + var groupsDict = groups.ToDictionary(g => g.Item1.ExternalId); + var existingGroups = (await _groupRepository.GetManyByOrganizationIdAsync(organizationId)).ToList(); + var existingGroupsDict = existingGroups.ToDictionary(g => g.ExternalId); + + var newGroups = groups + .Where(g => !existingGroupsDict.ContainsKey(g.Item1.ExternalId)) + .Select(g => g.Item1); + var updateGroups = existingGroups + .Where(eg => groups.Any(g => g.Item1.ExternalId == eg.ExternalId && g.Item1.Name != eg.Name)) + .ToList(); - var createdGroups = new List(); foreach(var group in newGroups) { group.CreationDate = group.RevisionDate = DateTime.UtcNow; + await _groupRepository.CreateAsync(group); - createdGroups.Add(group); + await UpdateUsersAsync(group, groupsDict[group.ExternalId].Item2, existingUsersIdDict); } foreach(var group in updateGroups) { group.RevisionDate = DateTime.UtcNow; group.Name = existingGroupsDict[group.ExternalId].Name; + await _groupRepository.ReplaceAsync(group); - } - - // Add the newly created groups to existing groups so that we have a complete list to reference below for users. - existingGroups.AddRange(createdGroups); - existingGroupsDict = existingGroups.ToDictionary(g => g.ExternalId); - } - - // Users - if(!users?.Any() ?? true) - { - return; - } - - var existingUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId); - var existingUsersDict = existingUsers.ToDictionary(u => u.Email); - - var newUsers = users.Where(u => !existingUsersDict.ContainsKey(u.Key)).ToList(); - var updateUsers = users.Where(u => existingUsersDict.ContainsKey(u.Key)); - - var seatsAvailable = int.MaxValue; - if(organization.Seats.HasValue) - { - var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organizationId); - seatsAvailable = organization.Seats.Value - userCount; - if(seatsAvailable < newUsers.Count) - { - // throw exception? - return; + await UpdateUsersAsync(group, groupsDict[group.ExternalId].Item2, existingUsersIdDict); } } + } - foreach(var user in newUsers) - { - try - { - var newUser = await InviteUserAsync(organizationId, importingUserId, user.Key, OrganizationUserType.User, - false, new List()); - - var groupsIdsForUser = user.Value.Where(id => existingGroupsDict.ContainsKey(id)) - .Select(id => existingGroupsDict[id].Id).ToList(); - if(groupsIdsForUser.Any()) - { - await _organizationUserRepository.UpdateGroupsAsync(newUser.Id, groupsIdsForUser); - } - } - catch(BadRequestException) - { - continue; - } - } - - var existingGroupUsers = await _groupRepository.GetManyGroupUsersByOrganizationIdAsync(organizationId); - foreach(var user in updateUsers) - { - if(!existingUsersDict.ContainsKey(user.Key)) - { - continue; - } - - var existingUser = existingUsersDict[user.Key]; - var existingGroupIdsForUser = new HashSet(existingGroupUsers - .Where(gu => gu.OrganizationUserId == existingUser.Id) - .Select(gu => gu.GroupId)); - var newGroupsIdsForUser = new HashSet(user.Value - .Where(id => existingGroupsDict.ContainsKey(id)) - .Select(id => existingGroupsDict[id].Id)); - - if(!existingGroupIdsForUser.SetEquals(newGroupsIdsForUser)) - { - await _organizationUserRepository.UpdateGroupsAsync(existingUser.Id, newGroupsIdsForUser); - } - } + private async Task UpdateUsersAsync(Group group, HashSet groupUsers, + Dictionary existingUsersIdDict) + { + var users = groupUsers.Union(existingUsersIdDict.Keys).Select(u => existingUsersIdDict[u]); + await _groupRepository.UpdateUsersAsync(group.Id, users); } private async Task> GetConfirmedOwnersAsync(Guid organizationId) diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index becbb1c1d5..90b8ab84a9 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -190,5 +190,6 @@ + \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/GroupUser_UpdateUsers.sql b/src/Sql/dbo/Stored Procedures/GroupUser_UpdateUsers.sql new file mode 100644 index 0000000000..c1cb7110c3 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/GroupUser_UpdateUsers.sql @@ -0,0 +1,46 @@ +CREATE PROCEDURE [dbo].[GroupUser_UpdateUsers] + @GroupId UNIQUEIDENTIFIER, + @OrganizationUserIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + DECLARE @OrgId UNIQUEIDENTIFIER = ( + SELECT TOP 1 + [OrganizationId] + FROM + [dbo].[Group] + WHERE + [Id] = @GroupId + ) + + ;WITH [AvailableUsersCTE] AS( + SELECT + [Id] + FROM + [dbo].[OrganizationUser] + WHERE + [OrganizationId] = @OrgId + ) + MERGE + [dbo].[GroupUser] AS [Target] + USING + @OrganizationUserIds AS [Source] + ON + [Target].[GroupId] = @GroupId + AND [Target].[OrganizationUserId] = [Source].[Id] + WHEN NOT MATCHED BY TARGET + AND [Source].[Id] IN (SELECT [Id] FROM [AvailableUsersCTE]) THEN + INSERT VALUES + ( + @GroupId, + [Source].[Id] + ) + WHEN NOT MATCHED BY SOURCE + AND [Target].[GroupId] = @GroupId + AND [Target].[OrganizationUserId] IN (SELECT [Id] FROM [AvailableUsersCTE]) THEN + DELETE + ; + + -- TODO: Bump account revision date for all @OrganizationUserIds +END \ No newline at end of file