diff --git a/src/Api/Controllers/GroupsController.cs b/src/Api/Controllers/GroupsController.cs index 0220cfd5e2..b704602f39 100644 --- a/src/Api/Controllers/GroupsController.cs +++ b/src/Api/Controllers/GroupsController.cs @@ -1,7 +1,11 @@ using Bit.Api.Models.Request; using Bit.Api.Models.Response; +using Bit.Api.Vault.AuthorizationHandlers.Groups; +using Bit.Core; using Bit.Core.Context; +using Bit.Core.Entities; using Bit.Core.Exceptions; +using Bit.Core.Models.Data; using Bit.Core.OrganizationFeatures.Groups.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -21,6 +25,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 FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); public GroupsController( IGroupRepository groupRepository, @@ -29,7 +37,9 @@ public class GroupsController : Controller ICurrentContext currentContext, ICreateGroupCommand createGroupCommand, IUpdateGroupCommand updateGroupCommand, - IDeleteGroupCommand deleteGroupCommand) + IDeleteGroupCommand deleteGroupCommand, + IFeatureService featureService, + IAuthorizationService authorizationService) { _groupRepository = groupRepository; _groupService = groupService; @@ -38,6 +48,8 @@ public class GroupsController : Controller _createGroupCommand = createGroupCommand; _updateGroupCommand = updateGroupCommand; _deleteGroupCommand = deleteGroupCommand; + _featureService = featureService; + _authorizationService = authorizationService; } [HttpGet("{id}")] @@ -65,20 +77,34 @@ 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); + ICollection>> groups; - if (!canAccess) + if (FlexibleCollectionsIsEnabled) { - throw new NotFoundException(); + groups = await _groupRepository.GetManyWithCollectionsByOrganizationIdAsync(orgId); + var authorized = (await _authorizationService.AuthorizeAsync(User, groups.Select(g => g.Item1), GroupOperations.Read)).Succeeded; + if (!authorized) + { + throw new NotFoundException(); + } + } + else + { + var canAccess = await _currentContext.ManageGroups(orgId) || + await _currentContext.ViewAssignedCollections(orgId) || + await _currentContext.ViewAllCollections(orgId) || + await _currentContext.ManageUsers(orgId); + + if (!canAccess) + { + throw new NotFoundException(); + } + + groups = await _groupRepository.GetManyWithCollectionsByOrganizationIdAsync(orgId); } - var groups = await _groupRepository.GetManyWithCollectionsByOrganizationIdAsync(orgIdGuid); var responses = groups.Select(g => new GroupDetailsResponseModel(g.Item1, g.Item2)); return new ListResponseModel(responses); } diff --git a/src/Api/Utilities/ServiceCollectionExtensions.cs b/src/Api/Utilities/ServiceCollectionExtensions.cs index be6a7a066a..cdf6442af4 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; @@ -121,5 +123,7 @@ public static class ServiceCollectionExtensions public static void AddAuthorizationHandlers(this IServiceCollection services) { services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Api/Vault/AuthorizationHandlers/Groups/GroupAuthorizationHandler.cs b/src/Api/Vault/AuthorizationHandlers/Groups/GroupAuthorizationHandler.cs new file mode 100644 index 0000000000..31af8ff782 --- /dev/null +++ b/src/Api/Vault/AuthorizationHandlers/Groups/GroupAuthorizationHandler.cs @@ -0,0 +1,97 @@ +using Bit.Core; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Api.Vault.AuthorizationHandlers.Groups; + +/// +/// Handles authorization logic for Group objects. +/// This uses new logic implemented in the Flexible Collections initiative. +/// +public class GroupAuthorizationHandler : BulkAuthorizationHandler +{ + private readonly ICurrentContext _currentContext; + private readonly IFeatureService _featureService; + + public GroupAuthorizationHandler( + ICurrentContext currentContext, + IFeatureService featureService) + { + _currentContext = currentContext; + _featureService = featureService; + } + + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + GroupOperationRequirement requirement, ICollection resources) + { + if (!_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext)) + { + // 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) + { + context.Fail(); + return; + } + + if (!resources.Any()) + { + context.Succeed(requirement); + return; + } + + if (!_currentContext.UserId.HasValue) + { + context.Fail(); + return; + } + + var targetOrganizationId = resources.First().OrganizationId; + + // Ensure all target collections belong to the same organization + if (resources.Any(tc => tc.OrganizationId != targetOrganizationId)) + { + throw new BadRequestException("Requested groups must belong to the same organization."); + } + + // Acting user is not a member of the target organization, fail + var org = _currentContext.GetOrganization(targetOrganizationId); + if (org == null) + { + context.Fail(); + return; + } + + switch (requirement) + { + case not null when requirement == GroupOperations.Read: + await CanReadAsync(context, requirement, org); + break; + } + } + + private async Task CanReadAsync(AuthorizationHandlerContext context, GroupOperationRequirement requirement, + CurrentContextOrganization org) + { + if (org.Type is OrganizationUserType.Owner or OrganizationUserType.Admin || + org.Permissions.ManageGroups || + org.Permissions.ManageUsers || + org.Permissions.EditAnyCollection || + org.Permissions.DeleteAnyCollection || + await _currentContext.ProviderUserForOrgAsync(org.Id)) + { + context.Succeed(requirement); + return; + } + + context.Fail(); + } +} diff --git a/src/Api/Vault/AuthorizationHandlers/Groups/GroupOperations.cs b/src/Api/Vault/AuthorizationHandlers/Groups/GroupOperations.cs new file mode 100644 index 0000000000..8cac205dda --- /dev/null +++ b/src/Api/Vault/AuthorizationHandlers/Groups/GroupOperations.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace Bit.Api.Vault.AuthorizationHandlers.Groups; + +public class GroupOperationRequirement : OperationAuthorizationRequirement { } + +public static class GroupOperations +{ + public static readonly GroupOperationRequirement Read = new() { Name = nameof(Read) }; +}