diff --git a/src/Api/Controllers/CiphersController.cs b/src/Api/Controllers/CiphersController.cs index 59ab5d4b14..19f5d78957 100644 --- a/src/Api/Controllers/CiphersController.cs +++ b/src/Api/Controllers/CiphersController.cs @@ -131,7 +131,7 @@ namespace Bit.Api.Controllers } [HttpPost("import")] - public async Task PostImport([FromBody]ImportRequestModel model) + public async Task PostImport([FromBody]ImportPasswordsRequestModel model) { var userId = _userService.GetProperUserId(User).Value; var folders = model.Folders.Select(f => f.ToFolder(userId)).ToList(); diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index cebe4849ce..fdf4131e59 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -228,5 +228,17 @@ namespace Bit.Api.Controllers await _organizationService.DeleteAsync(organization); } } + + [HttpPost("{id}/import")] + public Task Import(string id, [FromBody]ImportOrganizationUsersRequestModel model) + { + var orgIdGuid = new Guid(id); + if(!_currentContext.OrganizationAdmin(orgIdGuid)) + { + throw new NotFoundException(); + } + + return Task.FromResult(0); + } } } diff --git a/src/Core/Models/Api/Request/Accounts/ImportRequestModel.cs b/src/Core/Models/Api/Request/Accounts/ImportPasswordsRequestModel.cs similarity index 92% rename from src/Core/Models/Api/Request/Accounts/ImportRequestModel.cs rename to src/Core/Models/Api/Request/Accounts/ImportPasswordsRequestModel.cs index dfc272a1fb..8ab229c8ae 100644 --- a/src/Core/Models/Api/Request/Accounts/ImportRequestModel.cs +++ b/src/Core/Models/Api/Request/Accounts/ImportPasswordsRequestModel.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace Bit.Core.Models.Api { - public class ImportRequestModel + public class ImportPasswordsRequestModel { private LoginRequestModel[] _logins; diff --git a/src/Core/Models/Api/Request/Organizations/ImportOrganizationUsersRequestModel.cs b/src/Core/Models/Api/Request/Organizations/ImportOrganizationUsersRequestModel.cs new file mode 100644 index 0000000000..bdf79cbbd7 --- /dev/null +++ b/src/Core/Models/Api/Request/Organizations/ImportOrganizationUsersRequestModel.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Models.Api +{ + public class ImportOrganizationUsersRequestModel + { + public Group[] Groups { get; set; } + public User[] Users { get; set; } + + public class Group + { + [Required] + public string Name { get; set; } + [Required] + public string ExternalId { get; set; } + + public Table.Group ToGroup(Guid organizationId) + { + return new Table.Group + { + OrganizationId = organizationId, + Name = Name, + ExternalId = ExternalId + }; + } + } + + public class User + { + [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/Services/Implementations/GroupService.cs b/src/Core/Services/Implementations/GroupService.cs index 2728d88abe..70ab256f6c 100644 --- a/src/Core/Services/Implementations/GroupService.cs +++ b/src/Core/Services/Implementations/GroupService.cs @@ -36,6 +36,8 @@ namespace Bit.Core.Services if(group.Id == default(Guid)) { + group.CreationDate = group.RevisionDate = DateTime.UtcNow; + if(collections == null) { await _groupRepository.CreateAsync(group); @@ -47,6 +49,8 @@ namespace Bit.Core.Services } else { + group.RevisionDate = DateTime.UtcNow; + if(collections == null) { await _groupRepository.ReplaceAsync(group); diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 98fb478d60..ac00441964 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -21,6 +21,7 @@ namespace Bit.Core.Services private readonly IOrganizationUserRepository _organizationUserRepository; private readonly ICollectionRepository _collectionRepository; private readonly IUserRepository _userRepository; + private readonly IGroupRepository _groupRepository; private readonly IDataProtector _dataProtector; private readonly IMailService _mailService; private readonly IPushService _pushService; @@ -30,6 +31,7 @@ namespace Bit.Core.Services IOrganizationUserRepository organizationUserRepository, ICollectionRepository collectionRepository, IUserRepository userRepository, + IGroupRepository groupRepository, IDataProtectionProvider dataProtectionProvider, IMailService mailService, IPushService pushService) @@ -38,6 +40,7 @@ namespace Bit.Core.Services _organizationUserRepository = organizationUserRepository; _collectionRepository = collectionRepository; _userRepository = userRepository; + _groupRepository = groupRepository; _dataProtector = dataProtectionProvider.CreateProtector("OrganizationServiceDataProtector"); _mailService = mailService; _pushService = pushService; @@ -895,11 +898,105 @@ namespace Bit.Core.Services await _organizationUserRepository.DeleteAsync(orgUser); } + public async Task ImportAsync(Guid organizationId, Guid importingUserId, IEnumerable groups, + IEnumerable>> users) + { + var organization = await _organizationRepository.GetByIdAsync(organizationId); + if(organization == null) + { + throw new NotFoundException(); + } + + if(!organization.UseGroups) + { + throw new BadRequestException("Organization cannot use groups."); + } + + // Groups + var existingGroups = (await _groupRepository.GetManyByOrganizationIdAsync(organizationId)).ToList(); + var existingGroupsDict = existingGroups.ToDictionary(g => g.ExternalId); + + 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)); + + foreach(var group in newGroups) + { + group.CreationDate = group.RevisionDate = DateTime.UtcNow; + await _groupRepository.CreateAsync(group); + } + + 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(newGroups); + existingGroupsDict = existingGroups.ToDictionary(g => g.ExternalId); + + // Users + 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; + } + } + + 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; + } + } + + foreach(var user in updateUsers) + { + if(!existingUsersDict.ContainsKey(user.Key)) + { + continue; + } + + var existingUser = existingUsersDict[user.Key]; + var groupsIdsForUser = user.Value.Where(id => existingGroupsDict.ContainsKey(id)) + .Select(id => existingGroupsDict[id].Id).ToList(); + if(groupsIdsForUser.Any()) + { + await _organizationUserRepository.UpdateGroupsAsync(existingUser.Id, groupsIdsForUser); + } + } + } + private async Task> GetConfirmedOwnersAsync(Guid organizationId) { var owners = await _organizationUserRepository.GetManyByOrganizationAsync(organizationId, - Enums.OrganizationUserType.Owner); - return owners.Where(o => o.Status == Enums.OrganizationUserStatusType.Confirmed); + OrganizationUserType.Owner); + return owners.Where(o => o.Status == OrganizationUserStatusType.Confirmed); } } }