You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
275 lines
11 KiB
275 lines
11 KiB
using Bit.Api.AdminConsole.Models.Request; |
|
using Bit.Api.AdminConsole.Models.Response; |
|
using Bit.Api.Models.Response; |
|
using Bit.Api.Utilities; |
|
using Bit.Api.Vault.AuthorizationHandlers.Collections; |
|
using Bit.Api.Vault.AuthorizationHandlers.Groups; |
|
using Bit.Core; |
|
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; |
|
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Requests; |
|
using Bit.Core.AdminConsole.Repositories; |
|
using Bit.Core.AdminConsole.Services; |
|
using Bit.Core.Context; |
|
using Bit.Core.Exceptions; |
|
using Bit.Core.Repositories; |
|
using Bit.Core.Services; |
|
using Microsoft.AspNetCore.Authorization; |
|
using Microsoft.AspNetCore.Mvc; |
|
|
|
namespace Bit.Api.AdminConsole.Controllers; |
|
|
|
[Route("organizations/{orgId}/groups")] |
|
[Authorize("Application")] |
|
public class GroupsController : Controller |
|
{ |
|
private readonly IGroupRepository _groupRepository; |
|
private readonly IGroupService _groupService; |
|
private readonly IDeleteGroupCommand _deleteGroupCommand; |
|
private readonly IOrganizationRepository _organizationRepository; |
|
private readonly ICurrentContext _currentContext; |
|
private readonly ICreateGroupCommand _createGroupCommand; |
|
private readonly IUpdateGroupCommand _updateGroupCommand; |
|
private readonly IAuthorizationService _authorizationService; |
|
private readonly IApplicationCacheService _applicationCacheService; |
|
private readonly IUserService _userService; |
|
private readonly IFeatureService _featureService; |
|
private readonly IOrganizationUserRepository _organizationUserRepository; |
|
private readonly ICollectionRepository _collectionRepository; |
|
private readonly IGroupDetailsQuery _groupDetailsQuery; |
|
|
|
public GroupsController( |
|
IGroupRepository groupRepository, |
|
IGroupService groupService, |
|
IOrganizationRepository organizationRepository, |
|
ICurrentContext currentContext, |
|
ICreateGroupCommand createGroupCommand, |
|
IUpdateGroupCommand updateGroupCommand, |
|
IDeleteGroupCommand deleteGroupCommand, |
|
IAuthorizationService authorizationService, |
|
IApplicationCacheService applicationCacheService, |
|
IUserService userService, |
|
IFeatureService featureService, |
|
IOrganizationUserRepository organizationUserRepository, |
|
ICollectionRepository collectionRepository, |
|
IGroupDetailsQuery groupDetailsQuery) |
|
{ |
|
_groupRepository = groupRepository; |
|
_groupService = groupService; |
|
_organizationRepository = organizationRepository; |
|
_currentContext = currentContext; |
|
_createGroupCommand = createGroupCommand; |
|
_updateGroupCommand = updateGroupCommand; |
|
_deleteGroupCommand = deleteGroupCommand; |
|
_authorizationService = authorizationService; |
|
_applicationCacheService = applicationCacheService; |
|
_userService = userService; |
|
_featureService = featureService; |
|
_organizationUserRepository = organizationUserRepository; |
|
_collectionRepository = collectionRepository; |
|
_groupDetailsQuery = groupDetailsQuery; |
|
} |
|
|
|
[HttpGet("{id}")] |
|
public async Task<GroupResponseModel> Get(string orgId, string id) |
|
{ |
|
var group = await _groupRepository.GetByIdAsync(new Guid(id)); |
|
if (group == null || !await _currentContext.ManageGroups(group.OrganizationId)) |
|
{ |
|
throw new NotFoundException(); |
|
} |
|
|
|
return new GroupResponseModel(group); |
|
} |
|
|
|
[HttpGet("{id}/details")] |
|
public async Task<GroupDetailsResponseModel> GetDetails(string orgId, string id) |
|
{ |
|
var queryResult = await _groupDetailsQuery.GetGroupDetails(new GroupDetailsQueryRequest { OrganizationId = new Guid(orgId), GroupId = new Guid(id) }); |
|
var detail = queryResult.FirstOrDefault(); |
|
return new GroupDetailsResponseModel(detail.Group, detail.CollectionAccessSelection); |
|
} |
|
|
|
[HttpGet("")] |
|
public async Task<ListResponseModel<GroupDetailsResponseModel>> Get(Guid orgId) |
|
{ |
|
var authorized = |
|
(await _authorizationService.AuthorizeAsync(User, GroupOperations.ReadAll(orgId))).Succeeded; |
|
if (!authorized) |
|
{ |
|
throw new NotFoundException(); |
|
} |
|
|
|
var groups = await _groupDetailsQuery.GetGroupDetails(new GroupDetailsQueryRequest { OrganizationId = orgId }); |
|
var responses = groups.Select(g => new GroupDetailsResponseModel(g.Group, g.CollectionAccessSelection)); |
|
return new ListResponseModel<GroupDetailsResponseModel>(responses); |
|
} |
|
|
|
[HttpGet("{id}/users")] |
|
public async Task<IEnumerable<Guid>> GetUsers(string orgId, string id) |
|
{ |
|
var idGuid = new Guid(id); |
|
var group = await _groupRepository.GetByIdAsync(idGuid); |
|
if (group == null || !await _currentContext.ManageGroups(group.OrganizationId)) |
|
{ |
|
throw new NotFoundException(); |
|
} |
|
|
|
var groupIds = await _groupRepository.GetManyUserIdsByIdAsync(idGuid); |
|
return groupIds; |
|
} |
|
|
|
[HttpPost("")] |
|
public async Task<GroupResponseModel> Post(Guid orgId, [FromBody] GroupRequestModel model) |
|
{ |
|
if (!await _currentContext.ManageGroups(orgId)) |
|
{ |
|
throw new NotFoundException(); |
|
} |
|
|
|
// Flexible Collections - check the user has permission to grant access to the collections for the new group |
|
if (_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1) && model.Collections?.Any() == true) |
|
{ |
|
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Collections.Select(a => a.Id)); |
|
var authorized = |
|
(await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.ModifyGroupAccess)) |
|
.Succeeded; |
|
if (!authorized) |
|
{ |
|
throw new NotFoundException("You are not authorized to grant access to these collections."); |
|
} |
|
} |
|
|
|
var organization = await _organizationRepository.GetByIdAsync(orgId); |
|
var group = model.ToGroup(orgId); |
|
await _createGroupCommand.CreateGroupAsync(group, organization, model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), model.Users); |
|
|
|
return new GroupResponseModel(group); |
|
} |
|
|
|
[HttpPut("{id}")] |
|
[HttpPost("{id}")] |
|
public async Task<GroupResponseModel> Put(Guid orgId, Guid id, [FromBody] GroupRequestModel model) |
|
{ |
|
if (_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1)) |
|
{ |
|
// Use new Flexible Collections v1 logic |
|
return await Put_vNext(orgId, id, model); |
|
} |
|
|
|
// Pre-Flexible Collections v1 logic follows |
|
var group = await _groupRepository.GetByIdAsync(id); |
|
if (group == null || !await _currentContext.ManageGroups(group.OrganizationId)) |
|
{ |
|
throw new NotFoundException(); |
|
} |
|
|
|
var organization = await _organizationRepository.GetByIdAsync(orgId); |
|
|
|
await _updateGroupCommand.UpdateGroupAsync(model.ToGroup(group), organization, |
|
model.Collections.Select(c => c.ToSelectionReadOnly()).ToList(), model.Users); |
|
return new GroupResponseModel(group); |
|
} |
|
|
|
/// <summary> |
|
/// Put logic for Flexible Collections v1 |
|
/// </summary> |
|
private async Task<GroupResponseModel> Put_vNext(Guid orgId, Guid id, [FromBody] GroupRequestModel model) |
|
{ |
|
var (group, currentAccess) = await _groupRepository.GetByIdWithCollectionsAsync(id); |
|
if (group == null || !await _currentContext.ManageGroups(group.OrganizationId)) |
|
{ |
|
throw new NotFoundException(); |
|
} |
|
|
|
// Check whether the user is permitted to add themselves to the group |
|
var orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId); |
|
if (!orgAbility.AllowAdminAccessToAllCollectionItems) |
|
{ |
|
var userId = _userService.GetProperUserId(User).Value; |
|
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(orgId, userId); |
|
var currentGroupUsers = await _groupRepository.GetManyUserIdsByIdAsync(id); |
|
// OrganizationUser may be null if the current user is a provider |
|
if (organizationUser != null && !currentGroupUsers.Contains(organizationUser.Id) && model.Users.Contains(organizationUser.Id)) |
|
{ |
|
throw new BadRequestException("You cannot add yourself to groups."); |
|
} |
|
} |
|
|
|
// The client only sends collections that the saving user has permissions to edit. |
|
// On the server side, we need to (1) confirm this and (2) concat these with the collections that the user |
|
// can't edit before saving to the database. |
|
var currentCollections = await _collectionRepository |
|
.GetManyByManyIdsAsync(currentAccess.Select(cas => cas.Id)); |
|
|
|
var readonlyCollectionIds = new HashSet<Guid>(); |
|
foreach (var collection in currentCollections) |
|
{ |
|
if (!(await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyGroupAccess)) |
|
.Succeeded) |
|
{ |
|
readonlyCollectionIds.Add(collection.Id); |
|
} |
|
} |
|
|
|
if (model.Collections.Any(c => readonlyCollectionIds.Contains(c.Id))) |
|
{ |
|
throw new BadRequestException("You must have Can Manage permissions to edit a collection's membership"); |
|
} |
|
|
|
var editedCollectionAccess = model.Collections |
|
.Select(c => c.ToSelectionReadOnly()); |
|
var readonlyCollectionAccess = currentAccess |
|
.Where(ca => readonlyCollectionIds.Contains(ca.Id)); |
|
var collectionsToSave = editedCollectionAccess |
|
.Concat(readonlyCollectionAccess) |
|
.ToList(); |
|
|
|
var organization = await _organizationRepository.GetByIdAsync(orgId); |
|
|
|
await _updateGroupCommand.UpdateGroupAsync(model.ToGroup(group), organization, collectionsToSave, model.Users); |
|
return new GroupResponseModel(group); |
|
} |
|
|
|
[HttpDelete("{id}")] |
|
[HttpPost("{id}/delete")] |
|
public async Task Delete(string orgId, string id) |
|
{ |
|
var group = await _groupRepository.GetByIdAsync(new Guid(id)); |
|
if (group == null || !await _currentContext.ManageGroups(group.OrganizationId)) |
|
{ |
|
throw new NotFoundException(); |
|
} |
|
|
|
await _deleteGroupCommand.DeleteAsync(group); |
|
} |
|
|
|
[HttpDelete("")] |
|
[HttpPost("delete")] |
|
public async Task BulkDelete([FromBody] GroupBulkRequestModel model) |
|
{ |
|
var groups = await _groupRepository.GetManyByManyIds(model.Ids); |
|
|
|
foreach (var group in groups) |
|
{ |
|
if (!await _currentContext.ManageGroups(group.OrganizationId)) |
|
{ |
|
throw new NotFoundException(); |
|
} |
|
} |
|
|
|
await _deleteGroupCommand.DeleteManyAsync(groups); |
|
} |
|
|
|
[HttpDelete("{id}/user/{orgUserId}")] |
|
[HttpPost("{id}/delete-user/{orgUserId}")] |
|
public async Task Delete(string orgId, string id, string orgUserId) |
|
{ |
|
var group = await _groupRepository.GetByIdAsync(new Guid(id)); |
|
if (group == null || !await _currentContext.ManageGroups(group.OrganizationId)) |
|
{ |
|
throw new NotFoundException(); |
|
} |
|
|
|
await _groupService.DeleteUserAsync(group, new Guid(orgUserId)); |
|
} |
|
}
|
|
|