diff --git a/src/Api/AdminConsole/Controllers/GroupsController.cs b/src/Api/AdminConsole/Controllers/GroupsController.cs index 9946c192b..447ea4bdc 100644 --- a/src/Api/AdminConsole/Controllers/GroupsController.cs +++ b/src/Api/AdminConsole/Controllers/GroupsController.cs @@ -1,12 +1,16 @@ using Bit.Api.AdminConsole.Models.Request; using Bit.Api.AdminConsole.Models.Response; using Bit.Api.Models.Response; +using Bit.Api.Utilities; +using Bit.Api.Vault.AuthorizationHandlers.Groups; +using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Repositories; +using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -23,6 +27,10 @@ public class GroupsController : Controller private readonly ICurrentContext _currentContext; private readonly ICreateGroupCommand _createGroupCommand; private readonly IUpdateGroupCommand _updateGroupCommand; + private readonly IFeatureService _featureService; + private readonly IAuthorizationService _authorizationService; + + private bool UseFlexibleCollections => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); public GroupsController( IGroupRepository groupRepository, @@ -31,7 +39,9 @@ public class GroupsController : Controller ICurrentContext currentContext, ICreateGroupCommand createGroupCommand, IUpdateGroupCommand updateGroupCommand, - IDeleteGroupCommand deleteGroupCommand) + IDeleteGroupCommand deleteGroupCommand, + IFeatureService featureService, + IAuthorizationService authorizationService) { _groupRepository = groupRepository; _groupService = groupService; @@ -40,6 +50,8 @@ public class GroupsController : Controller _createGroupCommand = createGroupCommand; _updateGroupCommand = updateGroupCommand; _deleteGroupCommand = deleteGroupCommand; + _featureService = featureService; + _authorizationService = authorizationService; } [HttpGet("{id}")] @@ -67,20 +79,26 @@ public class GroupsController : Controller } [HttpGet("")] - public async Task> Get(string orgId) + public async Task> Get(Guid orgId) { - var orgIdGuid = new Guid(orgId); - var canAccess = await _currentContext.ManageGroups(orgIdGuid) || - await _currentContext.ViewAssignedCollections(orgIdGuid) || - await _currentContext.ViewAllCollections(orgIdGuid) || - await _currentContext.ManageUsers(orgIdGuid); + if (UseFlexibleCollections) + { + // New flexible collections logic + return await Get_vNext(orgId); + } + + // Old pre-flexible collections logic follows + var canAccess = await _currentContext.ManageGroups(orgId) || + await _currentContext.ViewAssignedCollections(orgId) || + await _currentContext.ViewAllCollections(orgId) || + await _currentContext.ManageUsers(orgId); if (!canAccess) { throw new NotFoundException(); } - var groups = await _groupRepository.GetManyWithCollectionsByOrganizationIdAsync(orgIdGuid); + var groups = await _groupRepository.GetManyWithCollectionsByOrganizationIdAsync(orgId); var responses = groups.Select(g => new GroupDetailsResponseModel(g.Item1, g.Item2)); return new ListResponseModel(responses); } @@ -185,4 +203,18 @@ public class GroupsController : Controller await _groupService.DeleteUserAsync(group, new Guid(orgUserId)); } + + private async Task> Get_vNext(Guid orgId) + { + var authorized = + (await _authorizationService.AuthorizeAsync(User, GroupOperations.ReadAll(orgId))).Succeeded; + if (!authorized) + { + throw new NotFoundException(); + } + + var groups = await _groupRepository.GetManyWithCollectionsByOrganizationIdAsync(orgId); + var responses = groups.Select(g => new GroupDetailsResponseModel(g.Item1, g.Item2)); + return new ListResponseModel(responses); + } } diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 5ca1003fd..703696c6a 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -2,6 +2,9 @@ using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response; +using Bit.Api.Utilities; +using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers; +using Bit.Core; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; @@ -36,6 +39,10 @@ public class OrganizationUsersController : Controller private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; private readonly IUpdateOrganizationUserGroupsCommand _updateOrganizationUserGroupsCommand; private readonly IAcceptOrgUserCommand _acceptOrgUserCommand; + private readonly IFeatureService _featureService; + private readonly IAuthorizationService _authorizationService; + + private bool UseFlexibleCollections => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); public OrganizationUsersController( IOrganizationRepository organizationRepository, @@ -49,7 +56,9 @@ public class OrganizationUsersController : Controller ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery, IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand, - IAcceptOrgUserCommand acceptOrgUserCommand) + IAcceptOrgUserCommand acceptOrgUserCommand, + IFeatureService featureService, + IAuthorizationService authorizationService) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -63,6 +72,8 @@ public class OrganizationUsersController : Controller _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; _updateOrganizationUserGroupsCommand = updateOrganizationUserGroupsCommand; _acceptOrgUserCommand = acceptOrgUserCommand; + _featureService = featureService; + _authorizationService = authorizationService; } [HttpGet("{id}")] @@ -85,18 +96,20 @@ public class OrganizationUsersController : Controller } [HttpGet("")] - public async Task> Get(string orgId, bool includeGroups = false, bool includeCollections = false) + public async Task> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false) { - var orgGuidId = new Guid(orgId); - if (!await _currentContext.ViewAllCollections(orgGuidId) && - !await _currentContext.ViewAssignedCollections(orgGuidId) && - !await _currentContext.ManageGroups(orgGuidId) && - !await _currentContext.ManageUsers(orgGuidId)) + var authorized = UseFlexibleCollections + ? (await _authorizationService.AuthorizeAsync(User, OrganizationUserOperations.ReadAll(orgId))).Succeeded + : await _currentContext.ViewAllCollections(orgId) || + await _currentContext.ViewAssignedCollections(orgId) || + await _currentContext.ManageGroups(orgId) || + await _currentContext.ManageUsers(orgId); + if (!authorized) { throw new NotFoundException(); } - var organizationUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(orgGuidId, includeGroups, includeCollections); + var organizationUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(orgId, includeGroups, includeCollections); var responseTasks = organizationUsers.Select(async o => new OrganizationUserUserDetailsResponseModel(o, await _userService.TwoFactorIsEnabledAsync(o))); var responses = await Task.WhenAll(responseTasks); diff --git a/src/Api/Controllers/CollectionsController.cs b/src/Api/Controllers/CollectionsController.cs index 8d8e737a6..abdb3f9fd 100644 --- a/src/Api/Controllers/CollectionsController.cs +++ b/src/Api/Controllers/CollectionsController.cs @@ -1,5 +1,6 @@ using Bit.Api.Models.Request; using Bit.Api.Models.Response; +using Bit.Api.Utilities; using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Core; using Bit.Core.Context; @@ -42,6 +43,7 @@ public class CollectionsController : Controller IOrganizationUserRepository organizationUserRepository) { _collectionRepository = collectionRepository; + _organizationUserRepository = organizationUserRepository; _collectionService = collectionService; _deleteCollectionCommand = deleteCollectionCommand; _userService = userService; @@ -57,18 +59,33 @@ public class CollectionsController : Controller [HttpGet("{id}")] public async Task Get(Guid orgId, Guid id) { + if (FlexibleCollectionsIsEnabled) + { + // New flexible collections logic + return await Get_vNext(id); + } + + // Old pre-flexible collections logic follows if (!await CanViewCollectionAsync(orgId, id)) { throw new NotFoundException(); } var collection = await GetCollectionAsync(id, orgId); + return new CollectionResponseModel(collection); } [HttpGet("{id}/details")] public async Task GetDetails(Guid orgId, Guid id) { + if (FlexibleCollectionsIsEnabled) + { + // New flexible collections logic + return await GetDetails_vNext(id); + } + + // Old pre-flexible collections logic follows if (!await ViewAtLeastOneCollectionAsync(orgId) && !await _currentContext.ManageUsers(orgId)) { throw new NotFoundException(); @@ -100,8 +117,14 @@ public class CollectionsController : Controller [HttpGet("details")] public async Task> GetManyWithDetails(Guid orgId) { - if (!await ViewAtLeastOneCollectionAsync(orgId) && !await _currentContext.ManageUsers(orgId) && - !await _currentContext.ManageGroups(orgId)) + if (FlexibleCollectionsIsEnabled) + { + // New flexible collections logic + return await GetManyWithDetails_vNext(orgId); + } + + // Old pre-flexible collections logic follows + if (!await ViewAtLeastOneCollectionAsync(orgId) && !await _currentContext.ManageUsers(orgId) && !await _currentContext.ManageGroups(orgId)) { throw new NotFoundException(); } @@ -135,8 +158,14 @@ public class CollectionsController : Controller [HttpGet("")] public async Task> Get(Guid orgId) { - IEnumerable orgCollections = await _collectionService.GetOrganizationCollectionsAsync(orgId); + if (FlexibleCollectionsIsEnabled) + { + // New flexible collections logic + return await GetByOrgId_vNext(orgId); + } + // Old pre-flexible collections logic follows + var orgCollections = await _collectionService.GetOrganizationCollectionsAsync(orgId); var responses = orgCollections.Select(c => new CollectionResponseModel(c)); return new ListResponseModel(responses); } @@ -153,6 +182,13 @@ public class CollectionsController : Controller [HttpGet("{id}/users")] public async Task> GetUsers(Guid orgId, Guid id) { + if (FlexibleCollectionsIsEnabled) + { + // New flexible collections logic + return await GetUsers_vNext(id); + } + + // Old pre-flexible collections logic follows var collection = await GetCollectionAsync(id, orgId); var collectionUsers = await _collectionRepository.GetManyUsersByIdAsync(collection.Id); var responses = collectionUsers.Select(cu => new SelectionReadOnlyResponseModel(cu)); @@ -165,7 +201,7 @@ public class CollectionsController : Controller var collection = model.ToCollection(orgId); var authorized = FlexibleCollectionsIsEnabled - ? (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) { @@ -205,6 +241,13 @@ public class CollectionsController : Controller [HttpPost("{id}")] public async Task Put(Guid orgId, Guid id, [FromBody] CollectionRequestModel model) { + if (FlexibleCollectionsIsEnabled) + { + // New flexible collections logic + return await Put_vNext(id, model); + } + + // Old pre-flexible collections logic follows if (!await CanEditCollectionAsync(orgId, id)) { throw new NotFoundException(); @@ -220,6 +263,14 @@ public class CollectionsController : Controller [HttpPut("{id}/users")] public async Task PutUsers(Guid orgId, Guid id, [FromBody] IEnumerable model) { + if (FlexibleCollectionsIsEnabled) + { + // New flexible collections logic + await PutUsers_vNext(id, model); + return; + } + + // Old pre-flexible collections logic follows if (!await CanEditCollectionAsync(orgId, id)) { throw new NotFoundException(); @@ -243,7 +294,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) { @@ -260,16 +311,20 @@ public class CollectionsController : Controller [HttpPost("{id}/delete")] public async Task Delete(Guid orgId, Guid id) { - var collection = await GetCollectionAsync(id, orgId); + if (FlexibleCollectionsIsEnabled) + { + // New flexible collections logic + await Delete_vNext(id); + return; + } - var authorized = FlexibleCollectionsIsEnabled - ? (await _authorizationService.AuthorizeAsync(User, collection, CollectionOperations.Delete)).Succeeded - : await CanDeleteCollectionAsync(orgId, id); - if (!authorized) + // Old pre-flexible collections logic follows + if (!await CanDeleteCollectionAsync(orgId, id)) { throw new NotFoundException(); } + var collection = await GetCollectionAsync(id, orgId); await _deleteCollectionCommand.DeleteAsync(collection); } @@ -281,7 +336,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(); @@ -311,14 +366,33 @@ public class CollectionsController : Controller [HttpDelete("{id}/user/{orgUserId}")] [HttpPost("{id}/delete-user/{orgUserId}")] - public async Task Delete(string orgId, string id, string orgUserId) + public async Task DeleteUser(Guid orgId, Guid id, Guid orgUserId) { - var collection = await GetCollectionAsync(new Guid(id), new Guid(orgId)); - await _collectionService.DeleteUserAsync(collection, new Guid(orgUserId)); + if (FlexibleCollectionsIsEnabled) + { + // New flexible collections logic + await DeleteUser_vNext(id, orgUserId); + return; + } + + // Old pre-flexible collections logic follows + var collection = await GetCollectionAsync(id, orgId); + await _collectionService.DeleteUserAsync(collection, orgUserId); } + private void DeprecatedPermissionsGuard() + { + if (FlexibleCollectionsIsEnabled) + { + throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF."); + } + } + + [Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")] private async Task GetCollectionAsync(Guid id, Guid orgId) { + DeprecatedPermissionsGuard(); + Collection collection = default; if (await _currentContext.ViewAllCollections(orgId)) { @@ -337,14 +411,6 @@ public class CollectionsController : Controller return collection; } - private void DeprecatedPermissionsGuard() - { - if (FlexibleCollectionsIsEnabled) - { - throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF."); - } - } - [Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")] private async Task CanCreateCollection(Guid orgId, Guid collectionId) { @@ -359,8 +425,11 @@ public class CollectionsController : Controller (o.Permissions?.CreateNewCollections ?? false)) ?? false); } + [Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")] private async Task CanEditCollectionAsync(Guid orgId, Guid collectionId) { + DeprecatedPermissionsGuard(); + if (collectionId == default) { return false; @@ -416,8 +485,11 @@ public class CollectionsController : Controller && (o.Permissions?.DeleteAnyCollection ?? false)) ?? false); } + [Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")] private async Task CanViewCollectionAsync(Guid orgId, Guid collectionId) { + DeprecatedPermissionsGuard(); + if (collectionId == default) { return false; @@ -438,8 +510,166 @@ public class CollectionsController : Controller return false; } + [Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")] private async Task ViewAtLeastOneCollectionAsync(Guid orgId) { + DeprecatedPermissionsGuard(); + return await _currentContext.ViewAllCollections(orgId) || await _currentContext.ViewAssignedCollections(orgId); } + + private async Task Get_vNext(Guid collectionId) + { + var collection = await _collectionRepository.GetByIdAsync(collectionId); + var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Read)).Succeeded; + if (!authorized) + { + throw new NotFoundException(); + } + + return new CollectionResponseModel(collection); + } + + private async Task GetDetails_vNext(Guid id) + { + // New flexible collections logic + var (collection, access) = await _collectionRepository.GetByIdWithAccessAsync(id); + var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ReadWithAccess)).Succeeded; + if (!authorized) + { + throw new NotFoundException(); + } + + return new CollectionAccessDetailsResponseModel(collection, access.Groups, access.Users); + } + + private async Task> GetManyWithDetails_vNext(Guid orgId) + { + // We always need to know which collections the current user is assigned to + var assignedOrgCollections = await _collectionRepository + .GetManyByUserIdWithAccessAsync(_currentContext.UserId.Value, orgId); + + var readAllAuthorized = + (await _authorizationService.AuthorizeAsync(User, CollectionOperations.ReadAllWithAccess(orgId))).Succeeded; + if (readAllAuthorized) + { + // The user can view all collections, but they may not always be assigned to all of them + var allOrgCollections = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(orgId); + + return new ListResponseModel(allOrgCollections.Select(c => + new CollectionAccessDetailsResponseModel(c.Item1, c.Item2.Groups, c.Item2.Users) + { + // Manually determine which collections they're assigned to + Assigned = assignedOrgCollections.Any(ac => ac.Item1.Id == c.Item1.Id) + }) + ); + } + + // Filter the assigned collections to only return those where the user has Manage permission + var manageableOrgCollections = assignedOrgCollections.Where(c => c.Item1.Manage).ToList(); + var readAssignedAuthorized = await _authorizationService.AuthorizeAsync(User, manageableOrgCollections.Select(c => c.Item1), BulkCollectionOperations.ReadWithAccess); + if (!readAssignedAuthorized.Succeeded) + { + throw new NotFoundException(); + } + + return new ListResponseModel(manageableOrgCollections.Select(c => + new CollectionAccessDetailsResponseModel(c.Item1, c.Item2.Groups, c.Item2.Users) + { + Assigned = true // Mapping from manageableOrgCollections implies they're all assigned + }) + ); + } + + private async Task> GetByOrgId_vNext(Guid orgId) + { + IEnumerable orgCollections; + + var readAllAuthorized = (await _authorizationService.AuthorizeAsync(User, CollectionOperations.ReadAll(orgId))).Succeeded; + if (readAllAuthorized) + { + orgCollections = await _collectionRepository.GetManyByOrganizationIdAsync(orgId); + } + else + { + var collections = await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId.Value); + var readAuthorized = (await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.Read)).Succeeded; + if (readAuthorized) + { + orgCollections = collections.Where(c => c.OrganizationId == orgId); + } + else + { + throw new NotFoundException(); + } + } + + var responses = orgCollections.Select(c => new CollectionResponseModel(c)); + return new ListResponseModel(responses); + } + + private async Task> GetUsers_vNext(Guid id) + { + var collection = await _collectionRepository.GetByIdAsync(id); + var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ReadAccess)).Succeeded; + if (!authorized) + { + throw new NotFoundException(); + } + + var collectionUsers = await _collectionRepository.GetManyUsersByIdAsync(collection.Id); + var responses = collectionUsers.Select(cu => new SelectionReadOnlyResponseModel(cu)); + return responses; + } + + private async Task Put_vNext(Guid id, CollectionRequestModel model) + { + var collection = await _collectionRepository.GetByIdAsync(id); + var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Update)).Succeeded; + if (!authorized) + { + throw new NotFoundException(); + } + + var groups = model.Groups?.Select(g => g.ToSelectionReadOnly()); + var users = model.Users?.Select(g => g.ToSelectionReadOnly()); + await _collectionService.SaveAsync(model.ToCollection(collection), groups, users); + return new CollectionResponseModel(collection); + } + + private async Task PutUsers_vNext(Guid id, IEnumerable model) + { + var collection = await _collectionRepository.GetByIdAsync(id); + var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyAccess)).Succeeded; + if (!authorized) + { + throw new NotFoundException(); + } + + await _collectionRepository.UpdateUsersAsync(collection.Id, model?.Select(g => g.ToSelectionReadOnly())); + } + + private async Task Delete_vNext(Guid id) + { + var collection = await _collectionRepository.GetByIdAsync(id); + var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Delete)).Succeeded; + if (!authorized) + { + throw new NotFoundException(); + } + + await _deleteCollectionCommand.DeleteAsync(collection); + } + + private async Task DeleteUser_vNext(Guid id, Guid orgUserId) + { + var collection = await _collectionRepository.GetByIdAsync(id); + var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyAccess)).Succeeded; + if (!authorized) + { + throw new NotFoundException(); + } + + await _collectionService.DeleteUserAsync(collection, orgUserId); + } } diff --git a/src/Api/Utilities/AuthorizationServiceExtensions.cs b/src/Api/Utilities/AuthorizationServiceExtensions.cs new file mode 100644 index 000000000..4f10162cb --- /dev/null +++ b/src/Api/Utilities/AuthorizationServiceExtensions.cs @@ -0,0 +1,32 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Api.Utilities; + +public static class AuthorizationServiceExtensions +{ + /// + /// Checks if a user meets a specific requirement. + /// + /// The providing authorization. + /// The user to evaluate the policy against. + /// The requirement to evaluate the policy against. + /// + /// A flag indicating whether requirement evaluation has succeeded or failed. + /// This value is true when the user fulfills the policy, otherwise false. + /// + public static Task AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, IAuthorizationRequirement requirement) + { + if (service == null) + { + throw new ArgumentNullException(nameof(service)); + } + + if (requirement == null) + { + throw new ArgumentNullException(nameof(requirement)); + } + + return service.AuthorizeAsync(user, resource: null, new[] { requirement }); + } +} diff --git a/src/Api/Utilities/ServiceCollectionExtensions.cs b/src/Api/Utilities/ServiceCollectionExtensions.cs index be6a7a066..a98d6722d 100644 --- a/src/Api/Utilities/ServiceCollectionExtensions.cs +++ b/src/Api/Utilities/ServiceCollectionExtensions.cs @@ -1,4 +1,6 @@ using Bit.Api.Vault.AuthorizationHandlers.Collections; +using Bit.Api.Vault.AuthorizationHandlers.Groups; +using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers; using Bit.Core.IdentityServer; using Bit.Core.Settings; using Bit.Core.Utilities; @@ -120,6 +122,9 @@ public static class ServiceCollectionExtensions public static void AddAuthorizationHandlers(this IServiceCollection services) { + 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 new file mode 100644 index 000000000..8dba839c5 --- /dev/null +++ b/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionAuthorizationHandler.cs @@ -0,0 +1,265 @@ +#nullable enable +using Bit.Core; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; + +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 +{ + private readonly ICurrentContext _currentContext; + private readonly ICollectionRepository _collectionRepository; + private readonly IFeatureService _featureService; + private Guid _targetOrganizationId; + + private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); + + public BulkCollectionAuthorizationHandler( + ICurrentContext currentContext, + ICollectionRepository collectionRepository, + IFeatureService featureService) + { + _currentContext = currentContext; + _collectionRepository = collectionRepository; + _featureService = featureService; + } + + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + BulkCollectionOperationRequirement requirement, ICollection? resources) + { + if (!FlexibleCollectionsIsEnabled) + { + // Flexible collections is OFF, should not be using this handler + throw new FeatureUnavailableException("Flexible collections is OFF when it should be ON."); + } + + // Establish pattern of authorization handler null checking passed resources + if (resources == null || !resources.Any()) + { + context.Fail(); + return; + } + + // Acting user is not authenticated, fail + if (!_currentContext.UserId.HasValue) + { + context.Fail(); + return; + } + + _targetOrganizationId = resources.First().OrganizationId; + + // Ensure all target collections belong to the same organization + if (resources.Any(tc => tc.OrganizationId != _targetOrganizationId)) + { + throw new BadRequestException("Requested collections must belong to the same organization."); + } + + var org = _currentContext.GetOrganization(_targetOrganizationId); + + switch (requirement) + { + case not null when requirement == BulkCollectionOperations.Create: + await CanCreateAsync(context, requirement, org); + break; + + case not null when requirement == BulkCollectionOperations.Read: + case not null when requirement == BulkCollectionOperations.ReadAccess: + await CanReadAsync(context, requirement, resources, org); + break; + + case not null when requirement == BulkCollectionOperations.ReadWithAccess: + await CanReadWithAccessAsync(context, requirement, resources, org); + break; + + case not null when requirement == BulkCollectionOperations.Update: + case not null when requirement == BulkCollectionOperations.ModifyAccess: + await CanUpdateCollection(context, requirement, resources, org); + break; + + case not null when requirement == BulkCollectionOperations.Delete: + await CanDeleteAsync(context, requirement, resources, org); + break; + } + } + + private async Task CanCreateAsync(AuthorizationHandlerContext context, IAuthorizationRequirement requirement, + CurrentContextOrganization? org) + { + // If the limit collection management setting is disabled, allow any user to create collections + // Otherwise, Owners, Admins, and users with CreateNewCollections permission can always create collections + if (org is + { LimitCollectionCreationDeletion: false } or + { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or + { Permissions.CreateNewCollections: true }) + { + context.Succeed(requirement); + return; + } + + // Allow provider users to create collections if they are a provider for the target organization + if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId)) + { + context.Succeed(requirement); + } + } + + private async Task CanReadAsync(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 + { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or + { Permissions.EditAnyCollection: true } or + { Permissions.DeleteAnyCollection: 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 isAssignedToCollections = await IsAssignedToCollectionsAsync(resources, org, false); + if (isAssignedToCollections) + { + 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 CanReadWithAccessAsync(AuthorizationHandlerContext context, IAuthorizationRequirement requirement, + ICollection resources, CurrentContextOrganization? org) + { + // Owners, Admins, and users with EditAnyCollection, DeleteAnyCollection or ManageUsers permission can always read a collection + if (org is + { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or + { Permissions.EditAnyCollection: true } or + { Permissions.DeleteAnyCollection: true } or + { Permissions.ManageUsers: true }) + { + context.Succeed(requirement); + return; + } + + // The acting user is a member of the target organization, + // ensure they have access with manage permission for the collection being read + if (org is not null) + { + var isAssignedToCollections = await IsAssignedToCollectionsAsync(resources, org, true); + if (isAssignedToCollections) + { + 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 update the target collections or manage access permissions for them. + /// + private async Task CanUpdateCollection(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 IsAssignedToCollectionsAsync(resources, org, 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 + if (org is + { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or + { Permissions.DeleteAnyCollection: 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 IsAssignedToCollectionsAsync(resources, org, true); + if (canManageCollections) + { + context.Succeed(requirement); + return; + } + } + + // Allow providers to delete collections if they are a provider for the target organization + if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId)) + { + context.Succeed(requirement); + } + } + + private async Task IsAssignedToCollectionsAsync( + ICollection targetCollections, + CurrentContextOrganization org, + bool requireManagePermission) + { + // List of collection Ids the acting user has access to + var assignedCollectionIds = + (await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId!.Value)) + .Where(c => + // Check Collections with Manage permission + (!requireManagePermission || c.Manage) && c.OrganizationId == org.Id) + .Select(c => c.Id) + .ToHashSet(); + + // Check if the acting user has access to all target collections + return targetCollections.All(tc => assignedCollectionIds.Contains(tc.Id)); + } +} diff --git a/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionOperations.cs b/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionOperations.cs new file mode 100644 index 000000000..fc60db6e5 --- /dev/null +++ b/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionOperations.cs @@ -0,0 +1,21 @@ +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 ReadWithAccess = new() { Name = nameof(ReadWithAccess) }; + 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/CollectionAuthorizationHandler.cs b/src/Api/Vault/AuthorizationHandlers/Collections/CollectionAuthorizationHandler.cs index 9bbfc94ff..419b542ab 100644 --- a/src/Api/Vault/AuthorizationHandlers/Collections/CollectionAuthorizationHandler.cs +++ b/src/Api/Vault/AuthorizationHandlers/Collections/CollectionAuthorizationHandler.cs @@ -1,176 +1,108 @@ #nullable enable using Bit.Core; using Bit.Core.Context; -using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; namespace Bit.Api.Vault.AuthorizationHandlers.Collections; /// -/// Handles authorization logic for Collection objects, including access permissions for users and groups. +/// Handles authorization logic for Collection operations. /// This uses new logic implemented in the Flexible Collections initiative. /// -public class CollectionAuthorizationHandler : BulkAuthorizationHandler +public class CollectionAuthorizationHandler : AuthorizationHandler { private readonly ICurrentContext _currentContext; - private readonly ICollectionRepository _collectionRepository; private readonly IFeatureService _featureService; - private Guid _targetOrganizationId; - public CollectionAuthorizationHandler(ICurrentContext currentContext, ICollectionRepository collectionRepository, + private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); + + public CollectionAuthorizationHandler( + ICurrentContext currentContext, IFeatureService featureService) { _currentContext = currentContext; - _collectionRepository = collectionRepository; _featureService = featureService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, - CollectionOperationRequirement requirement, ICollection? resources) + CollectionOperationRequirement requirement) { - if (!_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext)) + if (!FlexibleCollectionsIsEnabled) { // Flexible collections is OFF, should not be using this handler throw new FeatureUnavailableException("Flexible collections is OFF when it should be ON."); } - // Establish pattern of authorization handler null checking passed resources - if (resources == null || !resources.Any()) - { - context.Fail(); - return; - } - + // Acting user is not authenticated, fail if (!_currentContext.UserId.HasValue) { context.Fail(); return; } - _targetOrganizationId = resources.First().OrganizationId; - - // Ensure all target collections belong to the same organization - if (resources.Any(tc => tc.OrganizationId != _targetOrganizationId)) + if (requirement.OrganizationId == default) { - throw new BadRequestException("Requested collections must belong to the same organization."); + context.Fail(); + return; } - var org = _currentContext.GetOrganization(_targetOrganizationId); + var org = _currentContext.GetOrganization(requirement.OrganizationId); switch (requirement) { - case not null when requirement == CollectionOperations.Create: - await CanCreateAsync(context, requirement, org); + case not null when requirement.Name == nameof(CollectionOperations.ReadAll): + await CanReadAllAsync(context, requirement, org); break; - case not null when requirement == CollectionOperations.Delete: - await CanDeleteAsync(context, requirement, resources, org); - break; - - case not null when requirement == CollectionOperations.ModifyAccess: - await CanManageCollectionAccessAsync(context, requirement, resources, org); + case not null when requirement.Name == nameof(CollectionOperations.ReadAllWithAccess): + await CanReadAllWithAccessAsync(context, requirement, org); break; } } - private async Task CanCreateAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement, + private async Task CanReadAllAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement, CurrentContextOrganization? org) { - // If the limit collection management setting is disabled, allow any user to create collections - // Otherwise, Owners, Admins, and users with CreateNewCollections permission can always create collections + // Owners, Admins, and users with EditAnyCollection, DeleteAnyCollection, + // or AccessImportExport permission can always read a collection if (org is - { LimitCollectionCreationDeletion: false } or { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or - { Permissions.CreateNewCollections: true }) + { Permissions.EditAnyCollection: true } or + { Permissions.DeleteAnyCollection: true } or + { Permissions.AccessImportExport: true } or + { Permissions.ManageGroups: true }) { context.Succeed(requirement); return; } - // Allow provider users to create collections if they are a provider for the target organization - if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId)) + // Allow provider users to read collections if they are a provider for the target organization + if (await _currentContext.ProviderUserForOrgAsync(requirement.OrganizationId)) { context.Succeed(requirement); } } - private async Task CanDeleteAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement, - ICollection resources, CurrentContextOrganization? org) - { - // Owners, Admins, and users with DeleteAnyCollection permission can always delete collections - if (org is - { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or - { Permissions.DeleteAnyCollection: 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 manageableCollectionIds = - (await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId!.Value)) - .Where(c => c.Manage && c.OrganizationId == org.Id) - .Select(c => c.Id) - .ToHashSet(); - - // The acting user has permission to manage all target collections, succeed - if (resources.All(c => manageableCollectionIds.Contains(c.Id))) - { - context.Succeed(requirement); - return; - } - } - - // Allow providers to delete 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 targetCollections, + private async Task CanReadAllWithAccessAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement, CurrentContextOrganization? org) { - // Owners, Admins, and users with EditAnyCollection permission can always manage collection access + // Owners, Admins, and users with EditAnyCollection or DeleteAnyCollection + // permission can always read a collection if (org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or - { Permissions.EditAnyCollection: true }) + { Permissions.EditAnyCollection: true } or + { Permissions.DeleteAnyCollection: true } or + { Permissions.ManageUsers: true }) { context.Succeed(requirement); return; } - // Only check collection management permissions if the user is a member of the target organization (org != null) - if (org is not null) - { - var manageableCollectionIds = - (await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId!.Value)) - .Where(c => c.Manage && c.OrganizationId == org.Id) - .Select(c => c.Id) - .ToHashSet(); - - // The acting user has permission to manage all target collections, succeed - if (targetCollections.All(c => manageableCollectionIds.Contains(c.Id))) - { - context.Succeed(requirement); - return; - } - } - - // Allow providers to manage collections if they are a provider for the target organization - if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId)) + // Allow provider users to read collections if they are a provider for the target organization + if (await _currentContext.ProviderUserForOrgAsync(requirement.OrganizationId)) { context.Succeed(requirement); } diff --git a/src/Api/Vault/AuthorizationHandlers/Collections/CollectionOperations.cs b/src/Api/Vault/AuthorizationHandlers/Collections/CollectionOperations.cs index 8fccce433..e83d3020a 100644 --- a/src/Api/Vault/AuthorizationHandlers/Collections/CollectionOperations.cs +++ b/src/Api/Vault/AuthorizationHandlers/Collections/CollectionOperations.cs @@ -2,16 +2,26 @@ namespace Bit.Api.Vault.AuthorizationHandlers.Collections; -public class CollectionOperationRequirement : OperationAuthorizationRequirement { } +public class CollectionOperationRequirement : OperationAuthorizationRequirement +{ + public Guid OrganizationId { get; init; } + + public CollectionOperationRequirement(string name, Guid organizationId) + { + Name = name; + OrganizationId = organizationId; + } +} public static class CollectionOperations { - public static readonly CollectionOperationRequirement Create = new() { Name = nameof(Create) }; - 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) }; + public static CollectionOperationRequirement ReadAll(Guid organizationId) + { + return new CollectionOperationRequirement(nameof(ReadAll), organizationId); + } + public static CollectionOperationRequirement ReadAllWithAccess(Guid organizationId) + { + return new CollectionOperationRequirement(nameof(ReadAllWithAccess), organizationId); + } } + diff --git a/src/Api/Vault/AuthorizationHandlers/Groups/GroupAuthorizationHandler.cs b/src/Api/Vault/AuthorizationHandlers/Groups/GroupAuthorizationHandler.cs new file mode 100644 index 000000000..0081fdfd7 --- /dev/null +++ b/src/Api/Vault/AuthorizationHandlers/Groups/GroupAuthorizationHandler.cs @@ -0,0 +1,86 @@ +#nullable enable +using Bit.Core; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Services; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Api.Vault.AuthorizationHandlers.Groups; + +/// +/// Handles authorization logic for Group operations. +/// This uses new logic implemented in the Flexible Collections initiative. +/// +public class GroupAuthorizationHandler : AuthorizationHandler +{ + private readonly ICurrentContext _currentContext; + private readonly IFeatureService _featureService; + + private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); + + public GroupAuthorizationHandler( + ICurrentContext currentContext, + IFeatureService featureService) + { + _currentContext = currentContext; + _featureService = featureService; + } + + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + GroupOperationRequirement requirement) + { + if (!FlexibleCollectionsIsEnabled) + { + // Flexible collections is OFF, should not be using this handler + throw new FeatureUnavailableException("Flexible collections is OFF when it should be ON."); + } + + // Acting user is not authenticated, fail + if (!_currentContext.UserId.HasValue) + { + context.Fail(); + return; + } + + if (requirement.OrganizationId == default) + { + context.Fail(); + return; + } + + var org = _currentContext.GetOrganization(requirement.OrganizationId); + + switch (requirement) + { + case not null when requirement.Name == nameof(GroupOperations.ReadAll): + await CanReadAllAsync(context, requirement, org); + break; + } + } + + private async Task CanReadAllAsync(AuthorizationHandlerContext context, GroupOperationRequirement requirement, + CurrentContextOrganization? org) + { + // If the limit collection management setting is disabled, allow any user to read all groups + // Otherwise, Owners, Admins, and users with any of ManageGroups, ManageUsers, EditAnyCollection, DeleteAnyCollection, CreateNewCollections permissions can always read all groups + if (org is + { LimitCollectionCreationDeletion: false } or + { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or + { Permissions.ManageGroups: true } or + { Permissions.ManageUsers: true } or + { Permissions.EditAnyCollection: true } or + { Permissions.DeleteAnyCollection: true } or + { Permissions.CreateNewCollections: true }) + { + context.Succeed(requirement); + return; + } + + // Allow provider users to read all groups if they are a provider for the target organization + if (await _currentContext.ProviderUserForOrgAsync(requirement.OrganizationId)) + { + context.Succeed(requirement); + } + } +} diff --git a/src/Api/Vault/AuthorizationHandlers/Groups/GroupOperations.cs b/src/Api/Vault/AuthorizationHandlers/Groups/GroupOperations.cs new file mode 100644 index 000000000..7735bd52a --- /dev/null +++ b/src/Api/Vault/AuthorizationHandlers/Groups/GroupOperations.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace Bit.Api.Vault.AuthorizationHandlers.Groups; + +public class GroupOperationRequirement : OperationAuthorizationRequirement +{ + public Guid OrganizationId { get; init; } + + public GroupOperationRequirement(string name, Guid organizationId) + { + Name = name; + OrganizationId = organizationId; + } +} + +public static class GroupOperations +{ + public static GroupOperationRequirement ReadAll(Guid organizationId) + { + return new GroupOperationRequirement(nameof(ReadAll), organizationId); + } +} diff --git a/src/Api/Vault/AuthorizationHandlers/OrganizationUsers/OrganizationUserAuthorizationHandler.cs b/src/Api/Vault/AuthorizationHandlers/OrganizationUsers/OrganizationUserAuthorizationHandler.cs new file mode 100644 index 000000000..b78b65df3 --- /dev/null +++ b/src/Api/Vault/AuthorizationHandlers/OrganizationUsers/OrganizationUserAuthorizationHandler.cs @@ -0,0 +1,85 @@ +#nullable enable +using Bit.Core; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Services; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers; + +/// +/// Handles authorization logic for OrganizationUser objects. +/// This uses new logic implemented in the Flexible Collections initiative. +/// +public class OrganizationUserAuthorizationHandler : AuthorizationHandler +{ + private readonly ICurrentContext _currentContext; + private readonly IFeatureService _featureService; + + private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); + + public OrganizationUserAuthorizationHandler( + ICurrentContext currentContext, + IFeatureService featureService) + { + _currentContext = currentContext; + _featureService = featureService; + } + + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + OrganizationUserOperationRequirement requirement) + { + if (!FlexibleCollectionsIsEnabled) + { + // Flexible collections is OFF, should not be using this handler + throw new FeatureUnavailableException("Flexible collections is OFF when it should be ON."); + } + + if (!_currentContext.UserId.HasValue) + { + context.Fail(); + return; + } + + if (requirement.OrganizationId == default) + { + context.Fail(); + return; + } + + var org = _currentContext.GetOrganization(requirement.OrganizationId); + + switch (requirement) + { + case not null when requirement.Name == nameof(OrganizationUserOperations.ReadAll): + await CanReadAllAsync(context, requirement, org); + break; + } + } + + private async Task CanReadAllAsync(AuthorizationHandlerContext context, OrganizationUserOperationRequirement requirement, + CurrentContextOrganization? org) + { + // If the limit collection management setting is disabled, allow any user to read all organization users + // Otherwise, Owners, Admins, and users with any of ManageGroups, ManageUsers, EditAnyCollection, DeleteAnyCollection, CreateNewCollections permissions can always read all organization users + if (org is + { LimitCollectionCreationDeletion: false } or + { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or + { Permissions.ManageGroups: true } or + { Permissions.ManageUsers: true } or + { Permissions.EditAnyCollection: true } or + { Permissions.DeleteAnyCollection: true } or + { Permissions.CreateNewCollections: true }) + { + context.Succeed(requirement); + return; + } + + // Allow provider users to read all organization users if they are a provider for the target organization + if (await _currentContext.ProviderUserForOrgAsync(requirement.OrganizationId)) + { + context.Succeed(requirement); + } + } +} diff --git a/src/Api/Vault/AuthorizationHandlers/OrganizationUsers/OrganizationUserOperations.cs b/src/Api/Vault/AuthorizationHandlers/OrganizationUsers/OrganizationUserOperations.cs new file mode 100644 index 000000000..c085083c3 --- /dev/null +++ b/src/Api/Vault/AuthorizationHandlers/OrganizationUsers/OrganizationUserOperations.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers; + +public class OrganizationUserOperationRequirement : OperationAuthorizationRequirement +{ + public Guid OrganizationId { get; } + + public OrganizationUserOperationRequirement(string name, Guid organizationId) + { + Name = name; + OrganizationId = organizationId; + } +} + +public static class OrganizationUserOperations +{ + public static OrganizationUserOperationRequirement ReadAll(Guid organizationId) + { + return new OrganizationUserOperationRequirement(nameof(ReadAll), organizationId); + } +} diff --git a/src/Core/AdminConsole/Models/Data/Permissions.cs b/src/Core/AdminConsole/Models/Data/Permissions.cs index 4d1ef3402..8e94292b3 100644 --- a/src/Core/AdminConsole/Models/Data/Permissions.cs +++ b/src/Core/AdminConsole/Models/Data/Permissions.cs @@ -10,7 +10,9 @@ public class Permissions public bool CreateNewCollections { get; set; } public bool EditAnyCollection { get; set; } public bool DeleteAnyCollection { get; set; } + [Obsolete("Pre-Flexible Collections logic.")] public bool EditAssignedCollections { get; set; } + [Obsolete("Pre-Flexible Collections logic.")] public bool DeleteAssignedCollections { get; set; } public bool ManageGroups { get; set; } public bool ManagePolicies { get; set; } diff --git a/src/Core/Context/CurrentContext.cs b/src/Core/Context/CurrentContext.cs index 06255d796..ea0235ca2 100644 --- a/src/Core/Context/CurrentContext.cs +++ b/src/Core/Context/CurrentContext.cs @@ -5,9 +5,11 @@ using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.Exceptions; using Bit.Core.Identity; using Bit.Core.Models.Data; using Bit.Core.Repositories; +using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; using Microsoft.AspNetCore.Http; @@ -18,11 +20,14 @@ public class CurrentContext : ICurrentContext { private readonly IProviderOrganizationRepository _providerOrganizationRepository; private readonly IProviderUserRepository _providerUserRepository; + private readonly IFeatureService _featureService; private bool _builtHttpContext; private bool _builtClaimsPrincipal; private IEnumerable _providerOrganizationProviderDetails; private IEnumerable _providerUserOrganizations; + private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, this); + public virtual HttpContext HttpContext { get; set; } public virtual Guid? UserId { get; set; } public virtual User User { get; set; } @@ -44,10 +49,12 @@ public class CurrentContext : ICurrentContext public CurrentContext( IProviderOrganizationRepository providerOrganizationRepository, - IProviderUserRepository providerUserRepository) + IProviderUserRepository providerUserRepository, + IFeatureService featureService) { _providerOrganizationRepository = providerOrganizationRepository; _providerUserRepository = providerUserRepository; + _featureService = featureService; } public async virtual Task BuildAsync(HttpContext httpContext, GlobalSettings globalSettings) @@ -338,12 +345,22 @@ public class CurrentContext : ICurrentContext public async Task EditAssignedCollections(Guid orgId) { + if (FlexibleCollectionsIsEnabled) + { + throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF."); + } + return await OrganizationManager(orgId) || (Organizations?.Any(o => o.Id == orgId && (o.Permissions?.EditAssignedCollections ?? false)) ?? false); } public async Task DeleteAssignedCollections(Guid orgId) { + if (FlexibleCollectionsIsEnabled) + { + throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF."); + } + return await OrganizationManager(orgId) || (Organizations?.Any(o => o.Id == orgId && (o.Permissions?.DeleteAssignedCollections ?? false)) ?? false); } @@ -355,6 +372,12 @@ public class CurrentContext : ICurrentContext * Owner, Admin, Manager, and Provider checks are handled via the EditAssigned/DeleteAssigned context calls. * This entire method will be moved to the CollectionAuthorizationHandler in the future */ + + if (FlexibleCollectionsIsEnabled) + { + throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF."); + } + var canCreateNewCollections = false; var org = GetOrganization(orgId); if (org != null) diff --git a/src/Core/Context/ICurrentContext.cs b/src/Core/Context/ICurrentContext.cs index 444843a03..2d3990f32 100644 --- a/src/Core/Context/ICurrentContext.cs +++ b/src/Core/Context/ICurrentContext.cs @@ -45,8 +45,11 @@ public interface ICurrentContext Task AccessReports(Guid orgId); Task EditAnyCollection(Guid orgId); Task ViewAllCollections(Guid orgId); + [Obsolete("Pre-Flexible Collections logic.")] Task EditAssignedCollections(Guid orgId); + [Obsolete("Pre-Flexible Collections logic.")] Task DeleteAssignedCollections(Guid orgId); + [Obsolete("Pre-Flexible Collections logic.")] Task ViewAssignedCollections(Guid orgId); Task ManageGroups(Guid orgId); Task ManagePolicies(Guid orgId); diff --git a/src/Core/Repositories/ICollectionRepository.cs b/src/Core/Repositories/ICollectionRepository.cs index 8b5cc8d7e..79c7a19a9 100644 --- a/src/Core/Repositories/ICollectionRepository.cs +++ b/src/Core/Repositories/ICollectionRepository.cs @@ -10,7 +10,7 @@ public interface ICollectionRepository : IRepository Task> GetByIdWithAccessAsync(Guid id, Guid userId); Task> GetManyByOrganizationIdAsync(Guid organizationId); Task>> GetManyByOrganizationIdWithAccessAsync(Guid organizationId); - Task>> GetManyByUserIdWithAccessAsync(Guid userId, Guid organizationId); + Task>> GetManyByUserIdWithAccessAsync(Guid userId, Guid organizationId); Task GetByIdAsync(Guid id, Guid userId); Task> GetManyByManyIdsAsync(IEnumerable collectionIds); Task> GetManyByUserIdAsync(Guid userId); diff --git a/src/Core/Services/ICollectionService.cs b/src/Core/Services/ICollectionService.cs index 4d392a772..27c411819 100644 --- a/src/Core/Services/ICollectionService.cs +++ b/src/Core/Services/ICollectionService.cs @@ -7,5 +7,6 @@ public interface ICollectionService { Task SaveAsync(Collection collection, IEnumerable groups = null, IEnumerable users = null); Task DeleteUserAsync(Collection collection, Guid organizationUserId); + [Obsolete("Pre-Flexible Collections logic.")] Task> GetOrganizationCollectionsAsync(Guid organizationId); } diff --git a/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs b/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs index b9b0deefb..60299d9dd 100644 --- a/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs @@ -139,7 +139,7 @@ public class CollectionRepository : Repository, ICollectionRep } } - public async Task>> GetManyByUserIdWithAccessAsync(Guid userId, Guid organizationId) + public async Task>> GetManyByUserIdWithAccessAsync(Guid userId, Guid organizationId) { using (var connection = new SqlConnection(ConnectionString)) { @@ -148,14 +148,14 @@ public class CollectionRepository : Repository, ICollectionRep new { UserId = userId }, commandType: CommandType.StoredProcedure); - var collections = (await results.ReadAsync()).Where(c => c.OrganizationId == organizationId); + var collections = (await results.ReadAsync()).Where(c => c.OrganizationId == organizationId); var groups = (await results.ReadAsync()) .GroupBy(g => g.CollectionId); var users = (await results.ReadAsync()) .GroupBy(u => u.CollectionId); return collections.Select(collection => - new Tuple( + new Tuple( collection, new CollectionAccessDetails { @@ -181,7 +181,6 @@ public class CollectionRepository : Repository, ICollectionRep ) ).ToList(); } - } public async Task GetByIdAsync(Guid id, Guid userId) diff --git a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs index e1e778921..2d16c2386 100644 --- a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs @@ -231,7 +231,7 @@ public class CollectionRepository : Repository>> GetManyByUserIdWithAccessAsync(Guid userId, Guid organizationId) + public async Task>> GetManyByUserIdWithAccessAsync(Guid userId, Guid organizationId) { var collections = (await GetManyByUserIdAsync(userId)).Where(c => c.OrganizationId == organizationId).ToList(); using (var scope = ServiceScopeFactory.CreateScope()) @@ -249,7 +249,7 @@ public class CollectionRepository : Repository - new Tuple( + new Tuple( collection, new CollectionAccessDetails { diff --git a/src/Notifications/NotificationsHub.cs b/src/Notifications/NotificationsHub.cs index a86cf329c..d529ee1a0 100644 --- a/src/Notifications/NotificationsHub.cs +++ b/src/Notifications/NotificationsHub.cs @@ -18,7 +18,7 @@ public class NotificationsHub : Microsoft.AspNetCore.SignalR.Hub public override async Task OnConnectedAsync() { - var currentContext = new CurrentContext(null, null); + var currentContext = new CurrentContext(null, null, null); await currentContext.BuildAsync(Context.User, _globalSettings); if (currentContext.Organizations != null) { @@ -33,7 +33,7 @@ public class NotificationsHub : Microsoft.AspNetCore.SignalR.Hub public override async Task OnDisconnectedAsync(Exception exception) { - var currentContext = new CurrentContext(null, null); + var currentContext = new CurrentContext(null, null, null); await currentContext.BuildAsync(Context.User, _globalSettings); if (currentContext.Organizations != null) { diff --git a/test/Api.Test/Controllers/CollectionsControllerTests.cs b/test/Api.Test/Controllers/CollectionsControllerTests.cs index 3919e6f4e..f6156e9c4 100644 --- a/test/Api.Test/Controllers/CollectionsControllerTests.cs +++ b/test/Api.Test/Controllers/CollectionsControllerTests.cs @@ -36,7 +36,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); @@ -48,101 +48,126 @@ public class CollectionsControllerTests } [Theory, BitAutoData] - public async Task Put_Success(Guid orgId, Guid collectionId, Guid userId, CollectionRequestModel collectionRequest, + public async Task Put_Success(Collection collection, CollectionRequestModel collectionRequest, SutProvider sutProvider) { - sutProvider.GetDependency() - .ViewAssignedCollections(orgId) - .Returns(true); - - sutProvider.GetDependency() - .EditAssignedCollections(orgId) - .Returns(true); - - sutProvider.GetDependency() - .UserId - .Returns(userId); + Collection ExpectedCollection() => Arg.Is(c => c.Id == collection.Id && + c.Name == collectionRequest.Name && c.ExternalId == collectionRequest.ExternalId && + c.OrganizationId == collection.OrganizationId); sutProvider.GetDependency() - .GetByIdAsync(collectionId, userId) - .Returns(new CollectionDetails - { - OrganizationId = orgId, - }); + .GetByIdAsync(collection.Id) + .Returns(collection); - _ = await sutProvider.Sut.Put(orgId, collectionId, collectionRequest); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), + collection, + Arg.Is>(r => r.Contains(BulkCollectionOperations.Update))) + .Returns(AuthorizationResult.Success()); + + _ = await sutProvider.Sut.Put(collection.OrganizationId, collection.Id, collectionRequest); + + await sutProvider.GetDependency() + .Received(1) + .SaveAsync(ExpectedCollection(), Arg.Any>(), + Arg.Any>()); } [Theory, BitAutoData] - public async Task Put_CanNotEditAssignedCollection_ThrowsNotFound(Guid orgId, Guid collectionId, Guid userId, CollectionRequestModel collectionRequest, + public async Task Put_WithNoCollectionPermission_ThrowsNotFound(Collection collection, CollectionRequestModel collectionRequest, SutProvider sutProvider) { - sutProvider.GetDependency() - .EditAssignedCollections(orgId) - .Returns(true); - - sutProvider.GetDependency() - .UserId - .Returns(userId); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), + collection, + Arg.Is>(r => r.Contains(BulkCollectionOperations.Update))) + .Returns(AuthorizationResult.Failed()); sutProvider.GetDependency() - .GetByIdAsync(collectionId, userId) - .Returns(Task.FromResult(null)); + .GetByIdAsync(collection.Id) + .Returns(collection); - _ = await Assert.ThrowsAsync(async () => await sutProvider.Sut.Put(orgId, collectionId, collectionRequest)); + _ = await Assert.ThrowsAsync(async () => await sutProvider.Sut.Put(collection.OrganizationId, collection.Id, collectionRequest)); } [Theory, BitAutoData] - public async Task GetOrganizationCollectionsWithGroups_NoManagerPermissions_ThrowsNotFound(Organization organization, SutProvider sutProvider) + public async Task GetOrganizationCollectionsWithGroups_WithReadAllPermissions_GetsAllCollections(Organization organization, Guid userId, SutProvider sutProvider) { - sutProvider.GetDependency().ViewAssignedCollections(organization.Id).Returns(false); + sutProvider.GetDependency().UserId.Returns(userId); - await Assert.ThrowsAsync(() => sutProvider.Sut.GetManyWithDetails(organization.Id)); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyByOrganizationIdWithAccessAsync(default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyByUserIdWithAccessAsync(default, default); - } - - [Theory, BitAutoData] - public async Task GetOrganizationCollectionsWithGroups_AdminPermissions_GetsAllCollections(Organization organization, User user, SutProvider sutProvider) - { - sutProvider.GetDependency().UserId.Returns(user.Id); - sutProvider.GetDependency().ViewAllCollections(organization.Id).Returns(true); - sutProvider.GetDependency().OrganizationAdmin(organization.Id).Returns(true); + sutProvider.GetDependency() + .AuthorizeAsync( + Arg.Any(), + Arg.Any(), + Arg.Is>(requirements => + requirements.Cast().All(operation => + operation.Name == nameof(CollectionOperations.ReadAllWithAccess) + && operation.OrganizationId == organization.Id))) + .Returns(AuthorizationResult.Success()); await sutProvider.Sut.GetManyWithDetails(organization.Id); - await sutProvider.GetDependency().Received().GetManyByOrganizationIdWithAccessAsync(organization.Id); - await sutProvider.GetDependency().Received().GetManyByUserIdWithAccessAsync(user.Id, organization.Id); + await sutProvider.GetDependency().Received(1).GetManyByUserIdWithAccessAsync(userId, organization.Id); + await sutProvider.GetDependency().Received(1).GetManyByOrganizationIdWithAccessAsync(organization.Id); } [Theory, BitAutoData] - public async Task GetOrganizationCollectionsWithGroups_MissingViewAllPermissions_GetsAssignedCollections(Organization organization, User user, SutProvider sutProvider) + public async Task GetOrganizationCollectionsWithGroups_MissingReadAllPermissions_GetsAssignedCollections(Organization organization, Guid userId, SutProvider sutProvider) { - sutProvider.GetDependency().UserId.Returns(user.Id); - sutProvider.GetDependency().ViewAssignedCollections(organization.Id).Returns(true); - sutProvider.GetDependency().OrganizationManager(organization.Id).Returns(true); + sutProvider.GetDependency().UserId.Returns(userId); + + sutProvider.GetDependency() + .AuthorizeAsync( + Arg.Any(), + Arg.Any(), + Arg.Is>(requirements => + requirements.Cast().All(operation => + operation.Name == nameof(CollectionOperations.ReadAllWithAccess) + && operation.OrganizationId == organization.Id))) + .Returns(AuthorizationResult.Failed()); + + sutProvider.GetDependency() + .AuthorizeAsync( + Arg.Any(), + Arg.Any(), + Arg.Is>(requirements => + requirements.Cast().All(operation => + operation.Name == nameof(BulkCollectionOperations.ReadWithAccess)))) + .Returns(AuthorizationResult.Success()); await sutProvider.Sut.GetManyWithDetails(organization.Id); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyByOrganizationIdWithAccessAsync(default); - await sutProvider.GetDependency().Received().GetManyByUserIdWithAccessAsync(user.Id, organization.Id); + await sutProvider.GetDependency().Received(1).GetManyByUserIdWithAccessAsync(userId, organization.Id); + await sutProvider.GetDependency().DidNotReceive().GetManyByOrganizationIdWithAccessAsync(organization.Id); } [Theory, BitAutoData] - public async Task GetOrganizationCollectionsWithGroups_CustomUserWithManagerPermissions_GetsAssignedCollections(Organization organization, User user, SutProvider sutProvider) + public async Task GetOrganizationCollectionsWithGroups_MissingReadPermissions_ThrowsNotFound(Organization organization, Guid userId, SutProvider sutProvider) { - sutProvider.GetDependency().UserId.Returns(user.Id); - sutProvider.GetDependency().ViewAssignedCollections(organization.Id).Returns(true); - sutProvider.GetDependency().EditAssignedCollections(organization.Id).Returns(true); + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency() + .AuthorizeAsync( + Arg.Any(), + Arg.Any(), + Arg.Is>(requirements => + requirements.Cast().All(operation => + operation.Name == nameof(CollectionOperations.ReadAllWithAccess) + && operation.OrganizationId == organization.Id))) + .Returns(AuthorizationResult.Failed()); - await sutProvider.Sut.GetManyWithDetails(organization.Id); + sutProvider.GetDependency() + .AuthorizeAsync( + Arg.Any(), + Arg.Any(), + Arg.Is>(requirements => + requirements.Cast().All(operation => + operation.Name == nameof(BulkCollectionOperations.ReadWithAccess)))) + .Returns(AuthorizationResult.Failed()); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyByOrganizationIdWithAccessAsync(default); - await sutProvider.GetDependency().Received().GetManyByUserIdWithAccessAsync(user.Id, organization.Id); + _ = await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetManyWithDetails(organization.Id)); } - [Theory, BitAutoData] public async Task DeleteMany_Success(Guid orgId, Collection collection1, Collection collection2, SutProvider sutProvider) { @@ -172,7 +197,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 @@ -214,7 +239,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 @@ -249,7 +274,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()); @@ -263,7 +288,7 @@ public class CollectionsControllerTests Arg.Any(), ExpectedCollectionAccess(), Arg.Is>( - r => r.Contains(CollectionOperations.ModifyAccess)) + r => r.Contains(BulkCollectionOperations.ModifyAccess)) ); await sutProvider.GetDependency().Received() .AddAccessAsync( @@ -325,7 +350,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()); @@ -336,7 +361,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 new file mode 100644 index 000000000..59082bb3f --- /dev/null +++ b/test/Api.Test/Vault/AuthorizationHandlers/BulkCollectionAuthorizationHandlerTests.cs @@ -0,0 +1,948 @@ +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 BulkCollectionAuthorizationHandlerTests +{ + [Theory, CollectionCustomization] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task CanCreateAsync_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.Create }, + 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 CanCreateAsync_WhenUser_WithLimitCollectionCreationDeletionFalse_Success( + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = OrganizationUserType.User; + organization.LimitCollectionCreationDeletion = false; + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Create }, + 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, CollectionCustomization] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task CanCreateAsync_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.Create }, + 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 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(); + + var operationsToTest = new[] + { + BulkCollectionOperations.Read, BulkCollectionOperations.ReadAccess + }; + + foreach (var op in operationsToTest) + { + 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); + + // Recreate the SUT to reset the mocks/dependencies between tests + sutProvider.Recreate(); + } + } + + [Theory, CollectionCustomization] + [BitAutoData(true, false)] + [BitAutoData(false, true)] + public async Task CanReadAsync_WhenCustomUserWithRequiredPermissions_Success( + bool editAnyCollection, bool deleteAnyCollection, + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = OrganizationUserType.Custom; + organization.Permissions = new Permissions + { + EditAnyCollection = editAnyCollection, + DeleteAnyCollection = deleteAnyCollection + }; + + var operationsToTest = new[] + { + BulkCollectionOperations.Read, BulkCollectionOperations.ReadAccess + }; + + foreach (var op in operationsToTest) + { + 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); + + // Recreate the SUT to reset the mocks/dependencies between tests + sutProvider.Recreate(); + } + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanReadAsync_WhenUserIsAssignedToCollections_Success( + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = OrganizationUserType.User; + organization.LimitCollectionCreationDeletion = false; + organization.Permissions = new Permissions(); + + var operationsToTest = new[] + { + BulkCollectionOperations.Read, BulkCollectionOperations.ReadAccess + }; + + 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[] { BulkCollectionOperations.Read }, + 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 CanReadAsync_WhenUserIsNotAssignedToCollections_NoSuccess( + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = OrganizationUserType.User; + organization.LimitCollectionCreationDeletion = false; + organization.Permissions = new Permissions(); + + var operationsToTest = new[] + { + BulkCollectionOperations.Read, BulkCollectionOperations.ReadAccess + }; + + foreach (var op in operationsToTest) + { + 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); + + // Recreate the SUT to reset the mocks/dependencies between tests + sutProvider.Recreate(); + } + } + + [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 + }; + + var operationsToTest = new[] + { + BulkCollectionOperations.Read, BulkCollectionOperations.ReadAccess + }; + + foreach (var op in operationsToTest) + { + 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); + + // Recreate the SUT to reset the mocks/dependencies between tests + sutProvider.Recreate(); + } + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanReadAsync_WhenMissingOrgAccess_NoSuccess( + Guid userId, + ICollection collections, + SutProvider sutProvider) + { + var operationsToTest = new[] + { + BulkCollectionOperations.Read, BulkCollectionOperations.ReadAccess + }; + + 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 CanReadWithAccessAsync_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.ReadWithAccess }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, CollectionCustomization] + [BitAutoData(true, false, false)] + [BitAutoData(false, true, false)] + [BitAutoData(false, false, true)] + + public async Task CanReadWithAccessAsync_WhenCustomUserWithRequiredPermissions_Success( + bool editAnyCollection, bool deleteAnyCollection, bool manageUsers, + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = OrganizationUserType.Custom; + organization.Permissions = new Permissions + { + EditAnyCollection = editAnyCollection, + DeleteAnyCollection = deleteAnyCollection, + ManageUsers = manageUsers + }; + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.ReadWithAccess }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanReadWithAccessAsync_WhenUserCanManageCollections_Success( + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + foreach (var c in collections) + { + c.Manage = true; + } + + 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.ReadWithAccess }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanReadWithAccessAsync_WhenUserCanNotManageCollections_Success( + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + foreach (var c in collections) + { + c.Manage = false; + } + + 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.ReadWithAccess }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory, CollectionCustomization] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task CanReadWithAccessAsync_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.ReadWithAccess }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanReadWithAccessAsync_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.ReadWithAccess }, + new ClaimsPrincipal(), + collections + ); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory, CollectionCustomization] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task CanUpdateCollection_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 CanUpdateCollection_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 CanUpdateCollection_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 CanUpdateCollection_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 CanUpdateCollection_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[] { BulkCollectionOperations.Create }, + new ClaimsPrincipal(), + collections + ); + + // Simulate missing user id + sutProvider.GetDependency().UserId.Returns((Guid?)null); + + await sutProvider.Sut.HandleAsync(context); + Assert.True(context.HasFailed); + sutProvider.GetDependency().DidNotReceiveWithAnyArgs(); + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task HandleRequirementAsync_TargetCollectionsMultipleOrgs_Exception( + SutProvider sutProvider, + IList collections) + { + var actingUserId = Guid.NewGuid(); + + // Simulate a collection in a different organization + collections.First().OrganizationId = Guid.NewGuid(); + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Create }, + new ClaimsPrincipal(), + collections + ); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.HandleAsync(context)); + Assert.Equal("Requested collections must belong to the same organization.", exception.Message); + sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetOrganization(default); + } + + [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[] + { + BulkCollectionOperations.Create, + BulkCollectionOperations.Read, + BulkCollectionOperations.ReadAccess, + BulkCollectionOperations.Update, + BulkCollectionOperations.ModifyAccess, + BulkCollectionOperations.Delete, + }; + + foreach (var op in operationsToTest) + { + 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 + ); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + await sutProvider.GetDependency().Received().ProviderUserForOrgAsync(orgId); + + // Recreate the SUT to reset the mocks/dependencies between tests + sutProvider.Recreate(); + } + } +} diff --git a/test/Api.Test/Vault/AuthorizationHandlers/CollectionAuthorizationHandlerTests.cs b/test/Api.Test/Vault/AuthorizationHandlers/CollectionAuthorizationHandlerTests.cs index 6dc63b69e..5bd7b6f84 100644 --- a/test/Api.Test/Vault/AuthorizationHandlers/CollectionAuthorizationHandlerTests.cs +++ b/test/Api.Test/Vault/AuthorizationHandlers/CollectionAuthorizationHandlerTests.cs @@ -2,13 +2,9 @@ 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; @@ -21,63 +17,80 @@ namespace Bit.Api.Test.Vault.AuthorizationHandlers; [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, + [Theory] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task CanReadAllAsync_WhenAdminOrOwner_Success( + OrganizationUserType userType, + Guid userId, SutProvider sutProvider, + CurrentContextOrganization organization) + { + organization.Type = userType; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + new[] { CollectionOperations.ReadAll(organization.Id) }, + new ClaimsPrincipal(), + null); + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task CanReadAllAsync_WhenProviderUser_Success( + Guid userId, + SutProvider sutProvider, CurrentContextOrganization organization) + { + organization.Type = OrganizationUserType.User; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + new[] { CollectionOperations.ReadAll(organization.Id) }, + new ClaimsPrincipal(), + null); + + sutProvider.GetDependency() + .UserId + .Returns(userId); + sutProvider.GetDependency() + .ProviderUserForOrgAsync(organization.Id) + .Returns(true); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory] + [BitAutoData(true, false, false, false)] + [BitAutoData(false, true, false, false)] + [BitAutoData(false, false, true, false)] + [BitAutoData(false, false, false, true)] + public async Task CanReadAllAsync_WhenCustomUserWithRequiredPermissions_Success( + bool editAnyCollection, bool deleteAnyCollection, bool accessImportExport, bool manageGroups, SutProvider sutProvider, - ICollection collections, - ICollection collectionDetails, CurrentContextOrganization organization) { var actingUserId = Guid.NewGuid(); - foreach (var collectionDetail in collectionDetails) + + organization.Type = OrganizationUserType.Custom; + organization.Permissions = new Permissions { - collectionDetail.Manage = manageCollections; - } - - organization.Type = userType; - organization.Permissions.EditAnyCollection = editAnyCollection; + EditAnyCollection = editAnyCollection, + DeleteAnyCollection = deleteAnyCollection, + AccessImportExport = accessImportExport, + ManageGroups = manageGroups + }; var context = new AuthorizationHandlerContext( - new[] { CollectionOperations.ModifyAccess }, + new[] { CollectionOperations.ReadAll(organization.Id) }, 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.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 sutProvider, - ICollection 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); + null); sutProvider.GetDependency().UserId.Returns(actingUserId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); @@ -87,51 +100,177 @@ public class CollectionAuthorizationHandlerTests 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, + [Theory] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task CanReadAllAsync_WhenMissingPermissions_NoSuccess( + OrganizationUserType userType, SutProvider sutProvider, - ICollection collections, - ICollection collectionDetails, CurrentContextOrganization organization) { var actingUserId = Guid.NewGuid(); - foreach (var collectionDetail in collectionDetails) + + organization.Type = userType; + organization.Permissions = new Permissions { - collectionDetail.Manage = manageCollections; - } - - organization.Type = userType; - organization.Permissions.DeleteAnyCollection = deleteAnyCollection; - organization.LimitCollectionCreationDeletion = limitCollectionCreateDelete; + EditAnyCollection = false, + DeleteAnyCollection = false, + AccessImportExport = false + }; var context = new AuthorizationHandlerContext( - new[] { CollectionOperations.Delete }, + new[] { CollectionOperations.ReadAll(organization.Id) }, new ClaimsPrincipal(), - collections); + null); 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(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task CanReadAllWithAccessAsync_WhenAdminOrOwner_Success( + OrganizationUserType userType, + Guid userId, SutProvider sutProvider, + CurrentContextOrganization organization) + { + organization.Type = userType; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + new[] { CollectionOperations.ReadAllWithAccess(organization.Id) }, + new ClaimsPrincipal(), + null); + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); await sutProvider.Sut.HandleAsync(context); Assert.True(context.HasSucceeded); } - [Theory, BitAutoData, CollectionCustomization] + [Theory, BitAutoData] + public async Task CanReadAllWithAccessAsync_WhenProviderUser_Success( + Guid userId, + SutProvider sutProvider, CurrentContextOrganization organization) + { + organization.Type = OrganizationUserType.User; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + new[] { CollectionOperations.ReadAllWithAccess(organization.Id) }, + new ClaimsPrincipal(), + null); + + sutProvider.GetDependency() + .UserId + .Returns(userId); + sutProvider.GetDependency() + .ProviderUserForOrgAsync(organization.Id) + .Returns(true); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory] + [BitAutoData(true, false, false)] + [BitAutoData(false, true, false)] + [BitAutoData(false, false, true)] + public async Task CanReadAllWithAccessAsync_WhenCustomUserWithRequiredPermissions_Success( + bool editAnyCollection, bool deleteAnyCollection, bool manageUsers, + SutProvider sutProvider, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = OrganizationUserType.Custom; + organization.Permissions = new Permissions + { + EditAnyCollection = editAnyCollection, + DeleteAnyCollection = deleteAnyCollection, + ManageUsers = manageUsers + }; + + var context = new AuthorizationHandlerContext( + new[] { CollectionOperations.ReadAllWithAccess(organization.Id) }, + new ClaimsPrincipal(), + null); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task CanReadAllWithAccessAsync_WhenMissingPermissions_NoSuccess( + OrganizationUserType userType, + SutProvider sutProvider, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = userType; + organization.Permissions = new Permissions + { + EditAnyCollection = false, + DeleteAnyCollection = false, + AccessImportExport = false + }; + + var context = new AuthorizationHandlerContext( + new[] { CollectionOperations.ReadAllWithAccess(organization.Id) }, + new ClaimsPrincipal(), + null); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task HandleRequirementAsync_WhenMissingOrgAccess_NoSuccess( + Guid userId, + Guid organizationId, + SutProvider sutProvider) + { + var context = new AuthorizationHandlerContext( + new[] { CollectionOperations.ReadAll(organizationId) }, + new ClaimsPrincipal(), + null + ); + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); + + await sutProvider.Sut.HandleAsync(context); + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData] public async Task HandleRequirementAsync_MissingUserId_Failure( - SutProvider sutProvider, - ICollection collections) + Guid organizationId, + SutProvider sutProvider) { var context = new AuthorizationHandlerContext( - new[] { CollectionOperations.Create }, + new[] { CollectionOperations.ReadAll(organizationId) }, new ClaimsPrincipal(), - collections + null ); // Simulate missing user id @@ -139,119 +278,22 @@ public class CollectionAuthorizationHandlerTests await sutProvider.Sut.HandleAsync(context); Assert.True(context.HasFailed); - sutProvider.GetDependency().DidNotReceiveWithAnyArgs(); } - [Theory, BitAutoData, CollectionCustomization] - public async Task HandleRequirementAsync_TargetCollectionsMultipleOrgs_Exception( - SutProvider sutProvider, - IList collections) + [Theory, BitAutoData] + public async Task HandleRequirementAsync_NoSpecifiedOrgId_Failure( + SutProvider sutProvider) { - var actingUserId = Guid.NewGuid(); - - // Simulate a collection in a different organization - collections.First().OrganizationId = Guid.NewGuid(); - var context = new AuthorizationHandlerContext( - new[] { CollectionOperations.Create }, + new[] { CollectionOperations.ReadAll(default) }, new ClaimsPrincipal(), - collections + null ); - sutProvider.GetDependency().UserId.Returns(actingUserId); - - var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.HandleAsync(context)); - Assert.Equal("Requested collections must belong to the same organization.", exception.Message); - 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); + sutProvider.GetDependency().UserId.Returns(new Guid()); await sutProvider.Sut.HandleAsync(context); - Assert.False(context.HasSucceeded); - } - [Theory, BitAutoData, CollectionCustomization] - public async Task HandleRequirementAsync_Provider_Success( - SutProvider sutProvider, - ICollection collections) - { - var operationsToTest = new[] - { - CollectionOperations.Create, CollectionOperations.Delete, CollectionOperations.ModifyAccess - }; - - foreach (var op in operationsToTest) - { - var actingUserId = Guid.NewGuid(); - 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); - - // Recreate the SUT to reset the mocks/dependencies between tests - 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); + Assert.True(context.HasFailed); } } diff --git a/test/Api.Test/Vault/AuthorizationHandlers/GroupAuthorizationHandlerTests.cs b/test/Api.Test/Vault/AuthorizationHandlers/GroupAuthorizationHandlerTests.cs new file mode 100644 index 000000000..74711c80b --- /dev/null +++ b/test/Api.Test/Vault/AuthorizationHandlers/GroupAuthorizationHandlerTests.cs @@ -0,0 +1,197 @@ +using System.Security.Claims; +using Bit.Api.Vault.AuthorizationHandlers.Groups; +using Bit.Core; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Test.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 GroupAuthorizationHandlerTests +{ + [Theory] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task CanReadAllAsync_WhenAdminOrOwner_Success( + OrganizationUserType userType, + Guid userId, SutProvider sutProvider, + CurrentContextOrganization organization) + { + organization.Type = userType; + organization.LimitCollectionCreationDeletion = true; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + new[] { GroupOperations.ReadAll(organization.Id) }, + new ClaimsPrincipal(), + null); + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task CanReadAllAsync_WithProviderUser_Success( + Guid userId, + SutProvider sutProvider, CurrentContextOrganization organization) + { + organization.Type = OrganizationUserType.User; + organization.LimitCollectionCreationDeletion = true; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + new[] { GroupOperations.ReadAll(organization.Id) }, + new ClaimsPrincipal(), + null); + + sutProvider.GetDependency() + .UserId + .Returns(userId); + sutProvider.GetDependency() + .ProviderUserForOrgAsync(organization.Id) + .Returns(true); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory] + [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 CanReadAllAsync_WhenCustomUserWithRequiredPermissions_Success( + bool editAnyCollection, bool deleteAnyCollection, bool manageGroups, + bool manageUsers, bool limitCollectionCreationDeletion, + SutProvider sutProvider, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = OrganizationUserType.Custom; + organization.LimitCollectionCreationDeletion = limitCollectionCreationDeletion; + organization.Permissions = new Permissions + { + EditAnyCollection = editAnyCollection, + DeleteAnyCollection = deleteAnyCollection, + ManageGroups = manageGroups, + ManageUsers = manageUsers + }; + + var context = new AuthorizationHandlerContext( + new[] { GroupOperations.ReadAll(organization.Id) }, + new ClaimsPrincipal(), + null); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task CanReadAllAsync_WhenMissingPermissions_NoSuccess( + OrganizationUserType userType, + SutProvider sutProvider, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = userType; + organization.LimitCollectionCreationDeletion = true; + organization.Permissions = new Permissions + { + EditAnyCollection = false, + DeleteAnyCollection = false, + ManageGroups = false, + ManageUsers = false, + AccessImportExport = false + }; + + var context = new AuthorizationHandlerContext( + new[] { GroupOperations.ReadAll(organization.Id) }, + new ClaimsPrincipal(), + null); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task CanReadAllAsync_WhenMissingOrgAccess_NoSuccess( + Guid userId, + Guid organizationId, + SutProvider sutProvider) + { + var context = new AuthorizationHandlerContext( + new[] { GroupOperations.ReadAll(organizationId) }, + new ClaimsPrincipal(), + null + ); + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); + + await sutProvider.Sut.HandleAsync(context); + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task HandleRequirementAsync_MissingUserId_Failure( + Guid organizationId, + SutProvider sutProvider) + { + var context = new AuthorizationHandlerContext( + new[] { GroupOperations.ReadAll(organizationId) }, + new ClaimsPrincipal(), + null + ); + + // Simulate missing user id + sutProvider.GetDependency().UserId.Returns((Guid?)null); + + await sutProvider.Sut.HandleAsync(context); + Assert.False(context.HasSucceeded); + Assert.True(context.HasFailed); + } + + [Theory, BitAutoData] + public async Task HandleRequirementAsync_NoSpecifiedOrgId_Failure( + SutProvider sutProvider) + { + var context = new AuthorizationHandlerContext( + new[] { GroupOperations.ReadAll(default) }, + new ClaimsPrincipal(), + null + ); + + sutProvider.GetDependency().UserId.Returns(new Guid()); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + Assert.True(context.HasFailed); + } +} diff --git a/test/Api.Test/Vault/AuthorizationHandlers/OrganizationUserAuthorizationHandlerTests.cs b/test/Api.Test/Vault/AuthorizationHandlers/OrganizationUserAuthorizationHandlerTests.cs new file mode 100644 index 000000000..1f6916faf --- /dev/null +++ b/test/Api.Test/Vault/AuthorizationHandlers/OrganizationUserAuthorizationHandlerTests.cs @@ -0,0 +1,194 @@ +using System.Security.Claims; +using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers; +using Bit.Core; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Test.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 OrganizationUserAuthorizationHandlerTests +{ + [Theory] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task CanReadAllAsync_WhenAdminOrOwner_Success( + OrganizationUserType userType, + Guid userId, SutProvider sutProvider, + CurrentContextOrganization organization) + { + organization.Type = userType; + organization.LimitCollectionCreationDeletion = true; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + new[] { OrganizationUserOperations.ReadAll(organization.Id) }, + new ClaimsPrincipal(), + null); + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task CanReadAllAsync_WithProviderUser_Success( + Guid userId, + SutProvider sutProvider, CurrentContextOrganization organization) + { + organization.Type = OrganizationUserType.User; + organization.LimitCollectionCreationDeletion = true; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + new[] { OrganizationUserOperations.ReadAll(organization.Id) }, + new ClaimsPrincipal(), + null); + + sutProvider.GetDependency() + .UserId + .Returns(userId); + sutProvider.GetDependency() + .ProviderUserForOrgAsync(organization.Id) + .Returns(true); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory] + [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 CanReadAllAsync_WhenCustomUserWithRequiredPermissions_Success( + bool editAnyCollection, bool deleteAnyCollection, bool manageGroups, + bool manageUsers, bool limitCollectionCreationDeletion, + SutProvider sutProvider, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = OrganizationUserType.Custom; + organization.LimitCollectionCreationDeletion = limitCollectionCreationDeletion; + organization.Permissions = new Permissions + { + EditAnyCollection = editAnyCollection, + DeleteAnyCollection = deleteAnyCollection, + ManageGroups = manageGroups, + ManageUsers = manageUsers + }; + + var context = new AuthorizationHandlerContext( + new[] { OrganizationUserOperations.ReadAll(organization.Id) }, + new ClaimsPrincipal(), + null); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task CanReadAllAsync_WhenMissingPermissions_NoSuccess( + OrganizationUserType userType, + SutProvider sutProvider, + 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[] { OrganizationUserOperations.ReadAll(organization.Id) }, + new ClaimsPrincipal(), + null); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task HandleRequirementAsync_WhenMissingOrgAccess_NoSuccess( + Guid userId, + Guid organizationId, + SutProvider sutProvider) + { + var context = new AuthorizationHandlerContext( + new[] { OrganizationUserOperations.ReadAll(organizationId) }, + new ClaimsPrincipal(), + null + ); + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); + + await sutProvider.Sut.HandleAsync(context); + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task HandleRequirementAsync_MissingUserId_Failure( + Guid organizationId, + SutProvider sutProvider) + { + var context = new AuthorizationHandlerContext( + new[] { OrganizationUserOperations.ReadAll(organizationId) }, + new ClaimsPrincipal(), + null + ); + + // Simulate missing user id + sutProvider.GetDependency().UserId.Returns((Guid?)null); + + await sutProvider.Sut.HandleAsync(context); + Assert.True(context.HasFailed); + } + + [Theory, BitAutoData] + public async Task HandleRequirementAsync_NoSpecifiedOrgId_Failure( + SutProvider sutProvider) + { + var context = new AuthorizationHandlerContext( + new[] { OrganizationUserOperations.ReadAll(default) }, + new ClaimsPrincipal(), + null + ); + + sutProvider.GetDependency().UserId.Returns(new Guid()); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasFailed); + } +}