mirror of
https://github.com/bitwarden/server.git
synced 2025-02-23 03:01:23 +01:00
updated format of import data
This commit is contained in:
parent
befca0561b
commit
670b548b22
@ -239,8 +239,8 @@ namespace Bit.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
var userId = _userService.GetProperUserId(User);
|
var userId = _userService.GetProperUserId(User);
|
||||||
await _organizationService.ImportAsync(orgIdGuid, userId.Value, model.Groups.Select(g => g.ToGroup(orgIdGuid)),
|
await _organizationService.ImportAsync(orgIdGuid, userId.Value, model.Groups.Select(g => g.ToGroupTuple(orgIdGuid)),
|
||||||
model.Users.Select(u => u.ToKvp()));
|
model.NewUsers.Select(u => u.Email), model.RemoveUsers.Select(u => u.Email));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,7 @@ namespace Bit.Api
|
|||||||
|
|
||||||
// Services
|
// Services
|
||||||
services.AddBaseServices();
|
services.AddBaseServices();
|
||||||
services.AddDefaultServices();
|
services.AddNoopServices();
|
||||||
|
|
||||||
// Cors
|
// Cors
|
||||||
services.AddCors(config =>
|
services.AddCors(config =>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using Bit.Core.Models.Table;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
@ -7,7 +8,8 @@ namespace Bit.Core.Models.Api
|
|||||||
public class ImportOrganizationUsersRequestModel
|
public class ImportOrganizationUsersRequestModel
|
||||||
{
|
{
|
||||||
public Group[] Groups { get; set; }
|
public Group[] Groups { get; set; }
|
||||||
public User[] Users { get; set; }
|
public User[] NewUsers { get; set; }
|
||||||
|
public User[] RemoveUsers { get; set; }
|
||||||
|
|
||||||
public class Group
|
public class Group
|
||||||
{
|
{
|
||||||
@ -15,15 +17,18 @@ namespace Bit.Core.Models.Api
|
|||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
[Required]
|
[Required]
|
||||||
public string ExternalId { get; set; }
|
public string ExternalId { get; set; }
|
||||||
|
public IEnumerable<string> Users { get; set; }
|
||||||
|
|
||||||
public Table.Group ToGroup(Guid organizationId)
|
public Tuple<Table.Group, HashSet<string>> ToGroupTuple(Guid organizationId)
|
||||||
{
|
{
|
||||||
return new Table.Group
|
var group = new Table.Group
|
||||||
{
|
{
|
||||||
OrganizationId = organizationId,
|
OrganizationId = organizationId,
|
||||||
Name = Name,
|
Name = Name,
|
||||||
ExternalId = ExternalId
|
ExternalId = ExternalId
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return new Tuple<Table.Group, HashSet<string>>(group, new HashSet<string>(Users));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,12 +37,6 @@ namespace Bit.Core.Models.Api
|
|||||||
[Required]
|
[Required]
|
||||||
[EmailAddress]
|
[EmailAddress]
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
public IEnumerable<string> ExternalGroupIds { get; set; }
|
|
||||||
|
|
||||||
public KeyValuePair<string, IEnumerable<string>> ToKvp()
|
|
||||||
{
|
|
||||||
return new KeyValuePair<string, IEnumerable<string>>(Email, ExternalGroupIds);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,5 +16,6 @@ namespace Bit.Core.Repositories
|
|||||||
Task CreateAsync(Group obj, IEnumerable<SelectionReadOnly> collections);
|
Task CreateAsync(Group obj, IEnumerable<SelectionReadOnly> collections);
|
||||||
Task ReplaceAsync(Group obj, IEnumerable<SelectionReadOnly> collections);
|
Task ReplaceAsync(Group obj, IEnumerable<SelectionReadOnly> collections);
|
||||||
Task DeleteUserAsync(Guid groupId, Guid organizationUserId);
|
Task DeleteUserAsync(Guid groupId, Guid organizationUserId);
|
||||||
|
Task UpdateUsersAsync(Guid groupId, IEnumerable<Guid> organizationUserIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,6 +130,17 @@ namespace Bit.Core.Repositories.SqlServer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task UpdateUsersAsync(Guid groupId, IEnumerable<Guid> 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 class GroupWithCollections : Group
|
||||||
{
|
{
|
||||||
public DataTable Collections { get; set; }
|
public DataTable Collections { get; set; }
|
||||||
|
@ -29,7 +29,7 @@ namespace Bit.Core.Services
|
|||||||
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 ImportAsync(Guid organizationId, Guid importingUserId, IEnumerable<Group> groups,
|
Task ImportAsync(Guid organizationId, Guid importingUserId, IEnumerable<Tuple<Group, HashSet<string>>> groups,
|
||||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>> users);
|
IEnumerable<string> newUsers, IEnumerable<string> removeUsers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -813,7 +813,7 @@ namespace Bit.Core.Services
|
|||||||
Guid confirmingUserId)
|
Guid confirmingUserId)
|
||||||
{
|
{
|
||||||
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
||||||
if(orgUser == null || orgUser.Status != Enums.OrganizationUserStatusType.Accepted ||
|
if(orgUser == null || orgUser.Status != OrganizationUserStatusType.Accepted ||
|
||||||
orgUser.OrganizationId != organizationId)
|
orgUser.OrganizationId != organizationId)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("User not valid.");
|
throw new BadRequestException("User not valid.");
|
||||||
@ -898,8 +898,11 @@ namespace Bit.Core.Services
|
|||||||
await _organizationUserRepository.DeleteAsync(orgUser);
|
await _organizationUserRepository.DeleteAsync(orgUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ImportAsync(Guid organizationId, Guid importingUserId, IEnumerable<Group> groups,
|
public async Task ImportAsync(Guid organizationId,
|
||||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>> users)
|
Guid importingUserId,
|
||||||
|
IEnumerable<Tuple<Group, HashSet<string>>> groups,
|
||||||
|
IEnumerable<string> newUsers,
|
||||||
|
IEnumerable<string> removeUsers)
|
||||||
{
|
{
|
||||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||||
if(organization == null)
|
if(organization == null)
|
||||||
@ -912,102 +915,102 @@ namespace Bit.Core.Services
|
|||||||
throw new BadRequestException("Organization cannot use groups.");
|
throw new BadRequestException("Organization cannot use groups.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Groups
|
var newUsersSet = new HashSet<string>(newUsers);
|
||||||
var existingGroups = (await _groupRepository.GetManyByOrganizationIdAsync(organizationId)).ToList();
|
var existingUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
||||||
var existingGroupsDict = existingGroups.ToDictionary(g => g.ExternalId);
|
var existingUsersIdDict = existingUsers.ToDictionary(u => u.Email, u => u.Id);
|
||||||
|
|
||||||
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 createdGroups = new List<Group>();
|
|
||||||
foreach(var group in newGroups)
|
|
||||||
{
|
|
||||||
group.CreationDate = group.RevisionDate = DateTime.UtcNow;
|
|
||||||
await _groupRepository.CreateAsync(group);
|
|
||||||
createdGroups.Add(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(createdGroups);
|
|
||||||
existingGroupsDict = existingGroups.ToDictionary(g => g.ExternalId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Users
|
// Users
|
||||||
if(!users?.Any() ?? true)
|
// Remove Users
|
||||||
|
if(removeUsers.Any())
|
||||||
{
|
{
|
||||||
return;
|
var removeUsersSet = new HashSet<string>(removeUsers);
|
||||||
}
|
|
||||||
|
|
||||||
var existingUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
|
||||||
var existingUsersDict = existingUsers.ToDictionary(u => u.Email);
|
var existingUsersDict = existingUsers.ToDictionary(u => u.Email);
|
||||||
|
|
||||||
var newUsers = users.Where(u => !existingUsersDict.ContainsKey(u.Key)).ToList();
|
var usersToRemove = removeUsersSet
|
||||||
var updateUsers = users.Where(u => existingUsersDict.ContainsKey(u.Key));
|
.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<string>(existingUsers.Select(u => u.Email));
|
||||||
|
var usersToAdd = newUsersSet.Except(existingUsersSet).ToList();
|
||||||
|
|
||||||
var seatsAvailable = int.MaxValue;
|
var seatsAvailable = int.MaxValue;
|
||||||
if(organization.Seats.HasValue)
|
if(organization.Seats.HasValue)
|
||||||
{
|
{
|
||||||
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organizationId);
|
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organizationId);
|
||||||
seatsAvailable = organization.Seats.Value - userCount;
|
seatsAvailable = organization.Seats.Value - userCount;
|
||||||
if(seatsAvailable < newUsers.Count)
|
if(seatsAvailable < usersToAdd.Count)
|
||||||
{
|
{
|
||||||
// throw exception?
|
// throw exception?
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach(var user in newUsers)
|
foreach(var user in usersToAdd)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var newUser = await InviteUserAsync(organizationId, importingUserId, user.Key, OrganizationUserType.User,
|
var newUser = await InviteUserAsync(organizationId, importingUserId, user, OrganizationUserType.User,
|
||||||
false, new List<SelectionReadOnly>());
|
false, new List<SelectionReadOnly>());
|
||||||
|
existingUsersIdDict.Add(newUser.Email, newUser.Id);
|
||||||
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)
|
catch(BadRequestException)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var existingGroupUsers = await _groupRepository.GetManyGroupUsersByOrganizationIdAsync(organizationId);
|
|
||||||
foreach(var user in updateUsers)
|
|
||||||
{
|
|
||||||
if(!existingUsersDict.ContainsKey(user.Key))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var existingUser = existingUsersDict[user.Key];
|
// Groups
|
||||||
var existingGroupIdsForUser = new HashSet<Guid>(existingGroupUsers
|
if(groups?.Any() ?? false)
|
||||||
.Where(gu => gu.OrganizationUserId == existingUser.Id)
|
|
||||||
.Select(gu => gu.GroupId));
|
|
||||||
var newGroupsIdsForUser = new HashSet<Guid>(user.Value
|
|
||||||
.Where(id => existingGroupsDict.ContainsKey(id))
|
|
||||||
.Select(id => existingGroupsDict[id].Id));
|
|
||||||
|
|
||||||
if(!existingGroupIdsForUser.SetEquals(newGroupsIdsForUser))
|
|
||||||
{
|
{
|
||||||
await _organizationUserRepository.UpdateGroupsAsync(existingUser.Id, newGroupsIdsForUser);
|
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();
|
||||||
|
|
||||||
|
foreach(var group in newGroups)
|
||||||
|
{
|
||||||
|
group.CreationDate = group.RevisionDate = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _groupRepository.CreateAsync(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);
|
||||||
|
await UpdateUsersAsync(group, groupsDict[group.ExternalId].Item2, existingUsersIdDict);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task UpdateUsersAsync(Group group, HashSet<string> groupUsers,
|
||||||
|
Dictionary<string, Guid> existingUsersIdDict)
|
||||||
|
{
|
||||||
|
var users = groupUsers.Union(existingUsersIdDict.Keys).Select(u => existingUsersIdDict[u]);
|
||||||
|
await _groupRepository.UpdateUsersAsync(group.Id, users);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<IEnumerable<OrganizationUser>> GetConfirmedOwnersAsync(Guid organizationId)
|
private async Task<IEnumerable<OrganizationUser>> GetConfirmedOwnersAsync(Guid organizationId)
|
||||||
{
|
{
|
||||||
var owners = await _organizationUserRepository.GetManyByOrganizationAsync(organizationId,
|
var owners = await _organizationUserRepository.GetManyByOrganizationAsync(organizationId,
|
||||||
|
@ -190,5 +190,6 @@
|
|||||||
<Build Include="dbo\Stored Procedures\OrganizationUser_UpdateWithCollections.sql" />
|
<Build Include="dbo\Stored Procedures\OrganizationUser_UpdateWithCollections.sql" />
|
||||||
<Build Include="dbo\Stored Procedures\CollectionUser_Delete.sql" />
|
<Build Include="dbo\Stored Procedures\CollectionUser_Delete.sql" />
|
||||||
<Build Include="dbo\Stored Procedures\GroupUser_ReadByOrganizationId.sql" />
|
<Build Include="dbo\Stored Procedures\GroupUser_ReadByOrganizationId.sql" />
|
||||||
|
<Build Include="dbo\Stored Procedures\GroupUser_UpdateUsers.sql" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
46
src/Sql/dbo/Stored Procedures/GroupUser_UpdateUsers.sql
Normal file
46
src/Sql/dbo/Stored Procedures/GroupUser_UpdateUsers.sql
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user