diff --git a/src/Api/Controllers/GroupsController.cs b/src/Api/Controllers/GroupsController.cs index 735f5edd3a..c8514c35fb 100644 --- a/src/Api/Controllers/GroupsController.cs +++ b/src/Api/Controllers/GroupsController.cs @@ -8,6 +8,7 @@ using Bit.Core.Models.Api; using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Core; +using System.Collections.Generic; namespace Bit.Api.Controllers { @@ -67,6 +68,21 @@ namespace Bit.Api.Controllers return new ListResponseModel(responses); } + [HttpGet("{id}/users")] + public async Task> 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(responses); + } + [HttpPost("")] public async Task Post(string orgId, [FromBody]GroupRequestModel model) { diff --git a/src/Api/Controllers/OrganizationUsersController.cs b/src/Api/Controllers/OrganizationUsersController.cs index 809cbb4270..daedeaa32a 100644 --- a/src/Api/Controllers/OrganizationUsersController.cs +++ b/src/Api/Controllers/OrganizationUsersController.cs @@ -8,6 +8,7 @@ using Bit.Core.Models.Api; using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Core; +using System.Collections.Generic; namespace Bit.Api.Controllers { @@ -19,6 +20,7 @@ namespace Bit.Api.Controllers private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationService _organizationService; private readonly ICollectionRepository _collectionRepository; + private readonly IGroupRepository _groupRepository; private readonly IUserService _userService; private readonly CurrentContext _currentContext; @@ -27,6 +29,7 @@ namespace Bit.Api.Controllers IOrganizationUserRepository organizationUserRepository, IOrganizationService organizationService, ICollectionRepository collectionRepository, + IGroupRepository groupRepository, IUserService userService, CurrentContext currentContext) { @@ -34,6 +37,7 @@ namespace Bit.Api.Controllers _organizationUserRepository = organizationUserRepository; _organizationService = organizationService; _collectionRepository = collectionRepository; + _groupRepository = groupRepository; _userService = userService; _currentContext = currentContext; } @@ -64,6 +68,20 @@ namespace Bit.Api.Controllers return new ListResponseModel(responses); } + [HttpGet("{id}/groups")] + public async Task> 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")] public async Task Invite(string orgId, [FromBody]OrganizationUserInviteRequestModel model) { @@ -135,6 +153,25 @@ namespace Bit.Api.Controllers 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}")] [HttpPost("{id}/delete")] public async Task Delete(string orgId, string id) diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationUserRequestModels.cs b/src/Core/Models/Api/Request/Organizations/OrganizationUserRequestModels.cs index 8a84b23937..6fa68d38f9 100644 --- a/src/Core/Models/Api/Request/Organizations/OrganizationUserRequestModels.cs +++ b/src/Core/Models/Api/Request/Organizations/OrganizationUserRequestModels.cs @@ -43,6 +43,12 @@ namespace Bit.Core.Models.Api } } + public class OrganizationUserUpdateGroupsRequestModel + { + [Required] + public IEnumerable GroupIds { get; set; } + } + public class OrganizationUserCollectionRequestModel { [Required] diff --git a/src/Core/Models/Api/Response/GroupUserResponseModel.cs b/src/Core/Models/Api/Response/GroupUserResponseModel.cs new file mode 100644 index 0000000000..c3851cf2c1 --- /dev/null +++ b/src/Core/Models/Api/Response/GroupUserResponseModel.cs @@ -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; } + } +} diff --git a/src/Core/Models/Data/GroupUserUserDetails.cs b/src/Core/Models/Data/GroupUserUserDetails.cs new file mode 100644 index 0000000000..16e4483d18 --- /dev/null +++ b/src/Core/Models/Data/GroupUserUserDetails.cs @@ -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; } + } +} diff --git a/src/Core/Repositories/IGroupRepository.cs b/src/Core/Repositories/IGroupRepository.cs index 0fffdffd3e..0283b7e01a 100644 --- a/src/Core/Repositories/IGroupRepository.cs +++ b/src/Core/Repositories/IGroupRepository.cs @@ -2,6 +2,7 @@ using Bit.Core.Models.Table; using System.Collections.Generic; using System.Threading.Tasks; +using Bit.Core.Models.Data; namespace Bit.Core.Repositories { @@ -9,6 +10,8 @@ namespace Bit.Core.Repositories { Task>> GetByIdWithCollectionsAsync(Guid id); Task> GetManyByOrganizationIdAsync(Guid organizationId); + Task> GetManyUserDetailsByIdAsync(Guid id); + Task> GetManyIdsByUserIdAsync(Guid organizationUserId); Task CreateAsync(Group obj, IEnumerable collectionIds); Task ReplaceAsync(Group obj, IEnumerable collectionIds); } diff --git a/src/Core/Repositories/IOrganizationUserRepository.cs b/src/Core/Repositories/IOrganizationUserRepository.cs index c86c0f4254..eaf2fb975e 100644 --- a/src/Core/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/Repositories/IOrganizationUserRepository.cs @@ -20,5 +20,6 @@ namespace Bit.Core.Repositories Task> GetManyDetailsByOrganizationAsync(Guid organizationId); Task> GetManyDetailsByUserAsync(Guid userId, OrganizationUserStatusType? status = null); + Task UpdateGroupsAsync(Guid orgUserId, IEnumerable groupIds); } } diff --git a/src/Core/Repositories/SqlServer/GroupRepository.cs b/src/Core/Repositories/SqlServer/GroupRepository.cs index c2f807bc2a..13bc6842dc 100644 --- a/src/Core/Repositories/SqlServer/GroupRepository.cs +++ b/src/Core/Repositories/SqlServer/GroupRepository.cs @@ -8,6 +8,7 @@ using System.Data.SqlClient; using System.Linq; using Newtonsoft.Json; using Bit.Core.Utilities; +using Bit.Core.Models.Data; namespace Bit.Core.Repositories.SqlServer { @@ -50,6 +51,32 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task> GetManyUserDetailsByIdAsync(Guid id) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[GroupUserUserDetails_ReadByGroupId]", + new { GroupId = id }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + + public async Task> GetManyIdsByUserIdAsync(Guid organizationUserId) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[GroupUser_ReadGroupIdsByOrganizationUserId]", + new { OrganizationUserId = organizationUserId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + public async Task CreateAsync(Group obj, IEnumerable collectionIds) { obj.SetNewId(); diff --git a/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs b/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs index 4ea14545b7..49efb1767a 100644 --- a/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs +++ b/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs @@ -8,6 +8,7 @@ using System.Linq; using Bit.Core.Models.Data; using System.Collections.Generic; using Bit.Core.Enums; +using Bit.Core.Utilities; namespace Bit.Core.Repositories.SqlServer { @@ -155,5 +156,16 @@ namespace Bit.Core.Repositories.SqlServer return results.ToList(); } } + + public async Task UpdateGroupsAsync(Guid orgUserId, IEnumerable groupIds) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteAsync( + "[dbo].[GroupUser_UpdateGroups]", + new { OrganizationUserId = orgUserId, GroupIds = groupIds.ToGuidIdArrayTVP() }, + commandType: CommandType.StoredProcedure); + } + } } } diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index 2e97ca8dc6..daa2facd4e 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -190,5 +190,12 @@ + + + + + + + \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/GroupUserUserDetails_ReadByGroupId.sql b/src/Sql/dbo/Stored Procedures/GroupUserUserDetails_ReadByGroupId.sql new file mode 100644 index 0000000000..65e1616e10 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/GroupUserUserDetails_ReadByGroupId.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[GroupUserUserDetails_ReadByGroupId] + @GroupId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[GroupUserUserDetailsView] + WHERE + [GroupId] = @GroupId +END diff --git a/src/Sql/dbo/Stored Procedures/GroupUser_ReadGroupIdsByOrganizationUserId.sql b/src/Sql/dbo/Stored Procedures/GroupUser_ReadGroupIdsByOrganizationUserId.sql new file mode 100644 index 0000000000..2f6ed3e530 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/GroupUser_ReadGroupIdsByOrganizationUserId.sql @@ -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 \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/GroupUser_UpdateGroups.sql b/src/Sql/dbo/Stored Procedures/GroupUser_UpdateGroups.sql new file mode 100644 index 0000000000..49479b3edd --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/GroupUser_UpdateGroups.sql @@ -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 \ No newline at end of file diff --git a/src/Sql/dbo/Tables/GroupUser.sql b/src/Sql/dbo/Tables/GroupUser.sql index 747c4d2827..965cc9e606 100644 --- a/src/Sql/dbo/Tables/GroupUser.sql +++ b/src/Sql/dbo/Tables/GroupUser.sql @@ -1,8 +1,8 @@ CREATE TABLE [dbo].[GroupUser] ( [GroupId] UNIQUEIDENTIFIER NOT NULL, - [UserId] UNIQUEIDENTIFIER NOT NULL, - CONSTRAINT [PK_GroupUser] PRIMARY KEY CLUSTERED ([GroupId] ASC, [UserId] ASC), + [OrganizationUserId] UNIQUEIDENTIFIER NOT NULL, + 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_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) + CONSTRAINT [FK_GroupUser_OrganizationUser] FOREIGN KEY ([OrganizationUserId]) REFERENCES [dbo].[OrganizationUser] ([Id]) ); diff --git a/src/Sql/dbo/Views/GroupUserUserDetailsView.sql b/src/Sql/dbo/Views/GroupUserUserDetailsView.sql new file mode 100644 index 0000000000..40e0355eab --- /dev/null +++ b/src/Sql/dbo/Views/GroupUserUserDetailsView.sql @@ -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] \ No newline at end of file