1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01:00

[AC-2655] Remove old permissions logic from CollectionsController (#4185)

* Replace all old methods with vNext methods

* Remove remaining Flexible Collections checks and remove helper method

* Remove unused private methods

* Update tests
This commit is contained in:
Thomas Rittson 2024-06-18 06:23:32 +10:00 committed by GitHub
parent 3ad4bc1cab
commit c375c18257
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 116 additions and 935 deletions

View File

@ -4,7 +4,6 @@ using Bit.Api.Utilities;
using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;
@ -26,8 +25,6 @@ public class CollectionsController : Controller
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IBulkAddCollectionAccessCommand _bulkAddCollectionAccessCommand; private readonly IBulkAddCollectionAccessCommand _bulkAddCollectionAccessCommand;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IApplicationCacheService _applicationCacheService;
public CollectionsController( public CollectionsController(
ICollectionRepository collectionRepository, ICollectionRepository collectionRepository,
@ -36,511 +33,21 @@ public class CollectionsController : Controller
IUserService userService, IUserService userService,
IAuthorizationService authorizationService, IAuthorizationService authorizationService,
ICurrentContext currentContext, ICurrentContext currentContext,
IBulkAddCollectionAccessCommand bulkAddCollectionAccessCommand, IBulkAddCollectionAccessCommand bulkAddCollectionAccessCommand)
IOrganizationUserRepository organizationUserRepository,
IApplicationCacheService applicationCacheService)
{ {
_collectionRepository = collectionRepository; _collectionRepository = collectionRepository;
_organizationUserRepository = organizationUserRepository;
_collectionService = collectionService; _collectionService = collectionService;
_deleteCollectionCommand = deleteCollectionCommand; _deleteCollectionCommand = deleteCollectionCommand;
_userService = userService; _userService = userService;
_authorizationService = authorizationService; _authorizationService = authorizationService;
_currentContext = currentContext; _currentContext = currentContext;
_bulkAddCollectionAccessCommand = bulkAddCollectionAccessCommand; _bulkAddCollectionAccessCommand = bulkAddCollectionAccessCommand;
_organizationUserRepository = organizationUserRepository;
_applicationCacheService = applicationCacheService;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
public async Task<CollectionResponseModel> Get(Guid orgId, Guid id) public async Task<CollectionResponseModel> Get(Guid orgId, Guid id)
{ {
if (await FlexibleCollectionsIsEnabledAsync(orgId)) var collection = await _collectionRepository.GetByIdAsync(id);
{
// New flexible collections logic
return await Get_vNext(id);
}
// Old pre-flexible collections logic follows
if (!await CanViewCollectionAsync(orgId, id))
{
throw new NotFoundException();
}
var collection = await GetCollectionAsync(id, orgId);
return new CollectionResponseModel(collection);
}
[HttpGet("{id}/details")]
public async Task<CollectionAccessDetailsResponseModel> GetDetails(Guid orgId, Guid id)
{
if (await FlexibleCollectionsIsEnabledAsync(orgId))
{
// New flexible collections logic
return await GetDetails_vNext(id);
}
// Old pre-flexible collections logic follows
if (!await ViewAtLeastOneCollectionAsync(orgId) && !await _currentContext.ManageUsers(orgId))
{
throw new NotFoundException();
}
if (await _currentContext.ViewAllCollections(orgId))
{
(var collection, var access) = await _collectionRepository.GetByIdWithAccessAsync(id);
if (collection == null || collection.OrganizationId != orgId)
{
throw new NotFoundException();
}
return new CollectionAccessDetailsResponseModel(collection, access.Groups, access.Users);
}
else
{
(var collection, var access) = await _collectionRepository.GetByIdWithAccessAsync(id,
_currentContext.UserId.Value, false);
if (collection == null || collection.OrganizationId != orgId)
{
throw new NotFoundException();
}
return new CollectionAccessDetailsResponseModel(collection, access.Groups, access.Users);
}
}
[HttpGet("details")]
public async Task<ListResponseModel<CollectionAccessDetailsResponseModel>> GetManyWithDetails(Guid orgId)
{
if (await FlexibleCollectionsIsEnabledAsync(orgId))
{
// New flexible collections logic
return await GetManyWithDetails_vNext(orgId);
}
// Old pre-flexible collections logic follows
if (!await ViewAtLeastOneCollectionAsync(orgId) && !await _currentContext.ManageUsers(orgId) && !await _currentContext.ManageGroups(orgId))
{
throw new NotFoundException();
}
// We always need to know which collections the current user is assigned to
var assignedOrgCollections =
await _collectionRepository.GetManyByUserIdWithAccessAsync(_currentContext.UserId.Value, orgId,
false);
if (await _currentContext.ViewAllCollections(orgId) || await _currentContext.ManageUsers(orgId))
{
// The user can view all collections, but they may not always be assigned to all of them
var allOrgCollections = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(orgId);
return new ListResponseModel<CollectionAccessDetailsResponseModel>(allOrgCollections.Select(c =>
new CollectionAccessDetailsResponseModel(c.Item1, c.Item2.Groups, c.Item2.Users)
{
// Manually determine which collections they're assigned to
Assigned = assignedOrgCollections.Any(ac => ac.Item1.Id == c.Item1.Id)
})
);
}
return new ListResponseModel<CollectionAccessDetailsResponseModel>(assignedOrgCollections.Select(c =>
new CollectionAccessDetailsResponseModel(c.Item1, c.Item2.Groups, c.Item2.Users)
{
Assigned = true // Mapping from assignedOrgCollections implies they're all assigned
})
);
}
[HttpGet("")]
public async Task<ListResponseModel<CollectionResponseModel>> Get(Guid orgId)
{
if (await FlexibleCollectionsIsEnabledAsync(orgId))
{
// New flexible collections logic
return await GetByOrgId_vNext(orgId);
}
// Old pre-flexible collections logic follows
IEnumerable<Collection> orgCollections = null;
if (await _currentContext.ManageGroups(orgId))
{
// ManageGroups users need to see all collections to manage other users' collection access.
// This is not added to collectionService.GetOrganizationCollectionsAsync as that may have
// unintended consequences on other logic that also uses that method.
// This is a quick fix but it will be properly fixed by permission changes in Flexible Collections.
// Get all collections for organization
orgCollections = await _collectionRepository.GetManyByOrganizationIdAsync(orgId);
}
else
{
// Returns all collections or collections the user is assigned to, depending on permissions
orgCollections = await _collectionService.GetOrganizationCollectionsAsync(orgId);
}
var responses = orgCollections.Select(c => new CollectionResponseModel(c));
return new ListResponseModel<CollectionResponseModel>(responses);
}
[HttpGet("~/collections")]
public async Task<ListResponseModel<CollectionDetailsResponseModel>> GetUser()
{
var collections = await _collectionRepository.GetManyByUserIdAsync(
_userService.GetProperUserId(User).Value, false);
var responses = collections.Select(c => new CollectionDetailsResponseModel(c));
return new ListResponseModel<CollectionDetailsResponseModel>(responses);
}
[HttpGet("{id}/users")]
public async Task<IEnumerable<SelectionReadOnlyResponseModel>> GetUsers(Guid orgId, Guid id)
{
if (await FlexibleCollectionsIsEnabledAsync(orgId))
{
// New flexible collections logic
return await GetUsers_vNext(id);
}
// Old pre-flexible collections logic follows
var collection = await GetCollectionAsync(id, orgId);
var collectionUsers = await _collectionRepository.GetManyUsersByIdAsync(collection.Id);
var responses = collectionUsers.Select(cu => new SelectionReadOnlyResponseModel(cu));
return responses;
}
[HttpPost("")]
public async Task<CollectionResponseModel> Post(Guid orgId, [FromBody] CollectionRequestModel model)
{
if (await FlexibleCollectionsIsEnabledAsync(orgId))
{
// New flexible collections logic
return await Post_vNext(orgId, model);
}
var collection = model.ToCollection(orgId);
var authorized = await CanCreateCollection(orgId, collection.Id) || await CanEditCollectionAsync(orgId, collection.Id);
if (!authorized)
{
throw new NotFoundException();
}
var groups = model.Groups?.Select(g => g.ToSelectionReadOnly());
var users = model.Users?.Select(g => g.ToSelectionReadOnly()).ToList() ?? new List<CollectionAccessSelection>();
// Pre-flexible collections logic assigned Managers to collections they create
var assignUserToCollection =
!await _currentContext.EditAnyCollection(orgId) &&
await _currentContext.EditAssignedCollections(orgId);
var isNewCollection = collection.Id == default;
if (assignUserToCollection && isNewCollection && _currentContext.UserId.HasValue)
{
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(orgId, _currentContext.UserId.Value);
// don't add duplicate access if the user has already specified it themselves
var existingAccess = users.Any(u => u.Id == orgUser.Id);
if (orgUser is { Status: OrganizationUserStatusType.Confirmed } && !existingAccess)
{
users.Add(new CollectionAccessSelection
{
Id = orgUser.Id,
ReadOnly = false
});
}
}
await _collectionService.SaveAsync(collection, groups, users);
if (!_currentContext.UserId.HasValue || await _currentContext.ProviderUserForOrgAsync(orgId))
{
return new CollectionResponseModel(collection);
}
// If we have a user, fetch the collection to get the latest permission details
var userCollectionDetails = await _collectionRepository.GetByIdAsync(collection.Id,
_currentContext.UserId.Value, await FlexibleCollectionsIsEnabledAsync(collection.OrganizationId));
return userCollectionDetails == null
? new CollectionResponseModel(collection)
: new CollectionDetailsResponseModel(userCollectionDetails);
}
[HttpPut("{id}")]
[HttpPost("{id}")]
public async Task<CollectionResponseModel> Put(Guid orgId, Guid id, [FromBody] CollectionRequestModel model)
{
if (await FlexibleCollectionsIsEnabledAsync(orgId))
{
// New flexible collections logic
return await Put_vNext(id, model);
}
// Old pre-flexible collections logic follows
if (!await CanEditCollectionAsync(orgId, id))
{
throw new NotFoundException();
}
var collection = await GetCollectionAsync(id, orgId);
var groups = model.Groups?.Select(g => g.ToSelectionReadOnly());
var users = model.Users?.Select(g => g.ToSelectionReadOnly());
await _collectionService.SaveAsync(model.ToCollection(collection), groups, users);
if (!_currentContext.UserId.HasValue || await _currentContext.ProviderUserForOrgAsync(collection.OrganizationId))
{
return new CollectionResponseModel(collection);
}
// If we have a user, fetch the collection details to get the latest permission details for the user
var updatedCollectionDetails = await _collectionRepository.GetByIdAsync(id, _currentContext.UserId.Value, await FlexibleCollectionsIsEnabledAsync(collection.OrganizationId));
return updatedCollectionDetails == null
? new CollectionResponseModel(collection)
: new CollectionDetailsResponseModel(updatedCollectionDetails);
}
[HttpPut("{id}/users")]
public async Task PutUsers(Guid orgId, Guid id, [FromBody] IEnumerable<SelectionReadOnlyRequestModel> model)
{
if (await FlexibleCollectionsIsEnabledAsync(orgId))
{
// New flexible collections logic
await PutUsers_vNext(id, model);
return;
}
// Old pre-flexible collections logic follows
if (!await CanEditCollectionAsync(orgId, id))
{
throw new NotFoundException();
}
var collection = await GetCollectionAsync(id, orgId);
await _collectionRepository.UpdateUsersAsync(collection.Id, model?.Select(g => g.ToSelectionReadOnly()));
}
[HttpPost("bulk-access")]
public async Task PostBulkCollectionAccess(Guid orgId, [FromBody] BulkCollectionAccessRequestModel model)
{
// Authorization logic assumes flexible collections is enabled
// Remove after all organizations have been migrated
if (!await FlexibleCollectionsIsEnabledAsync(orgId))
{
throw new NotFoundException("Feature disabled.");
}
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.CollectionIds);
if (collections.Count(c => c.OrganizationId == orgId) != model.CollectionIds.Count())
{
throw new NotFoundException("One or more collections not found.");
}
var result = await _authorizationService.AuthorizeAsync(User, collections,
new[] { BulkCollectionOperations.ModifyUserAccess, BulkCollectionOperations.ModifyGroupAccess });
if (!result.Succeeded)
{
throw new NotFoundException();
}
await _bulkAddCollectionAccessCommand.AddAccessAsync(
collections,
model.Users?.Select(u => u.ToSelectionReadOnly()).ToList(),
model.Groups?.Select(g => g.ToSelectionReadOnly()).ToList());
}
[HttpDelete("{id}")]
[HttpPost("{id}/delete")]
public async Task Delete(Guid orgId, Guid id)
{
if (await FlexibleCollectionsIsEnabledAsync(orgId))
{
// New flexible collections logic
await Delete_vNext(id);
return;
}
// Old pre-flexible collections logic follows
if (!await CanDeleteCollectionAsync(orgId, id))
{
throw new NotFoundException();
}
var collection = await GetCollectionAsync(id, orgId);
await _deleteCollectionCommand.DeleteAsync(collection);
}
[HttpDelete("")]
[HttpPost("delete")]
public async Task DeleteMany(Guid orgId, [FromBody] CollectionBulkDeleteRequestModel model)
{
if (await FlexibleCollectionsIsEnabledAsync(orgId))
{
// New flexible collections logic
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Ids);
var result = await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.Delete);
if (!result.Succeeded)
{
throw new NotFoundException();
}
await _deleteCollectionCommand.DeleteManyAsync(collections);
return;
}
// Old pre-flexible collections logic follows
if (!await _currentContext.DeleteAssignedCollections(orgId) && !await DeleteAnyCollection(orgId))
{
throw new NotFoundException();
}
var userCollections = await _collectionService.GetOrganizationCollectionsAsync(orgId);
var filteredCollections = userCollections
.Where(c => model.Ids.Contains(c.Id) && c.OrganizationId == orgId);
if (!filteredCollections.Any())
{
throw new BadRequestException("No collections found.");
}
await _deleteCollectionCommand.DeleteManyAsync(filteredCollections);
}
[HttpDelete("{id}/user/{orgUserId}")]
[HttpPost("{id}/delete-user/{orgUserId}")]
public async Task DeleteUser(Guid orgId, Guid id, Guid orgUserId)
{
if (await FlexibleCollectionsIsEnabledAsync(orgId))
{
// New flexible collections logic
await DeleteUser_vNext(id, orgUserId);
return;
}
// Old pre-flexible collections logic follows
var collection = await GetCollectionAsync(id, orgId);
await _collectionService.DeleteUserAsync(collection, orgUserId);
}
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
private async Task<Collection> GetCollectionAsync(Guid id, Guid orgId)
{
Collection collection = default;
if (await _currentContext.ViewAllCollections(orgId))
{
collection = await _collectionRepository.GetByIdAsync(id);
}
else if (await _currentContext.ViewAssignedCollections(orgId))
{
collection = await _collectionRepository.GetByIdAsync(id, _currentContext.UserId.Value, false);
}
if (collection == null || collection.OrganizationId != orgId)
{
throw new NotFoundException();
}
return collection;
}
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
private async Task<bool> CanCreateCollection(Guid orgId, Guid collectionId)
{
if (collectionId != default)
{
return false;
}
return await _currentContext.OrganizationManager(orgId) || (_currentContext.Organizations?.Any(o => o.Id == orgId &&
(o.Permissions?.CreateNewCollections ?? false)) ?? false);
}
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
private async Task<bool> CanEditCollectionAsync(Guid orgId, Guid collectionId)
{
if (collectionId == default)
{
return false;
}
if (await _currentContext.EditAnyCollection(orgId))
{
return true;
}
if (await _currentContext.EditAssignedCollections(orgId))
{
var collectionDetails =
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value, false);
return collectionDetails != null;
}
return false;
}
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
private async Task<bool> CanDeleteCollectionAsync(Guid orgId, Guid collectionId)
{
if (collectionId == default)
{
return false;
}
if (await DeleteAnyCollection(orgId))
{
return true;
}
if (await _currentContext.DeleteAssignedCollections(orgId))
{
var collectionDetails =
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value, false);
return collectionDetails != null;
}
return false;
}
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
private async Task<bool> DeleteAnyCollection(Guid orgId)
{
return await _currentContext.OrganizationAdmin(orgId) ||
(_currentContext.Organizations?.Any(o => o.Id == orgId
&& (o.Permissions?.DeleteAnyCollection ?? false)) ?? false);
}
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
private async Task<bool> CanViewCollectionAsync(Guid orgId, Guid collectionId)
{
if (collectionId == default)
{
return false;
}
if (await _currentContext.ViewAllCollections(orgId))
{
return true;
}
if (await _currentContext.ViewAssignedCollections(orgId))
{
var collectionDetails =
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value, false);
return collectionDetails != null;
}
return false;
}
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
private async Task<bool> ViewAtLeastOneCollectionAsync(Guid orgId)
{
return await _currentContext.ViewAllCollections(orgId) || await _currentContext.ViewAssignedCollections(orgId);
}
private async Task<CollectionResponseModel> Get_vNext(Guid collectionId)
{
var collection = await _collectionRepository.GetByIdAsync(collectionId);
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Read)).Succeeded; var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Read)).Succeeded;
if (!authorized) if (!authorized)
{ {
@ -550,9 +57,9 @@ public class CollectionsController : Controller
return new CollectionResponseModel(collection); return new CollectionResponseModel(collection);
} }
private async Task<CollectionAccessDetailsResponseModel> GetDetails_vNext(Guid id) [HttpGet("{id}/details")]
public async Task<CollectionAccessDetailsResponseModel> GetDetails(Guid orgId, Guid id)
{ {
// New flexible collections logic
var collectionAdminDetails = var collectionAdminDetails =
await _collectionRepository.GetByIdWithPermissionsAsync(id, _currentContext.UserId, true); await _collectionRepository.GetByIdWithPermissionsAsync(id, _currentContext.UserId, true);
@ -565,7 +72,8 @@ public class CollectionsController : Controller
return new CollectionAccessDetailsResponseModel(collectionAdminDetails); return new CollectionAccessDetailsResponseModel(collectionAdminDetails);
} }
private async Task<ListResponseModel<CollectionAccessDetailsResponseModel>> GetManyWithDetails_vNext(Guid orgId) [HttpGet("details")]
public async Task<ListResponseModel<CollectionAccessDetailsResponseModel>> GetManyWithDetails(Guid orgId)
{ {
var allOrgCollections = await _collectionRepository.GetManyByOrganizationIdWithPermissionsAsync( var allOrgCollections = await _collectionRepository.GetManyByOrganizationIdWithPermissionsAsync(
orgId, _currentContext.UserId.Value, true); orgId, _currentContext.UserId.Value, true);
@ -587,7 +95,8 @@ public class CollectionsController : Controller
)); ));
} }
private async Task<ListResponseModel<CollectionResponseModel>> GetByOrgId_vNext(Guid orgId) [HttpGet("")]
public async Task<ListResponseModel<CollectionResponseModel>> Get(Guid orgId)
{ {
IEnumerable<Collection> orgCollections; IEnumerable<Collection> orgCollections;
@ -606,7 +115,17 @@ public class CollectionsController : Controller
return new ListResponseModel<CollectionResponseModel>(responses); return new ListResponseModel<CollectionResponseModel>(responses);
} }
private async Task<IEnumerable<SelectionReadOnlyResponseModel>> GetUsers_vNext(Guid id) [HttpGet("~/collections")]
public async Task<ListResponseModel<CollectionDetailsResponseModel>> GetUser()
{
var collections = await _collectionRepository.GetManyByUserIdAsync(
_userService.GetProperUserId(User).Value, false);
var responses = collections.Select(c => new CollectionDetailsResponseModel(c));
return new ListResponseModel<CollectionDetailsResponseModel>(responses);
}
[HttpGet("{id}/users")]
public async Task<IEnumerable<SelectionReadOnlyResponseModel>> GetUsers(Guid orgId, Guid id)
{ {
var collection = await _collectionRepository.GetByIdAsync(id); var collection = await _collectionRepository.GetByIdAsync(id);
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ReadAccess)).Succeeded; var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ReadAccess)).Succeeded;
@ -620,7 +139,8 @@ public class CollectionsController : Controller
return responses; return responses;
} }
private async Task<CollectionAccessDetailsResponseModel> Post_vNext(Guid orgId, [FromBody] CollectionRequestModel model) [HttpPost("")]
public async Task<CollectionResponseModel> Post(Guid orgId, [FromBody] CollectionRequestModel model)
{ {
var collection = model.ToCollection(orgId); var collection = model.ToCollection(orgId);
@ -646,7 +166,9 @@ public class CollectionsController : Controller
return new CollectionAccessDetailsResponseModel(collectionWithPermissions); return new CollectionAccessDetailsResponseModel(collectionWithPermissions);
} }
private async Task<CollectionAccessDetailsResponseModel> Put_vNext(Guid id, CollectionRequestModel model) [HttpPut("{id}")]
[HttpPost("{id}")]
public async Task<CollectionResponseModel> Put(Guid orgId, Guid id, [FromBody] CollectionRequestModel model)
{ {
var collection = await _collectionRepository.GetByIdAsync(id); var collection = await _collectionRepository.GetByIdAsync(id);
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Update)).Succeeded; var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Update)).Succeeded;
@ -670,7 +192,8 @@ public class CollectionsController : Controller
return new CollectionAccessDetailsResponseModel(collectionWithPermissions); return new CollectionAccessDetailsResponseModel(collectionWithPermissions);
} }
private async Task PutUsers_vNext(Guid id, IEnumerable<SelectionReadOnlyRequestModel> model) [HttpPut("{id}/users")]
public async Task PutUsers(Guid orgId, Guid id, [FromBody] IEnumerable<SelectionReadOnlyRequestModel> model)
{ {
var collection = await _collectionRepository.GetByIdAsync(id); var collection = await _collectionRepository.GetByIdAsync(id);
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyUserAccess)).Succeeded; var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyUserAccess)).Succeeded;
@ -682,7 +205,32 @@ public class CollectionsController : Controller
await _collectionRepository.UpdateUsersAsync(collection.Id, model?.Select(g => g.ToSelectionReadOnly())); await _collectionRepository.UpdateUsersAsync(collection.Id, model?.Select(g => g.ToSelectionReadOnly()));
} }
private async Task Delete_vNext(Guid id) [HttpPost("bulk-access")]
public async Task PostBulkCollectionAccess(Guid orgId, [FromBody] BulkCollectionAccessRequestModel model)
{
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.CollectionIds);
if (collections.Count(c => c.OrganizationId == orgId) != model.CollectionIds.Count())
{
throw new NotFoundException("One or more collections not found.");
}
var result = await _authorizationService.AuthorizeAsync(User, collections,
new[] { BulkCollectionOperations.ModifyUserAccess, BulkCollectionOperations.ModifyGroupAccess });
if (!result.Succeeded)
{
throw new NotFoundException();
}
await _bulkAddCollectionAccessCommand.AddAccessAsync(
collections,
model.Users?.Select(u => u.ToSelectionReadOnly()).ToList(),
model.Groups?.Select(g => g.ToSelectionReadOnly()).ToList());
}
[HttpDelete("{id}")]
[HttpPost("{id}/delete")]
public async Task Delete(Guid orgId, Guid id)
{ {
var collection = await _collectionRepository.GetByIdAsync(id); var collection = await _collectionRepository.GetByIdAsync(id);
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Delete)).Succeeded; var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Delete)).Succeeded;
@ -694,7 +242,23 @@ public class CollectionsController : Controller
await _deleteCollectionCommand.DeleteAsync(collection); await _deleteCollectionCommand.DeleteAsync(collection);
} }
private async Task DeleteUser_vNext(Guid id, Guid orgUserId) [HttpDelete("")]
[HttpPost("delete")]
public async Task DeleteMany(Guid orgId, [FromBody] CollectionBulkDeleteRequestModel model)
{
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Ids);
var result = await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.Delete);
if (!result.Succeeded)
{
throw new NotFoundException();
}
await _deleteCollectionCommand.DeleteManyAsync(collections);
}
[HttpDelete("{id}/user/{orgUserId}")]
[HttpPost("{id}/delete-user/{orgUserId}")]
public async Task DeleteUser(Guid orgId, Guid id, Guid orgUserId)
{ {
var collection = await _collectionRepository.GetByIdAsync(id); var collection = await _collectionRepository.GetByIdAsync(id);
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyUserAccess)).Succeeded; var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyUserAccess)).Succeeded;
@ -705,10 +269,4 @@ public class CollectionsController : Controller
await _collectionService.DeleteUserAsync(collection, orgUserId); await _collectionService.DeleteUserAsync(collection, orgUserId);
} }
private async Task<bool> FlexibleCollectionsIsEnabledAsync(Guid organizationId)
{
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
return organizationAbility?.FlexibleCollections ?? false;
}
} }

View File

@ -2,11 +2,11 @@
using Bit.Api.Controllers; using Bit.Api.Controllers;
using Bit.Api.Models.Request; using Bit.Api.Models.Request;
using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -23,14 +23,12 @@ namespace Bit.Api.Test.Controllers;
public class CollectionsControllerTests public class CollectionsControllerTests
{ {
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task Post_Success(OrganizationAbility organizationAbility, CollectionRequestModel collectionRequest, public async Task Post_Success(Organization organization, CollectionRequestModel collectionRequest,
SutProvider<CollectionsController> sutProvider) SutProvider<CollectionsController> sutProvider)
{ {
ArrangeOrganizationAbility(sutProvider, organizationAbility);
Collection ExpectedCollection() => Arg.Is<Collection>(c => Collection ExpectedCollection() => Arg.Is<Collection>(c =>
c.Name == collectionRequest.Name && c.ExternalId == collectionRequest.ExternalId && c.Name == collectionRequest.Name && c.ExternalId == collectionRequest.ExternalId &&
c.OrganizationId == organizationAbility.Id); c.OrganizationId == organization.Id);
sutProvider.GetDependency<IAuthorizationService>() sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
@ -38,7 +36,7 @@ public class CollectionsControllerTests
Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Create))) Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Create)))
.Returns(AuthorizationResult.Success()); .Returns(AuthorizationResult.Success());
_ = await sutProvider.Sut.Post(organizationAbility.Id, collectionRequest); _ = await sutProvider.Sut.Post(organization.Id, collectionRequest);
await sutProvider.GetDependency<ICollectionService>() await sutProvider.GetDependency<ICollectionService>()
.Received(1) .Received(1)
@ -48,11 +46,8 @@ public class CollectionsControllerTests
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task Put_Success(Collection collection, CollectionRequestModel collectionRequest, public async Task Put_Success(Collection collection, CollectionRequestModel collectionRequest,
SutProvider<CollectionsController> sutProvider, OrganizationAbility organizationAbility) SutProvider<CollectionsController> sutProvider)
{ {
ArrangeOrganizationAbility(sutProvider, organizationAbility);
collection.OrganizationId = organizationAbility.Id;
Collection ExpectedCollection() => Arg.Is<Collection>(c => c.Id == collection.Id && Collection ExpectedCollection() => Arg.Is<Collection>(c => c.Id == collection.Id &&
c.Name == collectionRequest.Name && c.ExternalId == collectionRequest.ExternalId && c.Name == collectionRequest.Name && c.ExternalId == collectionRequest.ExternalId &&
c.OrganizationId == collection.OrganizationId); c.OrganizationId == collection.OrganizationId);
@ -77,11 +72,8 @@ public class CollectionsControllerTests
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task Put_WithNoCollectionPermission_ThrowsNotFound(Collection collection, CollectionRequestModel collectionRequest, public async Task Put_WithNoCollectionPermission_ThrowsNotFound(Collection collection, CollectionRequestModel collectionRequest,
SutProvider<CollectionsController> sutProvider, OrganizationAbility organizationAbility) SutProvider<CollectionsController> sutProvider)
{ {
ArrangeOrganizationAbility(sutProvider, organizationAbility);
collection.OrganizationId = organizationAbility.Id;
sutProvider.GetDependency<IAuthorizationService>() sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
collection, collection,
@ -96,11 +88,9 @@ public class CollectionsControllerTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetOrganizationCollectionsWithGroups_WithReadAllPermissions_GetsAllCollections(OrganizationAbility organizationAbility, public async Task GetOrganizationCollectionsWithGroups_WithReadAllPermissions_GetsAllCollections(Organization organization,
Guid userId, SutProvider<CollectionsController> sutProvider) Guid userId, SutProvider<CollectionsController> sutProvider)
{ {
ArrangeOrganizationAbility(sutProvider, organizationAbility);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<IAuthorizationService>() sutProvider.GetDependency<IAuthorizationService>()
@ -110,20 +100,20 @@ public class CollectionsControllerTests
Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements => Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements =>
requirements.Cast<CollectionOperationRequirement>().All(operation => requirements.Cast<CollectionOperationRequirement>().All(operation =>
operation.Name == nameof(CollectionOperations.ReadAllWithAccess) operation.Name == nameof(CollectionOperations.ReadAllWithAccess)
&& operation.OrganizationId == organizationAbility.Id))) && operation.OrganizationId == organization.Id)))
.Returns(AuthorizationResult.Success()); .Returns(AuthorizationResult.Success());
await sutProvider.Sut.GetManyWithDetails(organizationAbility.Id); await sutProvider.Sut.GetManyWithDetails(organization.Id);
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdWithPermissionsAsync(organizationAbility.Id, userId, true); await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdWithPermissionsAsync(organization.Id, userId, true);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetOrganizationCollectionsWithGroups_MissingReadAllPermissions_GetsAssignedCollections( public async Task GetOrganizationCollectionsWithGroups_MissingReadAllPermissions_GetsAssignedCollections(
OrganizationAbility organizationAbility, Guid userId, SutProvider<CollectionsController> sutProvider, List<CollectionAdminDetails> collections) Organization organization, Guid userId, SutProvider<CollectionsController> sutProvider,
List<CollectionAdminDetails> collections)
{ {
ArrangeOrganizationAbility(sutProvider, organizationAbility); collections.ForEach(c => c.OrganizationId = organization.Id);
collections.ForEach(c => c.OrganizationId = organizationAbility.Id);
collections.ForEach(c => c.Manage = false); collections.ForEach(c => c.Manage = false);
var managedCollection = collections.First(); var managedCollection = collections.First();
@ -138,7 +128,7 @@ public class CollectionsControllerTests
Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements => Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements =>
requirements.Cast<CollectionOperationRequirement>().All(operation => requirements.Cast<CollectionOperationRequirement>().All(operation =>
operation.Name == nameof(CollectionOperations.ReadAllWithAccess) operation.Name == nameof(CollectionOperations.ReadAllWithAccess)
&& operation.OrganizationId == organizationAbility.Id))) && operation.OrganizationId == organization.Id)))
.Returns(AuthorizationResult.Failed()); .Returns(AuthorizationResult.Failed());
sutProvider.GetDependency<IAuthorizationService>() sutProvider.GetDependency<IAuthorizationService>()
@ -151,23 +141,23 @@ public class CollectionsControllerTests
.Returns(AuthorizationResult.Success()); .Returns(AuthorizationResult.Success());
sutProvider.GetDependency<ICollectionRepository>() sutProvider.GetDependency<ICollectionRepository>()
.GetManyByOrganizationIdWithPermissionsAsync(organizationAbility.Id, userId, true) .GetManyByOrganizationIdWithPermissionsAsync(organization.Id, userId, true)
.Returns(collections); .Returns(collections);
var response = await sutProvider.Sut.GetManyWithDetails(organizationAbility.Id); var response = await sutProvider.Sut.GetManyWithDetails(organization.Id);
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdWithPermissionsAsync(organizationAbility.Id, userId, true); await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdWithPermissionsAsync(organization.Id, userId, true);
Assert.Single(response.Data); Assert.Single(response.Data);
Assert.All(response.Data, c => Assert.Equal(organizationAbility.Id, c.OrganizationId)); Assert.All(response.Data, c => Assert.Equal(organization.Id, c.OrganizationId));
Assert.All(response.Data, c => Assert.Equal(managedCollection.Id, c.Id)); Assert.All(response.Data, c => Assert.Equal(managedCollection.Id, c.Id));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetOrganizationCollections_WithReadAllPermissions_GetsAllCollections( public async Task GetOrganizationCollections_WithReadAllPermissions_GetsAllCollections(
OrganizationAbility organizationAbility, List<Collection> collections, Guid userId, SutProvider<CollectionsController> sutProvider) Organization organization, List<Collection> collections, Guid userId,
SutProvider<CollectionsController> sutProvider)
{ {
ArrangeOrganizationAbility(sutProvider, organizationAbility); collections.ForEach(c => c.OrganizationId = organization.Id);
collections.ForEach(c => c.OrganizationId = organizationAbility.Id);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
@ -178,26 +168,25 @@ public class CollectionsControllerTests
Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements => Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements =>
requirements.Cast<CollectionOperationRequirement>().All(operation => requirements.Cast<CollectionOperationRequirement>().All(operation =>
operation.Name == nameof(CollectionOperations.ReadAll) operation.Name == nameof(CollectionOperations.ReadAll)
&& operation.OrganizationId == organizationAbility.Id))) && operation.OrganizationId == organization.Id)))
.Returns(AuthorizationResult.Success()); .Returns(AuthorizationResult.Success());
sutProvider.GetDependency<ICollectionRepository>() sutProvider.GetDependency<ICollectionRepository>()
.GetManyByOrganizationIdAsync(organizationAbility.Id) .GetManyByOrganizationIdAsync(organization.Id)
.Returns(collections); .Returns(collections);
var response = await sutProvider.Sut.Get(organizationAbility.Id); var response = await sutProvider.Sut.Get(organization.Id);
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdAsync(organizationAbility.Id); await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdAsync(organization.Id);
Assert.Equal(collections.Count, response.Data.Count()); Assert.Equal(collections.Count, response.Data.Count());
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetOrganizationCollections_MissingReadAllPermissions_GetsManageableCollections( public async Task GetOrganizationCollections_MissingReadAllPermissions_GetsManageableCollections(
OrganizationAbility organizationAbility, List<CollectionDetails> collections, Guid userId, SutProvider<CollectionsController> sutProvider) Organization organization, List<CollectionDetails> collections, Guid userId, SutProvider<CollectionsController> sutProvider)
{ {
ArrangeOrganizationAbility(sutProvider, organizationAbility); collections.ForEach(c => c.OrganizationId = organization.Id);
collections.ForEach(c => c.OrganizationId = organizationAbility.Id);
collections.ForEach(c => c.Manage = false); collections.ForEach(c => c.Manage = false);
var managedCollection = collections.First(); var managedCollection = collections.First();
@ -212,7 +201,7 @@ public class CollectionsControllerTests
Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements => Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements =>
requirements.Cast<CollectionOperationRequirement>().All(operation => requirements.Cast<CollectionOperationRequirement>().All(operation =>
operation.Name == nameof(CollectionOperations.ReadAll) operation.Name == nameof(CollectionOperations.ReadAll)
&& operation.OrganizationId == organizationAbility.Id))) && operation.OrganizationId == organization.Id)))
.Returns(AuthorizationResult.Failed()); .Returns(AuthorizationResult.Failed());
sutProvider.GetDependency<IAuthorizationService>() sutProvider.GetDependency<IAuthorizationService>()
@ -228,24 +217,22 @@ public class CollectionsControllerTests
.GetManyByUserIdAsync(userId, false) .GetManyByUserIdAsync(userId, false)
.Returns(collections); .Returns(collections);
var result = await sutProvider.Sut.Get(organizationAbility.Id); var result = await sutProvider.Sut.Get(organization.Id);
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceive().GetManyByOrganizationIdAsync(organizationAbility.Id); await sutProvider.GetDependency<ICollectionRepository>().DidNotReceive().GetManyByOrganizationIdAsync(organization.Id);
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdAsync(userId, false); await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdAsync(userId, false);
Assert.Single(result.Data); Assert.Single(result.Data);
Assert.All(result.Data, c => Assert.Equal(organizationAbility.Id, c.OrganizationId)); Assert.All(result.Data, c => Assert.Equal(organization.Id, c.OrganizationId));
Assert.All(result.Data, c => Assert.Equal(managedCollection.Id, c.Id)); Assert.All(result.Data, c => Assert.Equal(managedCollection.Id, c.Id));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task DeleteMany_Success(OrganizationAbility organizationAbility, Collection collection1, Collection collection2, public async Task DeleteMany_Success(Organization organization, Collection collection1, Collection collection2,
SutProvider<CollectionsController> sutProvider) SutProvider<CollectionsController> sutProvider)
{ {
// Arrange // Arrange
var orgId = organizationAbility.Id; var orgId = organization.Id;
ArrangeOrganizationAbility(sutProvider, organizationAbility);
var model = new CollectionBulkDeleteRequestModel var model = new CollectionBulkDeleteRequestModel
{ {
Ids = new[] { collection1.Id, collection2.Id } Ids = new[] { collection1.Id, collection2.Id }
@ -285,13 +272,11 @@ public class CollectionsControllerTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task DeleteMany_PermissionDenied_ThrowsNotFound(OrganizationAbility organizationAbility, Collection collection1, public async Task DeleteMany_PermissionDenied_ThrowsNotFound(Organization organization, Collection collection1,
Collection collection2, SutProvider<CollectionsController> sutProvider) Collection collection2, SutProvider<CollectionsController> sutProvider)
{ {
// Arrange // Arrange
var orgId = organizationAbility.Id; var orgId = organization.Id;
ArrangeOrganizationAbility(sutProvider, organizationAbility);
var model = new CollectionBulkDeleteRequestModel var model = new CollectionBulkDeleteRequestModel
{ {
Ids = new[] { collection1.Id, collection2.Id } Ids = new[] { collection1.Id, collection2.Id }
@ -331,12 +316,10 @@ public class CollectionsControllerTests
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task PostBulkCollectionAccess_Success(User actingUser, List<Collection> collections, public async Task PostBulkCollectionAccess_Success(User actingUser, List<Collection> collections,
OrganizationAbility organizationAbility, SutProvider<CollectionsController> sutProvider) Organization organization, SutProvider<CollectionsController> sutProvider)
{ {
// Arrange // Arrange
ArrangeOrganizationAbility(sutProvider, organizationAbility); collections.ForEach(c => c.OrganizationId = organization.Id);
collections.ForEach(c => c.OrganizationId = organizationAbility.Id);
var userId = Guid.NewGuid(); var userId = Guid.NewGuid();
var groupId = Guid.NewGuid(); var groupId = Guid.NewGuid();
var model = new BulkCollectionAccessRequestModel var model = new BulkCollectionAccessRequestModel
@ -365,7 +348,7 @@ public class CollectionsControllerTests
IEnumerable<Collection> ExpectedCollectionAccess() => Arg.Is<IEnumerable<Collection>>(cols => cols.SequenceEqual(collections)); IEnumerable<Collection> ExpectedCollectionAccess() => Arg.Is<IEnumerable<Collection>>(cols => cols.SequenceEqual(collections));
// Act // Act
await sutProvider.Sut.PostBulkCollectionAccess(organizationAbility.Id, model); await sutProvider.Sut.PostBulkCollectionAccess(organization.Id, model);
// Assert // Assert
await sutProvider.GetDependency<IAuthorizationService>().Received().AuthorizeAsync( await sutProvider.GetDependency<IAuthorizationService>().Received().AuthorizeAsync(
@ -384,11 +367,10 @@ public class CollectionsControllerTests
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task PostBulkCollectionAccess_CollectionsNotFound_Throws(User actingUser, public async Task PostBulkCollectionAccess_CollectionsNotFound_Throws(User actingUser,
OrganizationAbility organizationAbility, List<Collection> collections, Organization organization, List<Collection> collections,
SutProvider<CollectionsController> sutProvider) SutProvider<CollectionsController> sutProvider)
{ {
ArrangeOrganizationAbility(sutProvider, organizationAbility); collections.ForEach(c => c.OrganizationId = organization.Id);
collections.ForEach(c => c.OrganizationId = organizationAbility.Id);
var userId = Guid.NewGuid(); var userId = Guid.NewGuid();
var groupId = Guid.NewGuid(); var groupId = Guid.NewGuid();
@ -407,7 +389,7 @@ public class CollectionsControllerTests
.Returns(collections.Skip(1).ToList()); .Returns(collections.Skip(1).ToList());
var exception = await Assert.ThrowsAsync<NotFoundException>( var exception = await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.PostBulkCollectionAccess(organizationAbility.Id, model)); () => sutProvider.Sut.PostBulkCollectionAccess(organization.Id, model));
Assert.Equal("One or more collections not found.", exception.Message); Assert.Equal("One or more collections not found.", exception.Message);
await sutProvider.GetDependency<IAuthorizationService>().DidNotReceiveWithAnyArgs().AuthorizeAsync( await sutProvider.GetDependency<IAuthorizationService>().DidNotReceiveWithAnyArgs().AuthorizeAsync(
@ -421,13 +403,11 @@ public class CollectionsControllerTests
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task PostBulkCollectionAccess_CollectionsBelongToDifferentOrganizations_Throws(User actingUser, public async Task PostBulkCollectionAccess_CollectionsBelongToDifferentOrganizations_Throws(User actingUser,
OrganizationAbility organizationAbility, List<Collection> collections, Organization organization, List<Collection> collections,
SutProvider<CollectionsController> sutProvider) SutProvider<CollectionsController> sutProvider)
{ {
ArrangeOrganizationAbility(sutProvider, organizationAbility);
// First collection has a different orgId // First collection has a different orgId
collections.Skip(1).ToList().ForEach(c => c.OrganizationId = organizationAbility.Id); collections.Skip(1).ToList().ForEach(c => c.OrganizationId = organization.Id);
var userId = Guid.NewGuid(); var userId = Guid.NewGuid();
var groupId = Guid.NewGuid(); var groupId = Guid.NewGuid();
@ -446,7 +426,7 @@ public class CollectionsControllerTests
.Returns(collections); .Returns(collections);
var exception = await Assert.ThrowsAsync<NotFoundException>( var exception = await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.PostBulkCollectionAccess(organizationAbility.Id, model)); () => sutProvider.Sut.PostBulkCollectionAccess(organization.Id, model));
Assert.Equal("One or more collections not found.", exception.Message); Assert.Equal("One or more collections not found.", exception.Message);
await sutProvider.GetDependency<IAuthorizationService>().DidNotReceiveWithAnyArgs().AuthorizeAsync( await sutProvider.GetDependency<IAuthorizationService>().DidNotReceiveWithAnyArgs().AuthorizeAsync(
@ -458,42 +438,11 @@ public class CollectionsControllerTests
.AddAccessAsync(default, default, default); .AddAccessAsync(default, default, default);
} }
[Theory, BitAutoData]
public async Task PostBulkCollectionAccess_FlexibleCollectionsDisabled_Throws(OrganizationAbility organizationAbility, List<Collection> collections,
SutProvider<CollectionsController> sutProvider)
{
organizationAbility.FlexibleCollections = false;
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
.Returns(organizationAbility);
var userId = Guid.NewGuid();
var groupId = Guid.NewGuid();
var model = new BulkCollectionAccessRequestModel
{
CollectionIds = collections.Select(c => c.Id),
Users = new[] { new SelectionReadOnlyRequestModel { Id = userId, Manage = true } },
Groups = new[] { new SelectionReadOnlyRequestModel { Id = groupId, ReadOnly = true } },
};
var exception = await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.PostBulkCollectionAccess(organizationAbility.Id, model));
Assert.Equal("Feature disabled.", exception.Message);
await sutProvider.GetDependency<IAuthorizationService>().DidNotReceiveWithAnyArgs().AuthorizeAsync(
Arg.Any<ClaimsPrincipal>(),
Arg.Any<IEnumerable<Collection>>(),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()
);
await sutProvider.GetDependency<IBulkAddCollectionAccessCommand>().DidNotReceiveWithAnyArgs()
.AddAccessAsync(default, default, default);
}
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task PostBulkCollectionAccess_AccessDenied_Throws(User actingUser, List<Collection> collections, public async Task PostBulkCollectionAccess_AccessDenied_Throws(User actingUser, List<Collection> collections,
OrganizationAbility organizationAbility, SutProvider<CollectionsController> sutProvider) Organization organization, SutProvider<CollectionsController> sutProvider)
{ {
ArrangeOrganizationAbility(sutProvider, organizationAbility); collections.ForEach(c => c.OrganizationId = organization.Id);
collections.ForEach(c => c.OrganizationId = organizationAbility.Id);
var userId = Guid.NewGuid(); var userId = Guid.NewGuid();
var groupId = Guid.NewGuid(); var groupId = Guid.NewGuid();
@ -522,7 +471,7 @@ public class CollectionsControllerTests
IEnumerable<Collection> ExpectedCollectionAccess() => Arg.Is<IEnumerable<Collection>>(cols => cols.SequenceEqual(collections)); IEnumerable<Collection> ExpectedCollectionAccess() => Arg.Is<IEnumerable<Collection>>(cols => cols.SequenceEqual(collections));
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PostBulkCollectionAccess(organizationAbility.Id, model)); await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PostBulkCollectionAccess(organization.Id, model));
await sutProvider.GetDependency<IAuthorizationService>().Received().AuthorizeAsync( await sutProvider.GetDependency<IAuthorizationService>().Received().AuthorizeAsync(
Arg.Any<ClaimsPrincipal>(), Arg.Any<ClaimsPrincipal>(),
ExpectedCollectionAccess(), ExpectedCollectionAccess(),
@ -534,12 +483,4 @@ public class CollectionsControllerTests
await sutProvider.GetDependency<IBulkAddCollectionAccessCommand>().DidNotReceiveWithAnyArgs() await sutProvider.GetDependency<IBulkAddCollectionAccessCommand>().DidNotReceiveWithAnyArgs()
.AddAccessAsync(default, default, default); .AddAccessAsync(default, default, default);
} }
private void ArrangeOrganizationAbility(SutProvider<CollectionsController> sutProvider, OrganizationAbility organizationAbility)
{
organizationAbility.FlexibleCollections = true;
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
.Returns(organizationAbility);
}
} }

View File

@ -1,318 +0,0 @@
using Bit.Api.Controllers;
using Bit.Api.Models.Request;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using Collection = Bit.Core.Entities.Collection;
using User = Bit.Core.Entities.User;
namespace Bit.Api.Test.Controllers;
/// <summary>
/// CollectionsController tests that use pre-Flexible Collections logic. To be removed when the feature flag is removed.
/// Note the feature flag defaults to OFF so it is not explicitly set in these tests.
/// </summary>
[ControllerCustomize(typeof(CollectionsController))]
[SutProviderCustomize]
public class LegacyCollectionsControllerTests
{
[Theory, BitAutoData]
public async Task Post_Manager_AssignsToCollection_Success(Guid orgId, OrganizationUser orgUser, SutProvider<CollectionsController> sutProvider)
{
orgUser.Type = OrganizationUserType.Manager;
orgUser.Status = OrganizationUserStatusType.Confirmed;
sutProvider.GetDependency<ICurrentContext>()
.OrganizationManager(orgId)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>()
.EditAnyCollection(orgId)
.Returns(false);
sutProvider.GetDependency<ICurrentContext>()
.EditAssignedCollections(orgId)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>().UserId = orgUser.UserId;
sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(orgId, orgUser.UserId.Value)
.Returns(orgUser);
sutProvider.GetDependency<ICollectionRepository>()
.GetByIdAsync(Arg.Any<Guid>(), orgUser.UserId.Value, Arg.Any<bool>())
.Returns(new CollectionDetails());
var collectionRequest = new CollectionRequestModel
{
Name = "encrypted_string",
ExternalId = "my_external_id"
};
_ = await sutProvider.Sut.Post(orgId, collectionRequest);
var test = sutProvider.GetDependency<ICollectionService>().ReceivedCalls();
await sutProvider.GetDependency<ICollectionService>()
.Received(1)
.SaveAsync(Arg.Any<Collection>(), Arg.Any<IEnumerable<CollectionAccessSelection>>(),
Arg.Is<IEnumerable<CollectionAccessSelection>>(users => users.Any(u => u.Id == orgUser.Id && !u.ReadOnly && !u.HidePasswords && !u.Manage)));
}
[Theory, BitAutoData]
public async Task Post_Owner_DoesNotAssignToCollection_Success(Guid orgId, OrganizationUser orgUser, SutProvider<CollectionsController> sutProvider)
{
orgUser.Type = OrganizationUserType.Owner;
orgUser.Status = OrganizationUserStatusType.Confirmed;
sutProvider.GetDependency<ICurrentContext>()
.OrganizationManager(orgId)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>()
.EditAnyCollection(orgId)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>()
.EditAssignedCollections(orgId)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>().UserId = orgUser.UserId;
sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(orgId, orgUser.UserId.Value)
.Returns(orgUser);
sutProvider.GetDependency<ICollectionRepository>()
.GetByIdAsync(Arg.Any<Guid>(), orgUser.UserId.Value, Arg.Any<bool>())
.Returns(new CollectionDetails());
var collectionRequest = new CollectionRequestModel
{
Name = "encrypted_string",
ExternalId = "my_external_id"
};
_ = await sutProvider.Sut.Post(orgId, collectionRequest);
var test = sutProvider.GetDependency<ICollectionService>().ReceivedCalls();
await sutProvider.GetDependency<ICollectionService>()
.Received(1)
.SaveAsync(Arg.Any<Collection>(), Arg.Any<IEnumerable<CollectionAccessSelection>>(),
Arg.Is<IEnumerable<CollectionAccessSelection>>(users => !users.Any()));
}
[Theory, BitAutoData]
public async Task Put_Success(Guid orgId, Guid collectionId, Guid userId, CollectionRequestModel collectionRequest,
SutProvider<CollectionsController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>()
.ViewAssignedCollections(orgId)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>()
.EditAssignedCollections(orgId)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>()
.UserId
.Returns(userId);
sutProvider.GetDependency<ICollectionRepository>()
.GetByIdAsync(collectionId, userId, Arg.Any<bool>())
.Returns(new CollectionDetails
{
OrganizationId = orgId,
});
_ = await sutProvider.Sut.Put(orgId, collectionId, collectionRequest);
}
[Theory, BitAutoData]
public async Task Put_CanNotEditAssignedCollection_ThrowsNotFound(Guid orgId, Guid collectionId, Guid userId, CollectionRequestModel collectionRequest,
SutProvider<CollectionsController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>()
.EditAssignedCollections(orgId)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>()
.UserId
.Returns(userId);
sutProvider.GetDependency<ICollectionRepository>()
.GetByIdAsync(collectionId, userId, Arg.Any<bool>())
.Returns(Task.FromResult<CollectionDetails>(null));
_ = await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.Put(orgId, collectionId, collectionRequest));
}
[Theory, BitAutoData]
public async Task GetOrganizationCollectionsWithGroups_NoManagerPermissions_ThrowsNotFound(Organization organization, SutProvider<CollectionsController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ViewAssignedCollections(organization.Id).Returns(false);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetManyWithDetails(organization.Id));
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs().GetManyByOrganizationIdWithAccessAsync(default);
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs().GetManyByUserIdWithAccessAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task GetOrganizationCollectionsWithGroups_AdminPermissions_GetsAllCollections(Organization organization, User user, SutProvider<CollectionsController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(user.Id);
sutProvider.GetDependency<ICurrentContext>().ViewAllCollections(organization.Id).Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organization.Id).Returns(true);
await sutProvider.Sut.GetManyWithDetails(organization.Id);
await sutProvider.GetDependency<ICollectionRepository>().Received().GetManyByOrganizationIdWithAccessAsync(organization.Id);
await sutProvider.GetDependency<ICollectionRepository>().Received().GetManyByUserIdWithAccessAsync(user.Id, organization.Id, Arg.Any<bool>());
}
[Theory, BitAutoData]
public async Task GetOrganizationCollectionsWithGroups_MissingViewAllPermissions_GetsAssignedCollections(Organization organization, User user, SutProvider<CollectionsController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(user.Id);
sutProvider.GetDependency<ICurrentContext>().ViewAssignedCollections(organization.Id).Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationManager(organization.Id).Returns(true);
await sutProvider.Sut.GetManyWithDetails(organization.Id);
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs().GetManyByOrganizationIdWithAccessAsync(default);
await sutProvider.GetDependency<ICollectionRepository>().Received().GetManyByUserIdWithAccessAsync(user.Id, organization.Id, Arg.Any<bool>());
}
[Theory, BitAutoData]
public async Task GetOrganizationCollectionsWithGroups_CustomUserWithManagerPermissions_GetsAssignedCollections(Organization organization, User user, SutProvider<CollectionsController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(user.Id);
sutProvider.GetDependency<ICurrentContext>().ViewAssignedCollections(organization.Id).Returns(true);
sutProvider.GetDependency<ICurrentContext>().EditAssignedCollections(organization.Id).Returns(true);
await sutProvider.Sut.GetManyWithDetails(organization.Id);
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs().GetManyByOrganizationIdWithAccessAsync(default);
await sutProvider.GetDependency<ICollectionRepository>().Received().GetManyByUserIdWithAccessAsync(user.Id, organization.Id, Arg.Any<bool>());
}
[Theory, BitAutoData]
public async Task DeleteMany_Success(Guid orgId, User user, Collection collection1, Collection collection2, SutProvider<CollectionsController> sutProvider)
{
// Arrange
var model = new CollectionBulkDeleteRequestModel
{
Ids = new[] { collection1.Id, collection2.Id },
};
var collections = new List<Collection>
{
new CollectionDetails
{
Id = collection1.Id,
OrganizationId = orgId,
},
new CollectionDetails
{
Id = collection2.Id,
OrganizationId = orgId,
},
};
sutProvider.GetDependency<ICurrentContext>()
.DeleteAssignedCollections(orgId)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>()
.UserId
.Returns(user.Id);
sutProvider.GetDependency<ICollectionService>()
.GetOrganizationCollectionsAsync(orgId)
.Returns(collections);
// Act
await sutProvider.Sut.DeleteMany(orgId, model);
// Assert
await sutProvider.GetDependency<IDeleteCollectionCommand>()
.Received(1)
.DeleteManyAsync(Arg.Is<IEnumerable<Collection>>(coll => coll.Select(c => c.Id).SequenceEqual(collections.Select(c => c.Id))));
}
[Theory, BitAutoData]
public async Task DeleteMany_CanNotDeleteAssignedCollection_ThrowsNotFound(Guid orgId, Collection collection1, Collection collection2, SutProvider<CollectionsController> sutProvider)
{
// Arrange
var model = new CollectionBulkDeleteRequestModel
{
Ids = new[] { collection1.Id, collection2.Id },
};
sutProvider.GetDependency<ICurrentContext>()
.DeleteAssignedCollections(orgId)
.Returns(false);
// Assert
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.DeleteMany(orgId, model));
await sutProvider.GetDependency<IDeleteCollectionCommand>()
.DidNotReceiveWithAnyArgs()
.DeleteManyAsync((IEnumerable<Collection>)default);
}
[Theory, BitAutoData]
public async Task DeleteMany_UserCanNotAccessCollections_FiltersOutInvalid(Guid orgId, User user, Collection collection1, Collection collection2, SutProvider<CollectionsController> sutProvider)
{
// Arrange
var model = new CollectionBulkDeleteRequestModel
{
Ids = new[] { collection1.Id, collection2.Id },
};
var collections = new List<Collection>
{
new CollectionDetails
{
Id = collection2.Id,
OrganizationId = orgId,
},
};
sutProvider.GetDependency<ICurrentContext>()
.DeleteAssignedCollections(orgId)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>()
.UserId
.Returns(user.Id);
sutProvider.GetDependency<ICollectionService>()
.GetOrganizationCollectionsAsync(orgId)
.Returns(collections);
// Act
await sutProvider.Sut.DeleteMany(orgId, model);
// Assert
await sutProvider.GetDependency<IDeleteCollectionCommand>()
.Received(1)
.DeleteManyAsync(Arg.Is<IEnumerable<Collection>>(coll => coll.Select(c => c.Id).SequenceEqual(collections.Select(c => c.Id))));
}
}