diff --git a/src/Api/Controllers/CollectionsController.cs b/src/Api/Controllers/CollectionsController.cs index 5cfa8082bc..3c1fdf3c1a 100644 --- a/src/Api/Controllers/CollectionsController.cs +++ b/src/Api/Controllers/CollectionsController.cs @@ -198,7 +198,7 @@ public class CollectionsController : Controller var collection = model.ToCollection(orgId); var authorized = UseFlexibleCollections - ? (await _authorizationService.AuthorizeAsync(User, collection, CollectionOperations.Create)).Succeeded + ? (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Create)).Succeeded : await CanCreateCollection(orgId, collection.Id) || await CanEditCollectionAsync(orgId, collection.Id); if (!authorized) { @@ -269,7 +269,7 @@ public class CollectionsController : Controller throw new NotFoundException("One or more collections not found."); } - var result = await _authorizationService.AuthorizeAsync(User, collections, CollectionOperations.ModifyAccess); + var result = await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.ModifyAccess); if (!result.Succeeded) { @@ -311,7 +311,7 @@ public class CollectionsController : Controller { // New flexible collections logic var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Ids); - var result = await _authorizationService.AuthorizeAsync(User, collections, CollectionOperations.Delete); + var result = await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.Delete); if (!result.Succeeded) { throw new NotFoundException(); @@ -496,7 +496,7 @@ public class CollectionsController : Controller private async Task Get_vNext(Guid collectionId) { var collection = await _collectionRepository.GetByIdAsync(collectionId); - var authorized = (await _authorizationService.AuthorizeAsync(User, collection, CollectionOperations.Read)).Succeeded; + var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Read)).Succeeded; if (!authorized) { throw new NotFoundException(); @@ -509,7 +509,7 @@ public class CollectionsController : Controller { // New flexible collections logic var (collection, access) = await _collectionRepository.GetByIdWithAccessAsync(id); - var authorized = (await _authorizationService.AuthorizeAsync(User, collection, CollectionOperations.Read)).Succeeded; + var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Read)).Succeeded; if (!authorized) { throw new NotFoundException(); @@ -570,7 +570,7 @@ public class CollectionsController : Controller private async Task> GetUsers_vNext(Guid id) { var collection = await _collectionRepository.GetByIdAsync(id); - var authorized = (await _authorizationService.AuthorizeAsync(User, collection, CollectionOperations.ReadAccess)).Succeeded; + var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ReadAccess)).Succeeded; if (!authorized) { throw new NotFoundException(); @@ -584,7 +584,7 @@ public class CollectionsController : Controller private async Task Put_vNext(Guid id, CollectionRequestModel model) { var collection = await _collectionRepository.GetByIdAsync(id); - var authorized = (await _authorizationService.AuthorizeAsync(User, collection, CollectionOperations.Update)).Succeeded; + var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Update)).Succeeded; if (!authorized) { throw new NotFoundException(); @@ -599,7 +599,7 @@ public class CollectionsController : Controller private async Task PutUsers_vNext(Guid id, IEnumerable model) { var collection = await _collectionRepository.GetByIdAsync(id); - var authorized = (await _authorizationService.AuthorizeAsync(User, collection, CollectionOperations.ModifyAccess)).Succeeded; + var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyAccess)).Succeeded; if (!authorized) { throw new NotFoundException(); @@ -611,7 +611,7 @@ public class CollectionsController : Controller private async Task Delete_vNext(Guid id) { var collection = await _collectionRepository.GetByIdAsync(id); - var authorized = (await _authorizationService.AuthorizeAsync(User, collection, CollectionOperations.Delete)).Succeeded; + var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Delete)).Succeeded; if (!authorized) { throw new NotFoundException(); @@ -623,7 +623,7 @@ public class CollectionsController : Controller private async Task DeleteUser_vNext(Guid id, Guid orgUserId) { var collection = await _collectionRepository.GetByIdAsync(id); - var authorized = (await _authorizationService.AuthorizeAsync(User, collection, CollectionOperations.ModifyAccess)).Succeeded; + var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyAccess)).Succeeded; if (!authorized) { throw new NotFoundException(); diff --git a/src/Api/Utilities/ServiceCollectionExtensions.cs b/src/Api/Utilities/ServiceCollectionExtensions.cs index 7afa9315d9..a98d6722df 100644 --- a/src/Api/Utilities/ServiceCollectionExtensions.cs +++ b/src/Api/Utilities/ServiceCollectionExtensions.cs @@ -122,8 +122,8 @@ public static class ServiceCollectionExtensions public static void AddAuthorizationHandlers(this IServiceCollection services) { - services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); } diff --git a/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionAuthorizationHandler.cs b/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionAuthorizationHandler.cs index 00c45d7474..c169611bf6 100644 --- a/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionAuthorizationHandler.cs +++ b/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionAuthorizationHandler.cs @@ -15,7 +15,7 @@ namespace Bit.Api.Vault.AuthorizationHandlers.Collections; /// Handles authorization logic for Collection objects, including access permissions for users and groups. /// This uses new logic implemented in the Flexible Collections initiative. /// -public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler +public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler { private readonly ICurrentContext _currentContext; private readonly ICollectionRepository _collectionRepository; @@ -35,7 +35,7 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler? resources) + BulkCollectionOperationRequirement requirement, ICollection? resources) { if (!UseFlexibleCollections) { @@ -69,27 +69,30 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler targetCollections, CurrentContextOrganization org) + private async Task CanReadAsync(AuthorizationHandlerContext context, IAuthorizationRequirement requirement, + ICollection resources, CurrentContextOrganization? org) { - if (org.Type is OrganizationUserType.Owner or OrganizationUserType.Admin || - org.Permissions.EditAnyCollection || org.Permissions.DeleteAnyCollection || - await _currentContext.ProviderUserForOrgAsync(org.Id)) + // Owners, Admins, and users with EditAnyCollection or DeleteAnyCollection permission can always read a collection + if (org is + { LimitCollectionCreationDeletion: false } or + { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or + { Permissions.EditAnyCollection: true } or + { Permissions.DeleteAnyCollection: true } or + { Permissions.CreateNewCollections: true } or + { Permissions.ManageUsers: true }) { context.Succeed(requirement); return; } - var canManageCollections = await HasCollectionAccessAsync(targetCollections, org, requireManagePermission: false); - if (canManageCollections) + // The acting user is a member of the target organization, + // ensure they have access for the collection being read + if (org is not null) + { + var canManageCollections = await HasCollectionAccessAsync(resources, org, requireManagePermission: false); + if (canManageCollections) + { + context.Succeed(requirement); + return; + } + } + + // Allow provider users to read collections if they are a provider for the target organization + if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId)) { context.Succeed(requirement); } } - private async Task CanDeleteAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement, + private async Task CanReadAccessAsync(AuthorizationHandlerContext context, IAuthorizationRequirement requirement, + ICollection resources, CurrentContextOrganization? org) + { + // Owners, Admins, and users with EditAnyCollection or DeleteAnyCollection permission can always read a collection + if (org is + { LimitCollectionCreationDeletion: false } or + { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or + { Permissions.EditAnyCollection: true } or + { Permissions.DeleteAnyCollection: true } or + { Permissions.CreateNewCollections: true }) + { + context.Succeed(requirement); + return; + } + + // The acting user is a member of the target organization, + // ensure they have access for the collection being read + if (org is not null) + { + var canManageCollections = await HasCollectionAccessAsync(resources, org, requireManagePermission: false); + if (canManageCollections) + { + context.Succeed(requirement); + return; + } + } + + // Allow provider users to read collections if they are a provider for the target organization + if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId)) + { + context.Succeed(requirement); + } + } + + /// + /// Ensures the acting user is allowed to manage access permissions for the target collections. + /// + private async Task CanManageCollectionAccessAsync(AuthorizationHandlerContext context, + IAuthorizationRequirement requirement, ICollection resources, + CurrentContextOrganization? org) + { + // Owners, Admins, and users with EditAnyCollection permission can always manage collection access + if (org is + { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or + { Permissions.EditAnyCollection: true }) + { + context.Succeed(requirement); + return; + } + + // The acting user is a member of the target organization, + // ensure they have manage permission for the collection being managed + if (org is not null) + { + var canManageCollections = await HasCollectionAccessAsync(resources, org, requireManagePermission: true); + if (canManageCollections) + { + context.Succeed(requirement); + return; + } + } + + // Allow providers to manage collections if they are a provider for the target organization + if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId)) + { + context.Succeed(requirement); + } + } + + private async Task CanDeleteAsync(AuthorizationHandlerContext context, IAuthorizationRequirement requirement, ICollection resources, CurrentContextOrganization? org) { // Owners, Admins, and users with DeleteAnyCollection permission can always delete collections @@ -159,41 +248,6 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler - /// Ensures the acting user is allowed to manage access permissions for the target collections. - /// - private async Task CanManageCollectionAccessAsync(AuthorizationHandlerContext context, - IAuthorizationRequirement requirement, ICollection resources, - CurrentContextOrganization? org) - { - // Owners, Admins, and users with EditAnyCollection permission can always manage collection access - if (org is - { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or - { Permissions.EditAnyCollection: true }) - { - context.Succeed(requirement); - return; - } - - // The limit collection management setting is disabled, - // ensure acting user has manage permissions for all collections being deleted - if (org is { LimitCollectionCreationDeletion: false }) - { - var canManageCollections = await HasCollectionAccessAsync(resources, org, requireManagePermission: true); - if (canManageCollections) - { - context.Succeed(requirement); - return; - } - } - - // Allow providers to manage collections if they are a provider for the target organization - if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId)) - { - context.Succeed(requirement); - } - } - private async Task HasCollectionAccessAsync( ICollection targetCollections, CurrentContextOrganization org, diff --git a/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionOperations.cs b/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionOperations.cs new file mode 100644 index 0000000000..7eacc37514 --- /dev/null +++ b/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionOperations.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace Bit.Api.Vault.AuthorizationHandlers.Collections; + +public class BulkCollectionOperationRequirement : OperationAuthorizationRequirement { } + +public static class BulkCollectionOperations +{ + public static readonly BulkCollectionOperationRequirement Create = new() { Name = nameof(Create) }; + public static readonly BulkCollectionOperationRequirement Read = new() { Name = nameof(Read) }; + public static readonly BulkCollectionOperationRequirement ReadAccess = new() { Name = nameof(ReadAccess) }; + public static readonly BulkCollectionOperationRequirement Update = new() { Name = nameof(Update) }; + /// + /// The operation that represents creating, updating, or removing collection access. + /// Combined together to allow for a single requirement to be used for each operation + /// as they all currently share the same underlying authorization logic. + /// + public static readonly BulkCollectionOperationRequirement ModifyAccess = new() { Name = nameof(ModifyAccess) }; + public static readonly BulkCollectionOperationRequirement Delete = new() { Name = nameof(Delete) }; +} diff --git a/src/Api/Vault/AuthorizationHandlers/Collections/CollectionOperations.cs b/src/Api/Vault/AuthorizationHandlers/Collections/CollectionOperations.cs index 09c11b3aea..e83d3020ac 100644 --- a/src/Api/Vault/AuthorizationHandlers/Collections/CollectionOperations.cs +++ b/src/Api/Vault/AuthorizationHandlers/Collections/CollectionOperations.cs @@ -6,8 +6,6 @@ public class CollectionOperationRequirement : OperationAuthorizationRequirement { public Guid OrganizationId { get; init; } - public CollectionOperationRequirement() { } - public CollectionOperationRequirement(string name, Guid organizationId) { Name = name; @@ -17,9 +15,6 @@ public class CollectionOperationRequirement : OperationAuthorizationRequirement public static class CollectionOperations { - public static readonly CollectionOperationRequirement Create = new() { Name = nameof(Create) }; - public static readonly CollectionOperationRequirement Read = new() { Name = nameof(Read) }; - public static readonly CollectionOperationRequirement ReadAccess = new() { Name = nameof(ReadAccess) }; public static CollectionOperationRequirement ReadAll(Guid organizationId) { return new CollectionOperationRequirement(nameof(ReadAll), organizationId); @@ -28,12 +23,5 @@ public static class CollectionOperations { return new CollectionOperationRequirement(nameof(ReadAllWithAccess), organizationId); } - public static readonly CollectionOperationRequirement Update = new() { Name = nameof(Update) }; - public static readonly CollectionOperationRequirement Delete = new() { Name = nameof(Delete) }; - /// - /// The operation that represents creating, updating, or removing collection access. - /// Combined together to allow for a single requirement to be used for each operation - /// as they all currently share the same underlying authorization logic. - /// - public static readonly CollectionOperationRequirement ModifyAccess = new() { Name = nameof(ModifyAccess) }; } + diff --git a/test/Api.Test/Controllers/CollectionsControllerTests.cs b/test/Api.Test/Controllers/CollectionsControllerTests.cs index da5f554e97..f156cf4bc0 100644 --- a/test/Api.Test/Controllers/CollectionsControllerTests.cs +++ b/test/Api.Test/Controllers/CollectionsControllerTests.cs @@ -35,7 +35,7 @@ public class CollectionsControllerTests sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), ExpectedCollection(), - Arg.Is>(r => r.Contains(CollectionOperations.Create))) + Arg.Is>(r => r.Contains(BulkCollectionOperations.Create))) .Returns(AuthorizationResult.Success()); _ = await sutProvider.Sut.Post(orgId, collectionRequest); @@ -61,7 +61,7 @@ public class CollectionsControllerTests sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), collection, - Arg.Is>(r => r.Contains(CollectionOperations.Update))) + Arg.Is>(r => r.Contains(BulkCollectionOperations.Update))) .Returns(AuthorizationResult.Success()); _ = await sutProvider.Sut.Put(collection.OrganizationId, collection.Id, collectionRequest); @@ -79,7 +79,7 @@ public class CollectionsControllerTests sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), collection, - Arg.Is>(r => r.Contains(CollectionOperations.Update))) + Arg.Is>(r => r.Contains(BulkCollectionOperations.Update))) .Returns(AuthorizationResult.Failed()); sutProvider.GetDependency() @@ -160,7 +160,7 @@ public class CollectionsControllerTests sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), collections, - Arg.Is>(r => r.Contains(CollectionOperations.Delete))) + Arg.Is>(r => r.Contains(BulkCollectionOperations.Delete))) .Returns(AuthorizationResult.Success()); // Act @@ -202,7 +202,7 @@ public class CollectionsControllerTests sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), collections, - Arg.Is>(r => r.Contains(CollectionOperations.Delete))) + Arg.Is>(r => r.Contains(BulkCollectionOperations.Delete))) .Returns(AuthorizationResult.Failed()); // Assert @@ -237,7 +237,7 @@ public class CollectionsControllerTests sutProvider.GetDependency().AuthorizeAsync( Arg.Any(), ExpectedCollectionAccess(), Arg.Is>( - r => r.Contains(CollectionOperations.ModifyAccess) + r => r.Contains(BulkCollectionOperations.ModifyAccess) )) .Returns(AuthorizationResult.Success()); @@ -251,7 +251,7 @@ public class CollectionsControllerTests Arg.Any(), ExpectedCollectionAccess(), Arg.Is>( - r => r.Contains(CollectionOperations.ModifyAccess)) + r => r.Contains(BulkCollectionOperations.ModifyAccess)) ); await sutProvider.GetDependency().Received() .AddAccessAsync( @@ -313,7 +313,7 @@ public class CollectionsControllerTests sutProvider.GetDependency().AuthorizeAsync( Arg.Any(), ExpectedCollectionAccess(), Arg.Is>( - r => r.Contains(CollectionOperations.ModifyAccess) + r => r.Contains(BulkCollectionOperations.ModifyAccess) )) .Returns(AuthorizationResult.Failed()); @@ -324,7 +324,7 @@ public class CollectionsControllerTests Arg.Any(), ExpectedCollectionAccess(), Arg.Is>( - r => r.Contains(CollectionOperations.ModifyAccess)) + r => r.Contains(BulkCollectionOperations.ModifyAccess)) ); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() .AddAccessAsync(default, default, default); diff --git a/test/Api.Test/Vault/AuthorizationHandlers/BulkCollectionAuthorizationHandlerTests.cs b/test/Api.Test/Vault/AuthorizationHandlers/BulkCollectionAuthorizationHandlerTests.cs index 75e508f058..f2fd99c71d 100644 --- a/test/Api.Test/Vault/AuthorizationHandlers/BulkCollectionAuthorizationHandlerTests.cs +++ b/test/Api.Test/Vault/AuthorizationHandlers/BulkCollectionAuthorizationHandlerTests.cs @@ -22,35 +22,25 @@ namespace Bit.Api.Test.Vault.AuthorizationHandlers; public class BulkCollectionAuthorizationHandlerTests { [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 sutProvider, + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task CanCreateAsync_WhenAdminOrOwner_Success( + OrganizationUserType userType, + Guid userId, SutProvider sutProvider, ICollection collections, - ICollection collectionDetails, CurrentContextOrganization organization) { - var actingUserId = Guid.NewGuid(); - foreach (var collectionDetail in collectionDetails) - { - collectionDetail.Manage = manageCollections; - } - organization.Type = userType; - organization.Permissions.EditAnyCollection = editAnyCollection; + organization.LimitCollectionCreationDeletion = true; + organization.Permissions = new Permissions(); var context = new AuthorizationHandlerContext( - new[] { CollectionOperations.ModifyAccess }, + new[] { BulkCollectionOperations.Create }, new ClaimsPrincipal(), collections); - sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().UserId.Returns(userId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collectionDetails); await sutProvider.Sut.HandleAsync(context); @@ -58,24 +48,25 @@ public class BulkCollectionAuthorizationHandlerTests } [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, + [BitAutoData(true, true)] + [BitAutoData(false, false)] + public async Task CanCreateAsync_WhenCustomUserWithRequiredPermissions_Success( + bool createNewCollections, bool limitCollectionCreationDeletion, SutProvider sutProvider, ICollection collections, CurrentContextOrganization organization) { var actingUserId = Guid.NewGuid(); - organization.Type = userType; - organization.Permissions.CreateNewCollections = createNewCollection; - organization.LimitCollectionCreationDeletion = limitCollectionCreateDelete; + organization.Type = OrganizationUserType.Custom; + organization.LimitCollectionCreationDeletion = limitCollectionCreationDeletion; + organization.Permissions = new Permissions + { + CreateNewCollections = createNewCollections + }; var context = new AuthorizationHandlerContext( - new[] { CollectionOperations.Create }, + new[] { BulkCollectionOperations.Create }, new ClaimsPrincipal(), collections); @@ -88,48 +79,693 @@ public class BulkCollectionAuthorizationHandlerTests } [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, + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task CanCreateAsync_WhenMissingPermissions_NoSuccess( + OrganizationUserType userType, SutProvider sutProvider, ICollection collections, - ICollection 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; + organization.LimitCollectionCreationDeletion = true; + organization.Permissions = new Permissions + { + EditAnyCollection = false, + DeleteAnyCollection = false, + ManageGroups = false, + ManageUsers = false + }; var context = new AuthorizationHandlerContext( - new[] { CollectionOperations.Delete }, + new[] { BulkCollectionOperations.Create }, new ClaimsPrincipal(), collections); sutProvider.GetDependency().UserId.Returns(actingUserId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collectionDetails); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanCreateAsync_WhenMissingOrgAccess_NoSuccess( + Guid userId, + ICollection collections, + SutProvider sutProvider) + { + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Create }, + new ClaimsPrincipal(), + collections + ); + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); + + await sutProvider.Sut.HandleAsync(context); + Assert.False(context.HasSucceeded); + } + + [Theory, CollectionCustomization] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task CanReadAsync_WhenAdminOrOwner_Success( + OrganizationUserType userType, + Guid userId, SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + organization.Type = userType; + organization.LimitCollectionCreationDeletion = true; + organization.Permissions = new Permissions(); + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Read }, + new ClaimsPrincipal(), + collections); await sutProvider.Sut.HandleAsync(context); Assert.True(context.HasSucceeded); } + [Theory, CollectionCustomization] + [BitAutoData(true, false, false, false, true)] + [BitAutoData(false, true, false, false, true)] + [BitAutoData(false, false, true, false, true)] + [BitAutoData(false, false, false, true, true)] + [BitAutoData(false, false, false, false, false)] + + public async Task CanReadAsync_WhenCustomUserWithRequiredPermissions_Success( + bool manageUsers, bool editAnyCollection, bool deleteAnyCollection, + bool createNewCollections, bool limitCollectionCreationDeletion, + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = OrganizationUserType.Custom; + organization.LimitCollectionCreationDeletion = limitCollectionCreationDeletion; + organization.Permissions = new Permissions + { + ManageUsers = manageUsers, + EditAnyCollection = editAnyCollection, + DeleteAnyCollection = deleteAnyCollection, + CreateNewCollections = createNewCollections + }; + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Read }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanReadAsync_WhenUserWithRequiredPermissions_Success( + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = OrganizationUserType.User; + organization.LimitCollectionCreationDeletion = false; + organization.Permissions = new Permissions(); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Read }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, CollectionCustomization] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task CanReadAsync_WhenMissingPermissions_NoSuccess( + OrganizationUserType userType, + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = userType; + organization.LimitCollectionCreationDeletion = true; + organization.Permissions = new Permissions + { + EditAnyCollection = false, + DeleteAnyCollection = false, + ManageGroups = false, + ManageUsers = false + }; + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Read }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanReadAsync_WhenMissingOrgAccess_NoSuccess( + Guid userId, + ICollection collections, + SutProvider sutProvider) + { + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Read }, + new ClaimsPrincipal(), + collections + ); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + // + + [Theory, CollectionCustomization] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task CanReadAccessAsync_WhenAdminOrOwner_Success( + OrganizationUserType userType, + Guid userId, SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + organization.Type = userType; + organization.LimitCollectionCreationDeletion = true; + organization.Permissions = new Permissions(); + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.ReadAccess }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, CollectionCustomization] + [BitAutoData(true, false, false, true)] + [BitAutoData(false, true, false, true)] + [BitAutoData(false, false, true, true)] + [BitAutoData(false, false, false, false)] + + public async Task CanReadAccessAsync_WhenCustomUserWithRequiredPermissions_Success( + bool editAnyCollection, bool deleteAnyCollection, + bool createNewCollections, bool limitCollectionCreationDeletion, + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = OrganizationUserType.Custom; + organization.LimitCollectionCreationDeletion = limitCollectionCreationDeletion; + organization.Permissions = new Permissions + { + EditAnyCollection = editAnyCollection, + DeleteAnyCollection = deleteAnyCollection, + CreateNewCollections = createNewCollections + }; + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.ReadAccess }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanReadAccessAsync_WhenUserWithRequiredPermissions_Success( + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = OrganizationUserType.User; + organization.LimitCollectionCreationDeletion = false; + organization.Permissions = new Permissions(); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.ReadAccess }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, CollectionCustomization] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task CanReadAccessAsync_WhenMissingPermissions_NoSuccess( + OrganizationUserType userType, + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = userType; + organization.LimitCollectionCreationDeletion = true; + organization.Permissions = new Permissions + { + EditAnyCollection = false, + DeleteAnyCollection = false, + ManageGroups = false, + ManageUsers = false + }; + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.ReadAccess }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanReadAccessAsync_WhenMissingOrgAccess_NoSuccess( + Guid userId, + ICollection collections, + SutProvider sutProvider) + { + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.ReadAccess }, + new ClaimsPrincipal(), + collections + ); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + // + + [Theory, CollectionCustomization] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task CanManageCollectionAccessAsync_WhenAdminOrOwner_Success( + OrganizationUserType userType, + Guid userId, SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + organization.Type = userType; + organization.LimitCollectionCreationDeletion = true; + organization.Permissions = new Permissions(); + + var operationsToTest = new[] + { + BulkCollectionOperations.Update, BulkCollectionOperations.ModifyAccess + }; + + foreach (var op in operationsToTest) + { + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + var context = new AuthorizationHandlerContext( + new[] { op }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + + // Recreate the SUT to reset the mocks/dependencies between tests + sutProvider.Recreate(); + } + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanManageCollectionAccessAsync_WithEditAnyCollectionPermission_Success( + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = OrganizationUserType.Custom; + organization.Permissions = new Permissions + { + EditAnyCollection = true + }; + + var operationsToTest = new[] + { + BulkCollectionOperations.Update, BulkCollectionOperations.ModifyAccess + }; + + foreach (var op in operationsToTest) + { + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + var context = new AuthorizationHandlerContext( + new[] { op }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + + // Recreate the SUT to reset the mocks/dependencies between tests + sutProvider.Recreate(); + } + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanManageCollectionAccessAsync_WithManageCollectionPermission_Success( + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = OrganizationUserType.User; + organization.Permissions = new Permissions(); + + foreach (var c in collections) + { + c.Manage = true; + } + + var operationsToTest = new[] + { + BulkCollectionOperations.Update, BulkCollectionOperations.ModifyAccess + }; + + foreach (var op in operationsToTest) + { + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); + + var context = new AuthorizationHandlerContext( + new[] { op }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + + // Recreate the SUT to reset the mocks/dependencies between tests + sutProvider.Recreate(); + } + } + + [Theory, CollectionCustomization] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task CanManageCollectionAccessAsync_WhenMissingPermissions_NoSuccess( + OrganizationUserType userType, + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = userType; + organization.LimitCollectionCreationDeletion = false; + organization.Permissions = new Permissions + { + EditAnyCollection = false, + DeleteAnyCollection = false, + ManageGroups = false, + ManageUsers = false + }; + + foreach (var collectionDetail in collections) + { + collectionDetail.Manage = true; + } + // Simulate one collection missing the manage permission + collections.First().Manage = false; + + var operationsToTest = new[] + { + BulkCollectionOperations.Update, BulkCollectionOperations.ModifyAccess + }; + + foreach (var op in operationsToTest) + { + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + var context = new AuthorizationHandlerContext( + new[] { op }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + + // Recreate the SUT to reset the mocks/dependencies between tests + sutProvider.Recreate(); + } + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanManageCollectionAccessAsync_WhenMissingOrgAccess_NoSuccess( + Guid userId, + ICollection collections, + SutProvider sutProvider) + { + var operationsToTest = new[] + { + BulkCollectionOperations.Update, BulkCollectionOperations.ModifyAccess + }; + + foreach (var op in operationsToTest) + { + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); + + var context = new AuthorizationHandlerContext( + new[] { op }, + new ClaimsPrincipal(), + collections + ); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + + // Recreate the SUT to reset the mocks/dependencies between tests + sutProvider.Recreate(); + } + } + + [Theory, CollectionCustomization] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task CanDeleteAsync_WhenAdminOrOwner_Success( + OrganizationUserType userType, + Guid userId, SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + organization.Type = userType; + organization.LimitCollectionCreationDeletion = true; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Delete }, + new ClaimsPrincipal(), + collections); + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanDeleteAsync_WithDeleteAnyCollectionPermission_Success( + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = OrganizationUserType.Custom; + organization.LimitCollectionCreationDeletion = false; + organization.Permissions = new Permissions + { + DeleteAnyCollection = true + }; + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Delete }, + new ClaimsPrincipal(), + collections); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanDeleteAsync_WithManageCollectionPermission_Success( + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = OrganizationUserType.User; + organization.Permissions = new Permissions(); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); + + foreach (var c in collections) + { + c.Manage = true; + } + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Delete }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, CollectionCustomization] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task CanDeleteAsync_WhenMissingPermissions_NoSuccess( + OrganizationUserType userType, + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = userType; + organization.LimitCollectionCreationDeletion = true; + organization.Permissions = new Permissions + { + EditAnyCollection = false, + DeleteAnyCollection = false, + ManageGroups = false, + ManageUsers = false + }; + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Delete }, + new ClaimsPrincipal(), + collections); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanDeleteAsync_WhenMissingOrgAccess_NoSuccess( + Guid userId, + ICollection collections, + SutProvider sutProvider) + { + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Delete }, + new ClaimsPrincipal(), + collections + ); + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); + + await sutProvider.Sut.HandleAsync(context); + Assert.False(context.HasSucceeded); + } + [Theory, BitAutoData, CollectionCustomization] public async Task HandleRequirementAsync_MissingUserId_Failure( SutProvider sutProvider, ICollection collections) { var context = new AuthorizationHandlerContext( - new[] { CollectionOperations.Create }, + new[] { BulkCollectionOperations.Create }, new ClaimsPrincipal(), collections ); @@ -153,7 +789,7 @@ public class BulkCollectionAuthorizationHandlerTests collections.First().OrganizationId = Guid.NewGuid(); var context = new AuthorizationHandlerContext( - new[] { CollectionOperations.Create }, + new[] { BulkCollectionOperations.Create }, new ClaimsPrincipal(), collections ); @@ -165,51 +801,38 @@ public class BulkCollectionAuthorizationHandlerTests sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetOrganization(default); } - [Theory, BitAutoData, CollectionCustomization] - public async Task HandleRequirementAsync_MissingOrg_NoSuccess( - SutProvider sutProvider, - ICollection collections) - { - var actingUserId = Guid.NewGuid(); - - var context = new AuthorizationHandlerContext( - new[] { CollectionOperations.Create }, - new ClaimsPrincipal(), - collections - ); - - sutProvider.GetDependency().UserId.Returns(actingUserId); - sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); - - await sutProvider.Sut.HandleAsync(context); - Assert.False(context.HasSucceeded); - } - [Theory, BitAutoData, CollectionCustomization] public async Task HandleRequirementAsync_Provider_Success( SutProvider sutProvider, ICollection collections) { + var actingUserId = Guid.NewGuid(); + var orgId = collections.First().OrganizationId; + var operationsToTest = new[] { - CollectionOperations.Create, CollectionOperations.Delete, CollectionOperations.ModifyAccess + BulkCollectionOperations.Create, + BulkCollectionOperations.Read, + BulkCollectionOperations.ReadAccess, + BulkCollectionOperations.Update, + BulkCollectionOperations.ModifyAccess, + BulkCollectionOperations.Delete, }; foreach (var op in operationsToTest) { - var actingUserId = Guid.NewGuid(); + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(orgId).Returns((CurrentContextOrganization)null); + sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(true); + var context = new AuthorizationHandlerContext( new[] { op }, new ClaimsPrincipal(), collections ); - var orgId = collections.First().OrganizationId; - - sutProvider.GetDependency().UserId.Returns(actingUserId); - sutProvider.GetDependency().GetOrganization(orgId).Returns((CurrentContextOrganization)null); - sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(true); await sutProvider.Sut.HandleAsync(context); + Assert.True(context.HasSucceeded); await sutProvider.GetDependency().Received().ProviderUserForOrgAsync(orgId); @@ -217,41 +840,4 @@ public class BulkCollectionAuthorizationHandlerTests sutProvider.Recreate(); } } - - [Theory, BitAutoData, CollectionCustomization] - public async Task CanManageCollectionAccessAsync_MissingManageCollectionPermission_NoSuccess( - SutProvider sutProvider, - ICollection collections, - ICollection 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().UserId.Returns(actingUserId); - sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns(organization); - sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collectionDetails); - - await sutProvider.Sut.HandleAsync(context); - Assert.False(context.HasSucceeded); - sutProvider.GetDependency().ReceivedWithAnyArgs().GetOrganization(default); - await sutProvider.GetDependency().ReceivedWithAnyArgs() - .GetManyByUserIdAsync(default); - } }