mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 07:36:14 -05:00
[AC-1373] Flexible Collections (#3245)
* [AC-1117] Add manage permission (#3126) * Update sql files to add Manage permission * Add migration script * Rename collection manage migration file to remove duplicate migration date * Migrations * Add manage to models * Add manage to repository * Add constraint to Manage columns * Migration lint fixes * Add manage to OrganizationUserUserDetails_ReadWithCollectionsById * Add missing manage fields * Add 'Manage' to UserCollectionDetails * Use CREATE OR ALTER where possible * [AC-1374] Limit collection creation/deletion to Owner/Admin (#3145) * feat: update org table with new column, write migration, refs AC-1374 * feat: update views with new column, refs AC-1374 * feat: Alter sprocs (org create/update) to include new column, refs AC-1374 * feat: update entity/data/request/response models to handle new column, refs AC-1374 * feat: update necessary Provider related views during migration, refs AC-1374 * fix: update org create to default new column to false, refs AC-1374 * feat: added new API/request model for collection management and removed property from update request model, refs AC-1374 * fix: renamed migration script to be after secrets manage beta column changes, refs AC-1374 * fix: dotnet format, refs AC-1374 * feat: add ef migrations to reflect mssql changes, refs AC-1374 * fix: dotnet format, refs AC-1374 * feat: update API signature to accept Guid and explain Cd verbiage, refs AC-1374 * fix: merge conflict resolution * [AC-1174] CollectionUser and CollectionGroup authorization handlers (#3194) * [AC-1174] Introduce BulkAuthorizationHandler.cs * [AC-1174] Introduce CollectionUserAuthorizationHandler * [AC-1174] Add CreateForNewCollection CollectionUser requirement * [AC-1174] Add some more details to CollectionCustomization * [AC-1174] Formatting * [AC-1174] Add CollectionGroupOperation.cs * [AC-1174] Introduce CollectionGroupAuthorizationHandler.cs * [AC-1174] Cleanup CollectionFixture customization Implement and use re-usable extension method to support seeded Guids * [AC-1174] Introduce WithValueFromList AutoFixtureExtensions Modify CollectionCustomization to use multiple organization Ids for auto generated test data * [AC-1174] Simplify CollectionUserAuthorizationHandler.cs Modify the authorization handler to only perform authorization logic. Validation logic will need to be handled by any calling commands/controllers instead. * [AC-1174] Introduce shared CollectionAccessAuthorizationHandlerBase A shared base authorization handler was created for both CollectionUser and CollectionGroup resources, as they share the same underlying management authorization logic. * [AC-1174] Update CollectionUserAuthorizationHandler and CollectionGroupAuthorizationHandler to use the new CollectionAccessAuthorizationHandlerBase class * [AC-1174] Formatting * [AC-1174] Cleanup typo and redundant ToList() call * [AC-1174] Add check for provider users * [AC-1174] Reduce nested loops * [AC-1174] Introduce ICollectionAccess.cs * [AC-1174] Remove individual CollectionGroup and CollectionUser auth handlers and use base class instead * [AC-1174] Tweak unit test to fail minimally * [AC-1174] Reorganize authorization handlers in Core project * [AC-1174] Introduce new AddCoreAuthorizationHandlers() extension method * [AC-1174] Move CollectionAccessAuthorizationHandler into Api project * [AC-1174] Move CollectionFixture to Vault folder * [AC-1174] Rename operation to CreateUpdateDelete * [AC-1174] Require single organization for collection access authorization handler - Add requirement that all target collections must belong to the same organization - Simplify logic related to multiple organizations - Update tests and helpers - Use ToHashSet to improve lookup time * [AC-1174] Fix null reference exception * [AC-1174] Throw bad request exception when collections belong to different organizations * [AC-1174] Switch to CollectionAuthorizationHandler instead of CollectionAccessAuthorizationHandler to reduce complexity * Fix improper merge conflict resolution * fix: add permission check for collection management api, refs AC-1647 (#3252) * [AC-1125] Enforce org setting for creating/deleting collections (#3241) * [AC-1117] Add manage permission (#3126) * Update sql files to add Manage permission * Add migration script * Rename collection manage migration file to remove duplicate migration date * Migrations * Add manage to models * Add manage to repository * Add constraint to Manage columns * Migration lint fixes * Add manage to OrganizationUserUserDetails_ReadWithCollectionsById * Add missing manage fields * Add 'Manage' to UserCollectionDetails * Use CREATE OR ALTER where possible * [AC-1374] Limit collection creation/deletion to Owner/Admin (#3145) * feat: update org table with new column, write migration, refs AC-1374 * feat: update views with new column, refs AC-1374 * feat: Alter sprocs (org create/update) to include new column, refs AC-1374 * feat: update entity/data/request/response models to handle new column, refs AC-1374 * feat: update necessary Provider related views during migration, refs AC-1374 * fix: update org create to default new column to false, refs AC-1374 * feat: added new API/request model for collection management and removed property from update request model, refs AC-1374 * fix: renamed migration script to be after secrets manage beta column changes, refs AC-1374 * fix: dotnet format, refs AC-1374 * feat: add ef migrations to reflect mssql changes, refs AC-1374 * fix: dotnet format, refs AC-1374 * feat: update API signature to accept Guid and explain Cd verbiage, refs AC-1374 * feat: created collection auth handler/operations, added LimitCollectionCdOwnerAdmin to CurrentContentOrganization, refs AC-1125 * feat: create vault service collection extensions and register with base services, refs AC-1125 * feat: deprecated CurrentContext.CreateNewCollections, refs AC-1125 * feat: deprecate DeleteAnyCollection for single resource usages, refs AC-1125 * feat: move service registration to api, update references, refs AC-1125 * feat: add bulk delete authorization handler, refs AC-1125 * feat: always assign user and give manage access on create, refs AC-1125 * fix: updated CurrentContextOrganization type, refs AC-1125 * feat: combined existing collection authorization handlers/operations, refs AC-1125 * fix: OrganizationServiceTests -> CurrentContentOrganization typo, refs AC-1125 * fix: format, refs AC-1125 * fix: update collection controller tests, refs AC-1125 * fix: dotnet format, refs AC-1125 * feat: removed extra BulkAuthorizationHandler, refs AC-1125 * fix: dotnet format, refs AC-1125 * fix: change string to guid for org id, update bulk delete request model, refs AC-1125 * fix: remove delete many collection check, refs AC-1125 * fix: clean up collection auth handler, refs AC-1125 * fix: format fix for CollectionOperations, refs AC-1125 * fix: removed unnecessary owner check, add org null check to custom permission validation, refs AC-1125 * fix: remove unused methods in CurrentContext, refs AC-1125 * fix: removed obsolete test, fixed failling delete many test, refs AC-1125 * fix: CollectionAuthorizationHandlerTests fixes, refs AC-1125 * fix: OrganizationServiceTests fix broken test by mocking GetOrganization, refs AC-1125 * fix: CollectionAuthorizationHandler - remove unused repository, refs AC-1125 * feat: moved UserId null check to common method, refs AC-1125 * fix: updated auth handler tests to remove dependency on requirement for common code checks, refs AC-1125 * feat: updated conditionals/comments for create/delete methods within colleciton auth handler, refs AC-1125 * feat: added create/delete collection auth handler success methods, refs AC-1125 * fix: new up permissions to prevent excessive null checks, refs AC-1125 * fix: remove old reference to CreateNewCollections, refs AC-1125 * fix: typo within ViewAssignedCollections method, refs AC-1125 --------- Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com> * refactor: remove organizationId from CollectionBulkDeleteRequestModel, refs AC-1649 (#3282) * [AC-1174] Bulk Collection Management (#3229) * [AC-1174] Update SelectionReadOnlyRequestModel to use Guid for Id property * [AC-1174] Introduce initial bulk-access collection endpoint * [AC-1174] Introduce BulkAddCollectionAccessCommand and validation logic/tests * [AC-1174] Add CreateOrUpdateAccessMany method to CollectionRepository * [AC-1174] Add event logs for bulk add collection access command * [AC-1174] Add User_BumpAccountRevisionDateByCollectionIds and database migration script * [AC-1174] Implement EF repository method * [AC-1174] Improve null checks * [AC-1174] Remove unnecessary BulkCollectionAccessRequestModel helpers * [AC-1174] Add unit tests for new controller endpoint * [AC-1174] Fix formatting * [AC-1174] Remove comment * [AC-1174] Remove redundant organizationId parameter * [AC-1174] Ensure user and group Ids are distinct * [AC-1174] Cleanup tests based on PR feedback * [AC-1174] Formatting * [AC-1174] Update CollectionGroup alias in the sproc * [AC-1174] Add some additional comments to SQL sproc * [AC-1174] Add comment explaining additional SaveChangesAsync call --------- Co-authored-by: Thomas Rittson <trittson@bitwarden.com> * [AC-1646] Rename LimitCollectionCdOwnerAdmin column (#3300) * Rename LimitCollectionCdOwnerAdmin -> LimitCollectionCreationDeletion * Rename and bump migration script * [AC-1666] Removed EditAnyCollection from Create/Delete permission checks (#3301) * fix: remove EditAnyCollection from Create/Delete permission check, refs AC-1666 * fix: updated comment, refs AC-1666 * [AC-1669] Bug - Remove obsolete assignUserId from CollectionService.SaveAsync(...) (#3312) * fix: remove AssignUserId from CollectionService.SaveAsync, refs AC-1669 * fix: add manage access conditional before creating collection, refs AC-1669 * fix: move access logic for create/update, fix all tests, refs AC-1669 * fix: add CollectionAccessSelection fixture, update tests, update bad reqeuest message, refs AC-1669 * fix: format, refs AC-1669 * fix: update null params with specific arg.is null checks, refs Ac-1669 * fix: update attribute class name, refs AC-1669 * [AC-1713] [Flexible collections] Add feature flags to server (#3334) * Add feature flags for FlexibleCollections and BulkCollectionAccess * Flag new routes and behaviour --------- Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> * Add joint codeownership for auth handlers (#3346) * [AC-1717] Update default values for LimitCollectionCreationDeletion (#3365) * Change default value in organization create sproc to 1 * Drop old column name still present in some QA instances * Set LimitCollectionCreationDeletion value in code based on feature flag * Fix: add missing namespace after merging in master * Fix: add missing namespace after merging in master * [AC-1683] Fix DB migrations for new Manage permission (#3307) * [AC-1683] Update migration script and introduce V2 procedures and types * [AC-1683] Update repository calls to use new V2 procedures / types * [AC-1684] Update bulk add collection migration script to use new V2 type * [AC-1683] Undo Manage changes to more original procedures * [AC-1683] Restore whitespace changes * [AC-1683] Clarify comments regarding explicit column lists * [AC-1683] Update migration script dates * [AC-1683] Split the migration script for readability * [AC-1683] Re-name SelectReadOnlyArray_V2 to CollectionAccessSelectionType * [AC-1648] [Flexible Collections] Bump migration scripts before feature branch merge (#3371) * Bump dates on sql migration scripts * Bump date on ef migrations --------- Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com> Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Co-authored-by: Vincent Salucci <vincesalucci21@gmail.com> Co-authored-by: Shane Melton <smelton@bitwarden.com> Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com>
This commit is contained in:
@ -1,5 +1,8 @@
|
||||
using Bit.Api.Controllers;
|
||||
using System.Security.Claims;
|
||||
using Bit.Api.Controllers;
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -7,8 +10,10 @@ using Bit.Core.Models.Data;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
@ -16,30 +21,29 @@ namespace Bit.Api.Test.Controllers;
|
||||
|
||||
[ControllerCustomize(typeof(CollectionsController))]
|
||||
[SutProviderCustomize]
|
||||
[FeatureServiceCustomize(FeatureFlagKeys.FlexibleCollections)]
|
||||
public class CollectionsControllerTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task Post_Success(Guid orgId, SutProvider<CollectionsController> sutProvider)
|
||||
public async Task Post_Success(Guid orgId, CollectionRequestModel collectionRequest,
|
||||
SutProvider<CollectionsController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.CreateNewCollections(orgId)
|
||||
.Returns(true);
|
||||
Collection ExpectedCollection() => Arg.Is<Collection>(c =>
|
||||
c.Name == collectionRequest.Name && c.ExternalId == collectionRequest.ExternalId &&
|
||||
c.OrganizationId == orgId);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.EditAnyCollection(orgId)
|
||||
.Returns(false);
|
||||
|
||||
var collectionRequest = new CollectionRequestModel
|
||||
{
|
||||
Name = "encrypted_string",
|
||||
ExternalId = "my_external_id"
|
||||
};
|
||||
sutProvider.GetDependency<IAuthorizationService>()
|
||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
|
||||
ExpectedCollection(),
|
||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(CollectionOperations.Create)))
|
||||
.Returns(AuthorizationResult.Success());
|
||||
|
||||
_ = await sutProvider.Sut.Post(orgId, collectionRequest);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionService>()
|
||||
.Received(1)
|
||||
.SaveAsync(Arg.Any<Collection>(), Arg.Any<IEnumerable<CollectionAccessSelection>>(), null);
|
||||
.SaveAsync(Arg.Any<Collection>(), Arg.Any<IEnumerable<CollectionAccessSelection>>(),
|
||||
Arg.Any<IEnumerable<CollectionAccessSelection>>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -139,13 +143,12 @@ public class CollectionsControllerTests
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteMany_Success(Guid orgId, User user, Collection collection1, Collection collection2, SutProvider<CollectionsController> sutProvider)
|
||||
public async Task DeleteMany_Success(Guid orgId, Collection collection1, Collection collection2, SutProvider<CollectionsController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var model = new CollectionBulkDeleteRequestModel
|
||||
{
|
||||
Ids = new[] { collection1.Id.ToString(), collection2.Id.ToString() },
|
||||
OrganizationId = orgId.ToString()
|
||||
Ids = new[] { collection1.Id, collection2.Id }
|
||||
};
|
||||
|
||||
var collections = new List<Collection>
|
||||
@ -162,20 +165,17 @@ public class CollectionsControllerTests
|
||||
},
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.DeleteAssignedCollections(orgId)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.UserId
|
||||
.Returns(user.Id);
|
||||
|
||||
sutProvider.GetDependency<ICollectionService>()
|
||||
.GetOrganizationCollectionsAsync(orgId)
|
||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByManyIdsAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(collections);
|
||||
|
||||
sutProvider.GetDependency<IAuthorizationService>()
|
||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
|
||||
collections,
|
||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(CollectionOperations.Delete)))
|
||||
.Returns(AuthorizationResult.Success());
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.DeleteMany(model);
|
||||
await sutProvider.Sut.DeleteMany(orgId, model);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IDeleteCollectionCommand>()
|
||||
@ -185,69 +185,159 @@ public class CollectionsControllerTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteMany_CanNotDeleteAssignedCollection_ThrowsNotFound(Guid orgId, Collection collection1, Collection collection2, SutProvider<CollectionsController> sutProvider)
|
||||
public async Task DeleteMany_PermissionDenied_ThrowsNotFound(Guid orgId, Collection collection1, Collection collection2, SutProvider<CollectionsController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var model = new CollectionBulkDeleteRequestModel
|
||||
{
|
||||
Ids = new[] { collection1.Id.ToString(), collection2.Id.ToString() },
|
||||
OrganizationId = orgId.ToString()
|
||||
Ids = new[] { collection1.Id, collection2.Id }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.DeleteAssignedCollections(orgId)
|
||||
.Returns(false);
|
||||
var collections = new List<Collection>
|
||||
{
|
||||
new CollectionDetails
|
||||
{
|
||||
Id = collection1.Id,
|
||||
OrganizationId = orgId,
|
||||
},
|
||||
new CollectionDetails
|
||||
{
|
||||
Id = collection2.Id,
|
||||
OrganizationId = orgId,
|
||||
},
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByManyIdsAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(collections);
|
||||
|
||||
sutProvider.GetDependency<IAuthorizationService>()
|
||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
|
||||
collections,
|
||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(CollectionOperations.Delete)))
|
||||
.Returns(AuthorizationResult.Failed());
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<NotFoundException>(() =>
|
||||
sutProvider.Sut.DeleteMany(model));
|
||||
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)
|
||||
public async Task PostBulkCollectionAccess_Success(User actingUser, ICollection<Collection> collections, SutProvider<CollectionsController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var model = new CollectionBulkDeleteRequestModel
|
||||
var userId = Guid.NewGuid();
|
||||
var groupId = Guid.NewGuid();
|
||||
var model = new BulkCollectionAccessRequestModel
|
||||
{
|
||||
Ids = new[] { collection1.Id.ToString(), collection2.Id.ToString() },
|
||||
OrganizationId = orgId.ToString()
|
||||
CollectionIds = collections.Select(c => c.Id),
|
||||
Users = new[] { new SelectionReadOnlyRequestModel { Id = userId, Manage = true } },
|
||||
Groups = new[] { new SelectionReadOnlyRequestModel { Id = groupId, ReadOnly = true } },
|
||||
};
|
||||
|
||||
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)
|
||||
sutProvider.GetDependency<ICollectionRepository>()
|
||||
.GetManyByManyIdsAsync(model.CollectionIds)
|
||||
.Returns(collections);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.UserId.Returns(actingUser.Id);
|
||||
|
||||
sutProvider.GetDependency<IAuthorizationService>().AuthorizeAsync(
|
||||
Arg.Any<ClaimsPrincipal>(), ExpectedCollectionAccess(),
|
||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(
|
||||
r => r.Contains(CollectionOperations.ModifyAccess)
|
||||
))
|
||||
.Returns(AuthorizationResult.Success());
|
||||
|
||||
IEnumerable<Collection> ExpectedCollectionAccess() => Arg.Is<IEnumerable<Collection>>(cols => cols.SequenceEqual(collections));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.DeleteMany(model);
|
||||
await sutProvider.Sut.PostBulkCollectionAccess(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))));
|
||||
await sutProvider.GetDependency<IAuthorizationService>().Received().AuthorizeAsync(
|
||||
Arg.Any<ClaimsPrincipal>(),
|
||||
ExpectedCollectionAccess(),
|
||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(
|
||||
r => r.Contains(CollectionOperations.ModifyAccess))
|
||||
);
|
||||
await sutProvider.GetDependency<IBulkAddCollectionAccessCommand>().Received()
|
||||
.AddAccessAsync(
|
||||
Arg.Is<ICollection<Collection>>(g => g.SequenceEqual(collections)),
|
||||
Arg.Is<ICollection<CollectionAccessSelection>>(u => u.All(c => c.Id == userId && c.Manage)),
|
||||
Arg.Is<ICollection<CollectionAccessSelection>>(g => g.All(c => c.Id == groupId && c.ReadOnly)));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostBulkCollectionAccess_CollectionsNotFound_Throws(User actingUser, ICollection<Collection> collections, SutProvider<CollectionsController> sutProvider)
|
||||
{
|
||||
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 } },
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.UserId.Returns(actingUser.Id);
|
||||
|
||||
sutProvider.GetDependency<ICollectionRepository>()
|
||||
.GetManyByManyIdsAsync(model.CollectionIds)
|
||||
.Returns(collections.Skip(1).ToList());
|
||||
|
||||
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PostBulkCollectionAccess(model));
|
||||
|
||||
Assert.Equal("One or more collections not found.", 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]
|
||||
public async Task PostBulkCollectionAccess_AccessDenied_Throws(User actingUser, ICollection<Collection> collections, SutProvider<CollectionsController> sutProvider)
|
||||
{
|
||||
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 } },
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.UserId.Returns(actingUser.Id);
|
||||
|
||||
sutProvider.GetDependency<ICollectionRepository>()
|
||||
.GetManyByManyIdsAsync(model.CollectionIds)
|
||||
.Returns(collections);
|
||||
|
||||
sutProvider.GetDependency<IAuthorizationService>().AuthorizeAsync(
|
||||
Arg.Any<ClaimsPrincipal>(), ExpectedCollectionAccess(),
|
||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(
|
||||
r => r.Contains(CollectionOperations.ModifyAccess)
|
||||
))
|
||||
.Returns(AuthorizationResult.Failed());
|
||||
|
||||
IEnumerable<Collection> ExpectedCollectionAccess() => Arg.Is<IEnumerable<Collection>>(cols => cols.SequenceEqual(collections));
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PostBulkCollectionAccess(model));
|
||||
await sutProvider.GetDependency<IAuthorizationService>().Received().AuthorizeAsync(
|
||||
Arg.Any<ClaimsPrincipal>(),
|
||||
ExpectedCollectionAccess(),
|
||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(
|
||||
r => r.Contains(CollectionOperations.ModifyAccess))
|
||||
);
|
||||
await sutProvider.GetDependency<IBulkAddCollectionAccessCommand>().DidNotReceiveWithAnyArgs()
|
||||
.AddAccessAsync(default, default, default);
|
||||
}
|
||||
}
|
||||
|
254
test/Api.Test/Controllers/LegacyCollectionsControllerTests.cs
Normal file
254
test/Api.Test/Controllers/LegacyCollectionsControllerTests.cs
Normal file
@ -0,0 +1,254 @@
|
||||
using Bit.Api.Controllers;
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
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;
|
||||
|
||||
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_Success(Guid orgId, SutProvider<CollectionsController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationManager(orgId)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.EditAnyCollection(orgId)
|
||||
.Returns(false);
|
||||
|
||||
var collectionRequest = new CollectionRequestModel
|
||||
{
|
||||
Name = "encrypted_string",
|
||||
ExternalId = "my_external_id"
|
||||
};
|
||||
|
||||
_ = await sutProvider.Sut.Post(orgId, collectionRequest);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionService>()
|
||||
.Received(1)
|
||||
.SaveAsync(Arg.Any<Collection>(), Arg.Any<IEnumerable<CollectionAccessSelection>>(), null);
|
||||
}
|
||||
|
||||
[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)
|
||||
.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)
|
||||
.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);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
|
||||
[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))));
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,224 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Test.AutoFixture;
|
||||
using Bit.Core.Test.Vault.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.Vault.AuthorizationHandlers;
|
||||
|
||||
[SutProviderCustomize]
|
||||
[FeatureServiceCustomize(FeatureFlagKeys.FlexibleCollections)]
|
||||
public class CollectionAuthorizationHandlerTests
|
||||
{
|
||||
[Theory, CollectionCustomization]
|
||||
[BitAutoData(OrganizationUserType.User, false, true)]
|
||||
[BitAutoData(OrganizationUserType.Admin, false, false)]
|
||||
[BitAutoData(OrganizationUserType.Owner, false, false)]
|
||||
[BitAutoData(OrganizationUserType.Custom, true, false)]
|
||||
[BitAutoData(OrganizationUserType.Owner, true, true)]
|
||||
public async Task CanManageCollectionAccessAsync_Success(
|
||||
OrganizationUserType userType, bool editAnyCollection, bool manageCollections,
|
||||
SutProvider<CollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<Collection> collections,
|
||||
ICollection<CollectionDetails> collectionDetails,
|
||||
CurrentContextOrganization organization)
|
||||
{
|
||||
var actingUserId = Guid.NewGuid();
|
||||
foreach (var collectionDetail in collectionDetails)
|
||||
{
|
||||
collectionDetail.Manage = manageCollections;
|
||||
}
|
||||
|
||||
organization.Type = userType;
|
||||
organization.Permissions.EditAnyCollection = editAnyCollection;
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { CollectionOperations.ModifyAccess },
|
||||
new ClaimsPrincipal(),
|
||||
collections);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collectionDetails);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, CollectionCustomization]
|
||||
[BitAutoData(OrganizationUserType.User, false, false)]
|
||||
[BitAutoData(OrganizationUserType.Admin, false, true)]
|
||||
[BitAutoData(OrganizationUserType.Owner, false, true)]
|
||||
[BitAutoData(OrganizationUserType.Custom, true, true)]
|
||||
public async Task CanCreateAsync_Success(
|
||||
OrganizationUserType userType, bool createNewCollection, bool limitCollectionCreateDelete,
|
||||
SutProvider<CollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<Collection> collections,
|
||||
CurrentContextOrganization organization)
|
||||
{
|
||||
var actingUserId = Guid.NewGuid();
|
||||
|
||||
organization.Type = userType;
|
||||
organization.Permissions.CreateNewCollections = createNewCollection;
|
||||
organization.LimitCollectionCreationDeletion = limitCollectionCreateDelete;
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { CollectionOperations.Create },
|
||||
new ClaimsPrincipal(),
|
||||
collections);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, CollectionCustomization]
|
||||
[BitAutoData(OrganizationUserType.User, false, false, true)]
|
||||
[BitAutoData(OrganizationUserType.Admin, false, true, false)]
|
||||
[BitAutoData(OrganizationUserType.Owner, false, true, false)]
|
||||
[BitAutoData(OrganizationUserType.Custom, true, true, false)]
|
||||
public async Task CanDeleteAsync_Success(
|
||||
OrganizationUserType userType, bool deleteAnyCollection, bool limitCollectionCreateDelete, bool manageCollections,
|
||||
SutProvider<CollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<Collection> collections,
|
||||
ICollection<CollectionDetails> collectionDetails,
|
||||
CurrentContextOrganization organization)
|
||||
{
|
||||
var actingUserId = Guid.NewGuid();
|
||||
foreach (var collectionDetail in collectionDetails)
|
||||
{
|
||||
collectionDetail.Manage = manageCollections;
|
||||
}
|
||||
|
||||
organization.Type = userType;
|
||||
organization.Permissions.DeleteAnyCollection = deleteAnyCollection;
|
||||
organization.LimitCollectionCreationDeletion = limitCollectionCreateDelete;
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { CollectionOperations.Delete },
|
||||
new ClaimsPrincipal(),
|
||||
collections);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collectionDetails);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task HandleRequirementAsync_MissingUserId_Failure(
|
||||
SutProvider<CollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<Collection> collections)
|
||||
{
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { CollectionOperations.Create },
|
||||
new ClaimsPrincipal(),
|
||||
collections
|
||||
);
|
||||
|
||||
// Simulate missing user id
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns((Guid?)null);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
Assert.True(context.HasFailed);
|
||||
sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task HandleRequirementAsync_TargetCollectionsMultipleOrgs_Failure(
|
||||
SutProvider<CollectionAuthorizationHandler> sutProvider,
|
||||
IList<Collection> collections)
|
||||
{
|
||||
var actingUserId = Guid.NewGuid();
|
||||
|
||||
// Simulate a collection in a different organization
|
||||
collections.First().OrganizationId = Guid.NewGuid();
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { CollectionOperations.Create },
|
||||
new ClaimsPrincipal(),
|
||||
collections
|
||||
);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.HandleAsync(context));
|
||||
Assert.Equal("Requested collections must belong to the same organization.", exception.Message);
|
||||
sutProvider.GetDependency<ICurrentContext>().DidNotReceiveWithAnyArgs().GetOrganization(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task HandleRequirementAsync_MissingOrg_Failure(
|
||||
SutProvider<CollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<Collection> collections)
|
||||
{
|
||||
var actingUserId = Guid.NewGuid();
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { CollectionOperations.Create },
|
||||
new ClaimsPrincipal(),
|
||||
collections
|
||||
);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
Assert.True(context.HasFailed);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task CanManageCollectionAccessAsync_MissingManageCollectionPermission_Failure(
|
||||
SutProvider<CollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<Collection> collections,
|
||||
ICollection<CollectionDetails> collectionDetails,
|
||||
CurrentContextOrganization organization)
|
||||
{
|
||||
var actingUserId = Guid.NewGuid();
|
||||
|
||||
foreach (var collectionDetail in collectionDetails)
|
||||
{
|
||||
collectionDetail.Manage = true;
|
||||
}
|
||||
// Simulate one collection missing the manage permission
|
||||
collectionDetails.First().Manage = false;
|
||||
|
||||
// Ensure the user is not an owner/admin and does not have edit any collection permission
|
||||
organization.Type = OrganizationUserType.User;
|
||||
organization.Permissions.EditAnyCollection = false;
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { CollectionOperations.ModifyAccess },
|
||||
new ClaimsPrincipal(),
|
||||
collections
|
||||
);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns(organization);
|
||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collectionDetails);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
Assert.True(context.HasFailed);
|
||||
sutProvider.GetDependency<ICurrentContext>().ReceivedWithAnyArgs().GetOrganization(default);
|
||||
await sutProvider.GetDependency<ICollectionRepository>().ReceivedWithAnyArgs()
|
||||
.GetManyByUserIdAsync(default);
|
||||
}
|
||||
}
|
54
test/Core.Test/AutoFixture/AutoFixtureExtensions.cs
Normal file
54
test/Core.Test/AutoFixture/AutoFixtureExtensions.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using System.Linq.Expressions;
|
||||
using AutoFixture.Dsl;
|
||||
|
||||
namespace Bit.Core.Test.AutoFixture;
|
||||
|
||||
public static class AutoFixtureExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers that a writable Guid property should be assigned a random value that is derived from the given seed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This can be used to generate random Guids that are deterministic based on the seed and thus can be re-used for
|
||||
/// different entities that share the same identifiers. e.g. Collections, CollectionUsers, and CollectionGroups can
|
||||
/// all have the same Guids generate for their "collection id" properties.
|
||||
/// </remarks>
|
||||
/// <param name="composer"></param>
|
||||
/// <param name="propertyPicker">The Guid property to register</param>
|
||||
/// <param name="seed">The random seed to use for random Guid generation</param>
|
||||
public static IPostprocessComposer<T> WithGuidFromSeed<T>(this IPostprocessComposer<T> composer, Expression<Func<T, Guid>> propertyPicker, int seed)
|
||||
{
|
||||
var rnd = new Random(seed);
|
||||
return composer.With(propertyPicker, () =>
|
||||
{
|
||||
// While not as random/unique as Guid.NewGuid(), this is works well enough for testing purposes.
|
||||
var bytes = new byte[16];
|
||||
rnd.NextBytes(bytes);
|
||||
return new Guid(bytes);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers that a writable property should be assigned a value from the given list.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The value will be assigned in the order that the list is enumerated. Values will wrap around to the beginning
|
||||
/// should the end of the list be reached.
|
||||
/// </remarks>
|
||||
/// <param name="composer"></param>
|
||||
/// <param name="propertyPicker"></param>
|
||||
/// <param name="values"></param>
|
||||
public static IPostprocessComposer<T> WithValueFromList<T, TValue>(
|
||||
this IPostprocessComposer<T> composer,
|
||||
Expression<Func<T, TValue>> propertyPicker,
|
||||
ICollection<TValue> values)
|
||||
{
|
||||
var index = 0;
|
||||
return composer.With(propertyPicker, () =>
|
||||
{
|
||||
var value = values.ElementAt(index);
|
||||
index = (index + 1) % values.Count;
|
||||
return value;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
using System.Reflection;
|
||||
using AutoFixture;
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Core.Models.Data;
|
||||
|
||||
namespace Bit.Core.Test.AutoFixture;
|
||||
|
||||
public class CollectionAccessSelectionCustomization : ICustomization
|
||||
{
|
||||
public bool Manage { get; set; }
|
||||
|
||||
public CollectionAccessSelectionCustomization(bool manage)
|
||||
{
|
||||
Manage = manage;
|
||||
}
|
||||
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
fixture.Customize<CollectionAccessSelection>(composer => composer
|
||||
.With(o => o.Manage, Manage));
|
||||
}
|
||||
}
|
||||
|
||||
public class CollectionAccessSelectionCustomizeAttribute : CustomizeAttribute
|
||||
{
|
||||
private readonly bool _manage;
|
||||
|
||||
public CollectionAccessSelectionCustomizeAttribute(bool manage = false)
|
||||
{
|
||||
_manage = manage;
|
||||
}
|
||||
|
||||
public override ICustomization GetCustomization(ParameterInfo parameter)
|
||||
{
|
||||
return new CollectionAccessSelectionCustomization(_manage);
|
||||
}
|
||||
}
|
75
test/Core.Test/AutoFixture/FeatureServiceFixtures.cs
Normal file
75
test/Core.Test/AutoFixture/FeatureServiceFixtures.cs
Normal file
@ -0,0 +1,75 @@
|
||||
using System.Reflection;
|
||||
using AutoFixture;
|
||||
using AutoFixture.Kernel;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Bit.Core.Test.AutoFixture;
|
||||
|
||||
internal class FeatureServiceBuilder : ISpecimenBuilder
|
||||
{
|
||||
private readonly string _enabledFeatureFlag;
|
||||
|
||||
public FeatureServiceBuilder(string enabledFeatureFlag)
|
||||
{
|
||||
_enabledFeatureFlag = enabledFeatureFlag;
|
||||
}
|
||||
|
||||
public object Create(object request, ISpecimenContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (request is not ParameterInfo pi)
|
||||
{
|
||||
return new NoSpecimen();
|
||||
}
|
||||
|
||||
if (pi.ParameterType == typeof(IFeatureService))
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
var featureService = fixture.WithAutoNSubstitutions().Create<IFeatureService>();
|
||||
featureService
|
||||
.IsEnabled(_enabledFeatureFlag, Arg.Any<ICurrentContext>(), Arg.Any<bool>())
|
||||
.Returns(true);
|
||||
return featureService;
|
||||
}
|
||||
|
||||
return new NoSpecimen();
|
||||
}
|
||||
}
|
||||
|
||||
internal class FeatureServiceCustomization : ICustomization
|
||||
{
|
||||
private readonly string _enabledFeatureFlag;
|
||||
|
||||
public FeatureServiceCustomization(string enabledFeatureFlag)
|
||||
{
|
||||
_enabledFeatureFlag = enabledFeatureFlag;
|
||||
}
|
||||
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
fixture.Customizations.Add(new FeatureServiceBuilder(_enabledFeatureFlag));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Arranges the IFeatureService mock to enable the specified feature flag
|
||||
/// </summary>
|
||||
public class FeatureServiceCustomizeAttribute : BitCustomizeAttribute
|
||||
{
|
||||
private readonly string _enabledFeatureFlag;
|
||||
|
||||
public FeatureServiceCustomizeAttribute(string enabledFeatureFlag)
|
||||
{
|
||||
_enabledFeatureFlag = enabledFeatureFlag;
|
||||
}
|
||||
|
||||
public override ICustomization GetCustomization() => new FeatureServiceCustomization(_enabledFeatureFlag);
|
||||
}
|
@ -0,0 +1,273 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationCollections;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.Vault.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.OrganizationFeatures.OrganizationCollections;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class BulkAddCollectionAccessCommandTests
|
||||
{
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task AddAccessAsync_Success(SutProvider<BulkAddCollectionAccessCommand> sutProvider,
|
||||
Organization org,
|
||||
ICollection<Collection> collections,
|
||||
ICollection<OrganizationUser> organizationUsers,
|
||||
ICollection<Group> groups,
|
||||
IEnumerable<CollectionUser> collectionUsers,
|
||||
IEnumerable<CollectionGroup> collectionGroups)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))
|
||||
)
|
||||
.Returns(organizationUsers);
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyByManyIds(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionGroups.Select(u => u.GroupId)))
|
||||
)
|
||||
.Returns(groups);
|
||||
|
||||
var userAccessSelections = ToAccessSelection(collectionUsers);
|
||||
var groupAccessSelections = ToAccessSelection(collectionGroups);
|
||||
await sutProvider.Sut.AddAccessAsync(collections,
|
||||
userAccessSelections,
|
||||
groupAccessSelections
|
||||
);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received().GetManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(userAccessSelections.Select(u => u.Id)))
|
||||
);
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received().GetManyByManyIds(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(groupAccessSelections.Select(g => g.Id)))
|
||||
);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received().CreateOrUpdateAccessForManyAsync(
|
||||
org.Id,
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collections.Select(c => c.Id))),
|
||||
userAccessSelections,
|
||||
groupAccessSelections);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>().Received().LogCollectionEventsAsync(
|
||||
Arg.Is<IEnumerable<(Collection, EventType, DateTime?)>>(
|
||||
events => events.All(e =>
|
||||
collections.Contains(e.Item1) &&
|
||||
e.Item2 == EventType.Collection_Updated &&
|
||||
e.Item3.HasValue
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task ValidateRequestAsync_NoCollectionsProvided_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider)
|
||||
{
|
||||
var exception =
|
||||
await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.AddAccessAsync(null, null, null));
|
||||
|
||||
Assert.Contains("No collections were provided.", exception.Message);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs().GetManyByManyIdsAsync(default);
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetManyAsync(default);
|
||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyByManyIds(default);
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task ValidateRequestAsync_NoCollection_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider,
|
||||
IEnumerable<CollectionUser> collectionUsers,
|
||||
IEnumerable<CollectionGroup> collectionGroups)
|
||||
{
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddAccessAsync(Enumerable.Empty<Collection>().ToList(),
|
||||
ToAccessSelection(collectionUsers),
|
||||
ToAccessSelection(collectionGroups)
|
||||
));
|
||||
|
||||
Assert.Contains("No collections were provided.", exception.Message);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetManyAsync(default);
|
||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyByManyIds(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task ValidateRequestAsync_DifferentOrgs_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider,
|
||||
ICollection<Collection> collections,
|
||||
IEnumerable<CollectionUser> collectionUsers,
|
||||
IEnumerable<CollectionGroup> collectionGroups)
|
||||
{
|
||||
collections.First().OrganizationId = Guid.NewGuid();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddAccessAsync(collections,
|
||||
ToAccessSelection(collectionUsers),
|
||||
ToAccessSelection(collectionGroups)
|
||||
));
|
||||
|
||||
Assert.Contains("All collections must belong to the same organization.", exception.Message);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetManyAsync(default);
|
||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyByManyIds(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task ValidateRequestAsync_MissingUser_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider,
|
||||
IList<Collection> collections,
|
||||
IList<OrganizationUser> organizationUsers,
|
||||
IEnumerable<CollectionUser> collectionUsers,
|
||||
IEnumerable<CollectionGroup> collectionGroups)
|
||||
{
|
||||
organizationUsers.RemoveAt(0);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))
|
||||
)
|
||||
.Returns(organizationUsers);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddAccessAsync(collections,
|
||||
ToAccessSelection(collectionUsers),
|
||||
ToAccessSelection(collectionGroups)
|
||||
));
|
||||
|
||||
Assert.Contains("One or more users do not exist.", exception.Message);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received().GetManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))
|
||||
);
|
||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyByManyIds(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task ValidateRequestAsync_UserWrongOrg_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider,
|
||||
IList<Collection> collections,
|
||||
IList<OrganizationUser> organizationUsers,
|
||||
IEnumerable<CollectionUser> collectionUsers,
|
||||
IEnumerable<CollectionGroup> collectionGroups)
|
||||
{
|
||||
organizationUsers.First().OrganizationId = Guid.NewGuid();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))
|
||||
)
|
||||
.Returns(organizationUsers);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddAccessAsync(collections,
|
||||
ToAccessSelection(collectionUsers),
|
||||
ToAccessSelection(collectionGroups)
|
||||
));
|
||||
|
||||
Assert.Contains("One or more users do not belong to the same organization as the collection being assigned.", exception.Message);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received().GetManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))
|
||||
);
|
||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyByManyIds(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task ValidateRequestAsync_MissingGroup_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider,
|
||||
IList<Collection> collections,
|
||||
IList<OrganizationUser> organizationUsers,
|
||||
IList<Group> groups,
|
||||
IEnumerable<CollectionUser> collectionUsers,
|
||||
IEnumerable<CollectionGroup> collectionGroups)
|
||||
{
|
||||
groups.RemoveAt(0);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))
|
||||
)
|
||||
.Returns(organizationUsers);
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyByManyIds(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionGroups.Select(u => u.GroupId)))
|
||||
)
|
||||
.Returns(groups);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddAccessAsync(collections,
|
||||
ToAccessSelection(collectionUsers),
|
||||
ToAccessSelection(collectionGroups)
|
||||
));
|
||||
|
||||
Assert.Contains("One or more groups do not exist.", exception.Message);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received().GetManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))
|
||||
);
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received().GetManyByManyIds(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionGroups.Select(u => u.GroupId)))
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task ValidateRequestAsync_GroupWrongOrg_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider,
|
||||
IList<Collection> collections,
|
||||
IList<OrganizationUser> organizationUsers,
|
||||
IList<Group> groups,
|
||||
IEnumerable<CollectionUser> collectionUsers,
|
||||
IEnumerable<CollectionGroup> collectionGroups)
|
||||
{
|
||||
groups.First().OrganizationId = Guid.NewGuid();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))
|
||||
)
|
||||
.Returns(organizationUsers);
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyByManyIds(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionGroups.Select(u => u.GroupId)))
|
||||
)
|
||||
.Returns(groups);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddAccessAsync(collections,
|
||||
ToAccessSelection(collectionUsers),
|
||||
ToAccessSelection(collectionGroups)
|
||||
));
|
||||
|
||||
Assert.Contains("One or more groups do not belong to the same organization as the collection being assigned.", exception.Message);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received().GetManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))
|
||||
);
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received().GetManyByManyIds(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionGroups.Select(u => u.GroupId)))
|
||||
);
|
||||
}
|
||||
|
||||
private static ICollection<CollectionAccessSelection> ToAccessSelection(IEnumerable<CollectionUser> collectionUsers)
|
||||
{
|
||||
return collectionUsers.Select(cu => new CollectionAccessSelection
|
||||
{
|
||||
Id = cu.OrganizationUserId,
|
||||
Manage = cu.Manage,
|
||||
HidePasswords = cu.HidePasswords,
|
||||
ReadOnly = cu.ReadOnly
|
||||
}).ToList();
|
||||
}
|
||||
private static ICollection<CollectionAccessSelection> ToAccessSelection(IEnumerable<CollectionGroup> collectionGroups)
|
||||
{
|
||||
return collectionGroups.Select(cg => new CollectionAccessSelection
|
||||
{
|
||||
Id = cg.GroupId,
|
||||
Manage = cg.Manage,
|
||||
HidePasswords = cg.HidePasswords,
|
||||
ReadOnly = cg.ReadOnly
|
||||
}).ToList();
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@ -18,23 +19,7 @@ namespace Bit.Core.Test.Services;
|
||||
public class CollectionServiceTest
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_DefaultId_CreatesCollectionInTheRepository(Collection collection, Organization organization, SutProvider<CollectionService> sutProvider)
|
||||
{
|
||||
collection.Id = default;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
await sutProvider.Sut.SaveAsync(collection);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received().CreateAsync(collection, null, null);
|
||||
await sutProvider.GetDependency<IEventService>().Received()
|
||||
.LogCollectionEventAsync(collection, EventType.Collection_Created);
|
||||
Assert.True(collection.CreationDate - utcNow < TimeSpan.FromSeconds(1));
|
||||
Assert.True(collection.RevisionDate - utcNow < TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_DefaultIdWithUsers_CreatesCollectionInTheRepository(Collection collection, Organization organization, IEnumerable<CollectionAccessSelection> users, SutProvider<CollectionService> sutProvider)
|
||||
public async Task SaveAsync_DefaultIdWithUsers_CreatesCollectionInTheRepository(Collection collection, Organization organization, [CollectionAccessSelectionCustomize(true)] IEnumerable<CollectionAccessSelection> users, SutProvider<CollectionService> sutProvider)
|
||||
{
|
||||
collection.Id = default;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
@ -42,7 +27,9 @@ public class CollectionServiceTest
|
||||
|
||||
await sutProvider.Sut.SaveAsync(collection, null, users);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received().CreateAsync(collection, null, users);
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received()
|
||||
.CreateAsync(collection, Arg.Is<List<CollectionAccessSelection>>(l => l == null),
|
||||
Arg.Is<List<CollectionAccessSelection>>(l => l.Any(i => i.Manage == true)));
|
||||
await sutProvider.GetDependency<IEventService>().Received()
|
||||
.LogCollectionEventAsync(collection, EventType.Collection_Created);
|
||||
Assert.True(collection.CreationDate - utcNow < TimeSpan.FromSeconds(1));
|
||||
@ -51,7 +38,7 @@ public class CollectionServiceTest
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_DefaultIdWithGroupsAndUsers_CreateCollectionWithGroupsAndUsersInRepository(Collection collection,
|
||||
IEnumerable<CollectionAccessSelection> groups, IEnumerable<CollectionAccessSelection> users, Organization organization, SutProvider<CollectionService> sutProvider)
|
||||
[CollectionAccessSelectionCustomize(true)] IEnumerable<CollectionAccessSelection> groups, IEnumerable<CollectionAccessSelection> users, Organization organization, SutProvider<CollectionService> sutProvider)
|
||||
{
|
||||
collection.Id = default;
|
||||
organization.UseGroups = true;
|
||||
@ -60,7 +47,9 @@ public class CollectionServiceTest
|
||||
|
||||
await sutProvider.Sut.SaveAsync(collection, groups, users);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received().CreateAsync(collection, groups, users);
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received()
|
||||
.CreateAsync(collection, Arg.Is<List<CollectionAccessSelection>>(l => l.Any(i => i.Manage == true)),
|
||||
Arg.Any<List<CollectionAccessSelection>>());
|
||||
await sutProvider.GetDependency<IEventService>().Received()
|
||||
.LogCollectionEventAsync(collection, EventType.Collection_Created);
|
||||
Assert.True(collection.CreationDate - utcNow < TimeSpan.FromSeconds(1));
|
||||
@ -68,15 +57,17 @@ public class CollectionServiceTest
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_NonDefaultId_ReplacesCollectionInRepository(Collection collection, Organization organization, SutProvider<CollectionService> sutProvider)
|
||||
public async Task SaveAsync_NonDefaultId_ReplacesCollectionInRepository(Collection collection, Organization organization, [CollectionAccessSelectionCustomize(true)] IEnumerable<CollectionAccessSelection> users, SutProvider<CollectionService> sutProvider)
|
||||
{
|
||||
var creationDate = collection.CreationDate;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
await sutProvider.Sut.SaveAsync(collection);
|
||||
await sutProvider.Sut.SaveAsync(collection, null, users);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received().ReplaceAsync(collection, null, null);
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received().ReplaceAsync(collection,
|
||||
Arg.Is<List<CollectionAccessSelection>>(l => l == null),
|
||||
Arg.Is<List<CollectionAccessSelection>>(l => l.Any(i => i.Manage == true)));
|
||||
await sutProvider.GetDependency<IEventService>().Received()
|
||||
.LogCollectionEventAsync(collection, EventType.Collection_Updated);
|
||||
Assert.Equal(collection.CreationDate, creationDate);
|
||||
@ -84,39 +75,20 @@ public class CollectionServiceTest
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_OrganizationNotUseGroup_CreateCollectionWithoutGroupsInRepository(Collection collection, IEnumerable<CollectionAccessSelection> groups,
|
||||
public async Task SaveAsync_OrganizationNotUseGroup_CreateCollectionWithoutGroupsInRepository(Collection collection,
|
||||
IEnumerable<CollectionAccessSelection> groups, [CollectionAccessSelectionCustomize(true)] IEnumerable<CollectionAccessSelection> users,
|
||||
Organization organization, SutProvider<CollectionService> sutProvider)
|
||||
{
|
||||
collection.Id = default;
|
||||
organization.UseGroups = false;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
await sutProvider.Sut.SaveAsync(collection, groups);
|
||||
await sutProvider.Sut.SaveAsync(collection, groups, users);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received().CreateAsync(collection, null, null);
|
||||
await sutProvider.GetDependency<IEventService>().Received()
|
||||
.LogCollectionEventAsync(collection, EventType.Collection_Created);
|
||||
Assert.True(collection.CreationDate - utcNow < TimeSpan.FromSeconds(1));
|
||||
Assert.True(collection.RevisionDate - utcNow < TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_DefaultIdWithUserId_UpdateUserInCollectionRepository(Collection collection,
|
||||
Organization organization, OrganizationUser organizationUser, SutProvider<CollectionService> sutProvider)
|
||||
{
|
||||
collection.Id = default;
|
||||
organizationUser.Status = OrganizationUserStatusType.Confirmed;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(organization.Id, organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
await sutProvider.Sut.SaveAsync(collection, null, null, organizationUser.Id);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received().CreateAsync(collection, null, null);
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received()
|
||||
.GetByOrganizationAsync(organization.Id, organizationUser.Id);
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received().UpdateUsersAsync(collection.Id, Arg.Any<List<CollectionAccessSelection>>());
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received().CreateAsync(collection,
|
||||
Arg.Is<List<CollectionAccessSelection>>(l => l == null),
|
||||
Arg.Is<List<CollectionAccessSelection>>(l => l.Any(i => i.Manage == true)));
|
||||
await sutProvider.GetDependency<IEventService>().Received()
|
||||
.LogCollectionEventAsync(collection, EventType.Collection_Created);
|
||||
Assert.True(collection.CreationDate - utcNow < TimeSpan.FromSeconds(1));
|
||||
@ -135,14 +107,34 @@ public class CollectionServiceTest
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_ExceedsOrganizationMaxCollections_ThrowsBadRequest(Collection collection, Organization organization, SutProvider<CollectionService> sutProvider)
|
||||
public async Task SaveAsync_NoManageAccess_ThrowsBadRequest(Collection collection, Organization organization,
|
||||
[CollectionAccessSelectionCustomize] IEnumerable<CollectionAccessSelection> users, SutProvider<CollectionService> sutProvider)
|
||||
{
|
||||
collection.Id = default;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.FlexibleCollections, Arg.Any<ICurrentContext>(), Arg.Any<bool>())
|
||||
.Returns(true);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SaveAsync(collection, null, users));
|
||||
Assert.Contains("At least one member or group must have can manage permission.", ex.Message);
|
||||
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs().CreateAsync(default);
|
||||
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs().CreateAsync(default, default, default);
|
||||
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCollectionEventAsync(default, default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_ExceedsOrganizationMaxCollections_ThrowsBadRequest(Collection collection,
|
||||
Organization organization, [CollectionAccessSelectionCustomize(true)] IEnumerable<CollectionAccessSelection> users,
|
||||
SutProvider<CollectionService> sutProvider)
|
||||
{
|
||||
collection.Id = default;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICollectionRepository>().GetCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(organization.MaxCollections.Value);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SaveAsync(collection));
|
||||
var ex = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SaveAsync(collection, null, users));
|
||||
Assert.Equal($@"You have reached the maximum number of collections ({organization.MaxCollections.Value}) for this organization.", ex.Message);
|
||||
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs().CreateAsync(default);
|
||||
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs().CreateAsync(default, default, default);
|
||||
|
@ -607,12 +607,19 @@ public class OrganizationServiceTests
|
||||
currentContext.ManageSso(organization.Id).Returns(true);
|
||||
currentContext.AccessEventLogs(organization.Id).Returns(true);
|
||||
currentContext.AccessImportExport(organization.Id).Returns(true);
|
||||
currentContext.CreateNewCollections(organization.Id).Returns(true);
|
||||
currentContext.DeleteAnyCollection(organization.Id).Returns(true);
|
||||
currentContext.DeleteAssignedCollections(organization.Id).Returns(true);
|
||||
currentContext.EditAnyCollection(organization.Id).Returns(true);
|
||||
currentContext.EditAssignedCollections(organization.Id).Returns(true);
|
||||
currentContext.ManageResetPassword(organization.Id).Returns(true);
|
||||
currentContext.GetOrganization(organization.Id)
|
||||
.Returns(new CurrentContextOrganization()
|
||||
{
|
||||
Permissions = new Permissions
|
||||
{
|
||||
CreateNewCollections = true,
|
||||
DeleteAnyCollection = true
|
||||
}
|
||||
});
|
||||
|
||||
await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, invites);
|
||||
|
||||
@ -942,6 +949,14 @@ public class OrganizationServiceTests
|
||||
currentContext.OrganizationCustom(savingUser.OrganizationId).Returns(true);
|
||||
currentContext.ManageUsers(savingUser.OrganizationId).Returns(true);
|
||||
currentContext.AccessReports(savingUser.OrganizationId).Returns(true);
|
||||
currentContext.GetOrganization(savingUser.OrganizationId).Returns(
|
||||
new CurrentContextOrganization()
|
||||
{
|
||||
Permissions = new Permissions
|
||||
{
|
||||
AccessReports = true
|
||||
}
|
||||
});
|
||||
|
||||
await sutProvider.Sut.SaveUserAsync(newUserData, savingUser.UserId, collections, groups);
|
||||
}
|
||||
|
72
test/Core.Test/Utilities/BulkAuthorizationHandlerTests.cs
Normal file
72
test/Core.Test/Utilities/BulkAuthorizationHandlerTests.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Utilities;
|
||||
|
||||
public class BulkAuthorizationHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task HandleRequirementAsync_SingleResource_Success()
|
||||
{
|
||||
var handler = new TestBulkAuthorizationHandler();
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { new TestOperationRequirement() },
|
||||
new ClaimsPrincipal(),
|
||||
new TestResource());
|
||||
await handler.HandleAsync(context);
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleRequirementAsync_BulkResource_Success()
|
||||
{
|
||||
var handler = new TestBulkAuthorizationHandler();
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { new TestOperationRequirement() },
|
||||
new ClaimsPrincipal(),
|
||||
new[] { new TestResource(), new TestResource() });
|
||||
await handler.HandleAsync(context);
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleRequirementAsync_NoResources_Failure()
|
||||
{
|
||||
var handler = new TestBulkAuthorizationHandler();
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { new TestOperationRequirement() },
|
||||
new ClaimsPrincipal(),
|
||||
null);
|
||||
await handler.HandleAsync(context);
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleRequirementAsync_WrongResourceType_Failure()
|
||||
{
|
||||
var handler = new TestBulkAuthorizationHandler();
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { new TestOperationRequirement() },
|
||||
new ClaimsPrincipal(),
|
||||
new object());
|
||||
await handler.HandleAsync(context);
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
private class TestOperationRequirement : OperationAuthorizationRequirement { }
|
||||
|
||||
private class TestResource { }
|
||||
|
||||
private class TestBulkAuthorizationHandler : BulkAuthorizationHandler<TestOperationRequirement, TestResource>
|
||||
{
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
TestOperationRequirement requirement,
|
||||
ICollection<TestResource> resources)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
}
|
56
test/Core.Test/Vault/AutoFixture/CollectionFixture.cs
Normal file
56
test/Core.Test/Vault/AutoFixture/CollectionFixture.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Test.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
|
||||
namespace Bit.Core.Test.Vault.AutoFixture;
|
||||
|
||||
public class CollectionCustomization : ICustomization
|
||||
{
|
||||
private const int _collectionIdSeed = 1;
|
||||
private const int _userIdSeed = 2;
|
||||
private const int _groupIdSeed = 3;
|
||||
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
var orgId = Guid.NewGuid();
|
||||
|
||||
fixture.Customize<Organization>(composer => composer
|
||||
.With(o => o.Id, orgId));
|
||||
|
||||
fixture.Customize<CurrentContextOrganization>(composer => composer
|
||||
.With(o => o.Id, orgId));
|
||||
|
||||
fixture.Customize<OrganizationUser>(composer => composer
|
||||
.With(o => o.OrganizationId, orgId)
|
||||
.WithGuidFromSeed(o => o.Id, _userIdSeed));
|
||||
|
||||
fixture.Customize<Collection>(composer => composer
|
||||
.With(o => o.OrganizationId, orgId)
|
||||
.WithGuidFromSeed(c => c.Id, _collectionIdSeed));
|
||||
|
||||
fixture.Customize<CollectionDetails>(composer => composer
|
||||
.With(o => o.OrganizationId, orgId)
|
||||
.WithGuidFromSeed(cd => cd.Id, _collectionIdSeed));
|
||||
|
||||
fixture.Customize<CollectionUser>(c => c
|
||||
.WithGuidFromSeed(cu => cu.OrganizationUserId, _userIdSeed)
|
||||
.WithGuidFromSeed(cu => cu.CollectionId, _collectionIdSeed));
|
||||
|
||||
fixture.Customize<Group>(composer => composer
|
||||
.With(o => o.OrganizationId, orgId)
|
||||
.WithGuidFromSeed(o => o.Id, _groupIdSeed));
|
||||
|
||||
fixture.Customize<CollectionGroup>(c => c
|
||||
.WithGuidFromSeed(cu => cu.GroupId, _groupIdSeed)
|
||||
.WithGuidFromSeed(cu => cu.CollectionId, _collectionIdSeed));
|
||||
}
|
||||
}
|
||||
|
||||
public class CollectionCustomizationAttribute : BitCustomizeAttribute
|
||||
{
|
||||
public override ICustomization GetCustomization() => new CollectionCustomization();
|
||||
}
|
@ -91,6 +91,7 @@ public class CipherRepositoryTests
|
||||
Id = orgUser.Id,
|
||||
HidePasswords = true,
|
||||
ReadOnly = true,
|
||||
Manage = true
|
||||
},
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user