1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-14 09:38:16 -05:00

group user assignment apis

This commit is contained in:
Kyle Spearrin 2017-05-09 19:04:01 -04:00
parent 07878cbaeb
commit 7a4d20ac1f
15 changed files with 249 additions and 3 deletions

View File

@ -8,6 +8,7 @@ using Bit.Core.Models.Api;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core; using Bit.Core;
using System.Collections.Generic;
namespace Bit.Api.Controllers namespace Bit.Api.Controllers
{ {
@ -67,6 +68,21 @@ namespace Bit.Api.Controllers
return new ListResponseModel<GroupResponseModel>(responses); return new ListResponseModel<GroupResponseModel>(responses);
} }
[HttpGet("{id}/users")]
public async Task<ListResponseModel<GroupUserResponseModel>> GetUsers(string orgId, string id)
{
var idGuid = new Guid(id);
var group = await _groupRepository.GetByIdAsync(idGuid);
if(group == null || !_currentContext.OrganizationAdmin(group.OrganizationId))
{
throw new NotFoundException();
}
var groups = await _groupRepository.GetManyUserDetailsByIdAsync(idGuid);
var responses = groups.Select(g => new GroupUserResponseModel(g));
return new ListResponseModel<GroupUserResponseModel>(responses);
}
[HttpPost("")] [HttpPost("")]
public async Task<GroupResponseModel> Post(string orgId, [FromBody]GroupRequestModel model) public async Task<GroupResponseModel> Post(string orgId, [FromBody]GroupRequestModel model)
{ {

View File

@ -8,6 +8,7 @@ using Bit.Core.Models.Api;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core; using Bit.Core;
using System.Collections.Generic;
namespace Bit.Api.Controllers namespace Bit.Api.Controllers
{ {
@ -19,6 +20,7 @@ namespace Bit.Api.Controllers
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationService _organizationService; private readonly IOrganizationService _organizationService;
private readonly ICollectionRepository _collectionRepository; private readonly ICollectionRepository _collectionRepository;
private readonly IGroupRepository _groupRepository;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly CurrentContext _currentContext; private readonly CurrentContext _currentContext;
@ -27,6 +29,7 @@ namespace Bit.Api.Controllers
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IOrganizationService organizationService, IOrganizationService organizationService,
ICollectionRepository collectionRepository, ICollectionRepository collectionRepository,
IGroupRepository groupRepository,
IUserService userService, IUserService userService,
CurrentContext currentContext) CurrentContext currentContext)
{ {
@ -34,6 +37,7 @@ namespace Bit.Api.Controllers
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_organizationService = organizationService; _organizationService = organizationService;
_collectionRepository = collectionRepository; _collectionRepository = collectionRepository;
_groupRepository = groupRepository;
_userService = userService; _userService = userService;
_currentContext = currentContext; _currentContext = currentContext;
} }
@ -64,6 +68,20 @@ namespace Bit.Api.Controllers
return new ListResponseModel<OrganizationUserResponseModel>(responses); return new ListResponseModel<OrganizationUserResponseModel>(responses);
} }
[HttpGet("{id}/groups")]
public async Task<IEnumerable<string>> GetGroups(string orgId, string id)
{
var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id));
if(organizationUser == null || !_currentContext.OrganizationAdmin(organizationUser.OrganizationId))
{
throw new NotFoundException();
}
var groupIds = await _groupRepository.GetManyIdsByUserIdAsync(organizationUser.Id);
var responses = groupIds.Select(g => g.ToString());
return responses;
}
[HttpPost("invite")] [HttpPost("invite")]
public async Task Invite(string orgId, [FromBody]OrganizationUserInviteRequestModel model) public async Task Invite(string orgId, [FromBody]OrganizationUserInviteRequestModel model)
{ {
@ -135,6 +153,25 @@ namespace Bit.Api.Controllers
model.Collections?.Select(c => c.ToCollectionUser())); model.Collections?.Select(c => c.ToCollectionUser()));
} }
[HttpPut("{id}/groups")]
[HttpPost("{id}/groups")]
public async Task PutGroups(string orgId, string id, [FromBody]OrganizationUserUpdateGroupsRequestModel model)
{
var orgGuidId = new Guid(orgId);
if(!_currentContext.OrganizationAdmin(orgGuidId))
{
throw new NotFoundException();
}
var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id));
if(organizationUser == null || organizationUser.OrganizationId != orgGuidId)
{
throw new NotFoundException();
}
await _organizationUserRepository.UpdateGroupsAsync(organizationUser.Id, model.GroupIds.Select(g => new Guid(g)));
}
[HttpDelete("{id}")] [HttpDelete("{id}")]
[HttpPost("{id}/delete")] [HttpPost("{id}/delete")]
public async Task Delete(string orgId, string id) public async Task Delete(string orgId, string id)

View File

@ -43,6 +43,12 @@ namespace Bit.Core.Models.Api
} }
} }
public class OrganizationUserUpdateGroupsRequestModel
{
[Required]
public IEnumerable<string> GroupIds { get; set; }
}
public class OrganizationUserCollectionRequestModel public class OrganizationUserCollectionRequestModel
{ {
[Required] [Required]

View File

@ -0,0 +1,34 @@
using System;
using Bit.Core.Models.Data;
using Bit.Core.Enums;
namespace Bit.Core.Models.Api
{
public class GroupUserResponseModel : ResponseModel
{
public GroupUserResponseModel(GroupUserUserDetails groupUser)
: base("groupUser")
{
if(groupUser == null)
{
throw new ArgumentNullException(nameof(groupUser));
}
OrganizationUserId = groupUser.OrganizationUserId.ToString();
GroupId = groupUser.GroupId.ToString();
AccessAll = groupUser.AccessAll;
Name = groupUser.Name;
Email = groupUser.Email;
Type = groupUser.Type;
Status = groupUser.Status;
}
public string OrganizationUserId { get; set; }
public string GroupId { get; set; }
public bool AccessAll { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public OrganizationUserType Type { get; set; }
public OrganizationUserStatusType Status { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using System;
namespace Bit.Core.Models.Data
{
public class GroupUserUserDetails
{
public Guid OrganizationUserId { get; set; }
public Guid OrganizationId { get; set; }
public Guid GroupId { get; set; }
public bool AccessAll { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public Enums.OrganizationUserStatusType Status { get; set; }
public Enums.OrganizationUserType Type { get; set; }
}
}

View File

@ -2,6 +2,7 @@
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Models.Data;
namespace Bit.Core.Repositories namespace Bit.Core.Repositories
{ {
@ -9,6 +10,8 @@ namespace Bit.Core.Repositories
{ {
Task<Tuple<Group, ICollection<Guid>>> GetByIdWithCollectionsAsync(Guid id); Task<Tuple<Group, ICollection<Guid>>> GetByIdWithCollectionsAsync(Guid id);
Task<ICollection<Group>> GetManyByOrganizationIdAsync(Guid organizationId); Task<ICollection<Group>> GetManyByOrganizationIdAsync(Guid organizationId);
Task<ICollection<GroupUserUserDetails>> GetManyUserDetailsByIdAsync(Guid id);
Task<ICollection<Guid>> GetManyIdsByUserIdAsync(Guid organizationUserId);
Task CreateAsync(Group obj, IEnumerable<Guid> collectionIds); Task CreateAsync(Group obj, IEnumerable<Guid> collectionIds);
Task ReplaceAsync(Group obj, IEnumerable<Guid> collectionIds); Task ReplaceAsync(Group obj, IEnumerable<Guid> collectionIds);
} }

View File

@ -20,5 +20,6 @@ namespace Bit.Core.Repositories
Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync(Guid organizationId); Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync(Guid organizationId);
Task<ICollection<OrganizationUserOrganizationDetails>> GetManyDetailsByUserAsync(Guid userId, Task<ICollection<OrganizationUserOrganizationDetails>> GetManyDetailsByUserAsync(Guid userId,
OrganizationUserStatusType? status = null); OrganizationUserStatusType? status = null);
Task UpdateGroupsAsync(Guid orgUserId, IEnumerable<Guid> groupIds);
} }
} }

View File

@ -8,6 +8,7 @@ using System.Data.SqlClient;
using System.Linq; using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Core.Models.Data;
namespace Bit.Core.Repositories.SqlServer namespace Bit.Core.Repositories.SqlServer
{ {
@ -50,6 +51,32 @@ namespace Bit.Core.Repositories.SqlServer
} }
} }
public async Task<ICollection<GroupUserUserDetails>> GetManyUserDetailsByIdAsync(Guid id)
{
using(var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<GroupUserUserDetails>(
$"[{Schema}].[GroupUserUserDetails_ReadByGroupId]",
new { GroupId = id },
commandType: CommandType.StoredProcedure);
return results.ToList();
}
}
public async Task<ICollection<Guid>> GetManyIdsByUserIdAsync(Guid organizationUserId)
{
using(var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<Guid>(
$"[{Schema}].[GroupUser_ReadGroupIdsByOrganizationUserId]",
new { OrganizationUserId = organizationUserId },
commandType: CommandType.StoredProcedure);
return results.ToList();
}
}
public async Task CreateAsync(Group obj, IEnumerable<Guid> collectionIds) public async Task CreateAsync(Group obj, IEnumerable<Guid> collectionIds)
{ {
obj.SetNewId(); obj.SetNewId();

View File

@ -8,6 +8,7 @@ using System.Linq;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using System.Collections.Generic; using System.Collections.Generic;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Utilities;
namespace Bit.Core.Repositories.SqlServer namespace Bit.Core.Repositories.SqlServer
{ {
@ -155,5 +156,16 @@ namespace Bit.Core.Repositories.SqlServer
return results.ToList(); return results.ToList();
} }
} }
public async Task UpdateGroupsAsync(Guid orgUserId, IEnumerable<Guid> groupIds)
{
using(var connection = new SqlConnection(ConnectionString))
{
var results = await connection.ExecuteAsync(
"[dbo].[GroupUser_UpdateGroups]",
new { OrganizationUserId = orgUserId, GroupIds = groupIds.ToGuidIdArrayTVP() },
commandType: CommandType.StoredProcedure);
}
}
} }
} }

View File

@ -190,5 +190,12 @@
<Build Include="dbo\Stored Procedures\Collection_UpdateWithGroups.sql" /> <Build Include="dbo\Stored Procedures\Collection_UpdateWithGroups.sql" />
<Build Include="dbo\Stored Procedures\Collection_CreateWithGroups.sql" /> <Build Include="dbo\Stored Procedures\Collection_CreateWithGroups.sql" />
<Build Include="dbo\Stored Procedures\Collection_ReadWithGroupsById.sql" /> <Build Include="dbo\Stored Procedures\Collection_ReadWithGroupsById.sql" />
<Build Include="dbo\Views\GroupUserUserDetailsView.sql" />
<Build Include="dbo\Stored Procedures\GroupUserUserDetails_ReadByGroupId.sql" />
<Build Include="dbo\Stored Procedures\GroupUser_ReadGroupIdsByOrganizationUserId.sql" />
<Build Include="dbo\Stored Procedures\GroupUser_UpdateGroups.sql" />
</ItemGroup>
<ItemGroup>
<RefactorLog Include="Sql.refactorlog" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[GroupUserUserDetails_ReadByGroupId]
@GroupId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[GroupUserUserDetailsView]
WHERE
[GroupId] = @GroupId
END

View File

@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[GroupUser_ReadGroupIdsByOrganizationUserId]
@OrganizationUserId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
[GroupId]
FROM
[dbo].[GroupUser]
WHERE
[OrganizationUserId] = @OrganizationUserId
END

View File

@ -0,0 +1,44 @@
CREATE PROCEDURE [dbo].[GroupUser_UpdateGroups]
@OrganizationUserId UNIQUEIDENTIFIER,
@GroupIds AS [dbo].[GuidIdArray] READONLY
AS
BEGIN
SET NOCOUNT ON
DECLARE @OrgId UNIQUEIDENTIFIER = (
SELECT TOP 1
[OrganizationId]
FROM
[dbo].[OrganizationUser]
WHERE
[Id] = @OrganizationUserId
)
;WITH [AvailableGroupsCTE] AS(
SELECT
[Id]
FROM
[dbo].[Group]
WHERE
[OrganizationId] = @OrgId
)
MERGE
[dbo].[GroupUser] AS [Target]
USING
@GroupIds AS [Source]
ON
[Target].[GroupId] = [Source].[Id]
AND [Target].[OrganizationUserId] = @OrganizationUserId
WHEN NOT MATCHED BY TARGET
AND [Source].[Id] IN (SELECT [Id] FROM [AvailableGroupsCTE]) THEN
INSERT VALUES
(
[Source].[Id],
@OrganizationUserId
)
WHEN NOT MATCHED BY SOURCE
AND [Target].[OrganizationUserId] = @OrganizationUserId
AND [Target].[GroupId] IN (SELECT [Id] FROM [AvailableGroupsCTE]) THEN
DELETE
;
END

View File

@ -1,8 +1,8 @@
CREATE TABLE [dbo].[GroupUser] ( CREATE TABLE [dbo].[GroupUser] (
[GroupId] UNIQUEIDENTIFIER NOT NULL, [GroupId] UNIQUEIDENTIFIER NOT NULL,
[UserId] UNIQUEIDENTIFIER NOT NULL, [OrganizationUserId] UNIQUEIDENTIFIER NOT NULL,
CONSTRAINT [PK_GroupUser] PRIMARY KEY CLUSTERED ([GroupId] ASC, [UserId] ASC), CONSTRAINT [PK_GroupUser] PRIMARY KEY CLUSTERED ([GroupId] ASC, [OrganizationUserId] ASC),
CONSTRAINT [FK_GroupUser_Group] FOREIGN KEY ([GroupId]) REFERENCES [dbo].[Group] ([Id]) ON DELETE CASCADE, CONSTRAINT [FK_GroupUser_Group] FOREIGN KEY ([GroupId]) REFERENCES [dbo].[Group] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_GroupUser_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) CONSTRAINT [FK_GroupUser_OrganizationUser] FOREIGN KEY ([OrganizationUserId]) REFERENCES [dbo].[OrganizationUser] ([Id])
); );

View File

@ -0,0 +1,17 @@
CREATE VIEW [dbo].[GroupUserUserDetailsView]
AS
SELECT
OU.[Id] AS [OrganizationUserId],
OU.[OrganizationId],
OU.[AccessAll],
GU.[GroupId],
U.[Name],
ISNULL(U.[Email], OU.[Email]) Email,
OU.[Status],
OU.[Type]
FROM
[dbo].[OrganizationUser] OU
INNER JOIN
[dbo].[GroupUser] GU ON GU.[OrganizationUserId] = OU.[Id]
INNER JOIN
[dbo].[User] U ON U.[Id] = OU.[UserId]