diff --git a/src/Api/Controllers/OrganizationUsersController.cs b/src/Api/Controllers/OrganizationUsersController.cs index d1c4ebacd6..e1a1a3a576 100644 --- a/src/Api/Controllers/OrganizationUsersController.cs +++ b/src/Api/Controllers/OrganizationUsersController.cs @@ -1,6 +1,8 @@ using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response; using Bit.Api.Models.Response.Organizations; +using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers; +using Bit.Core; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -31,6 +33,10 @@ public class OrganizationUsersController : Controller private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery; private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; private readonly IUpdateOrganizationUserGroupsCommand _updateOrganizationUserGroupsCommand; + private readonly IFeatureService _featureService; + private readonly IAuthorizationService _authorizationService; + + private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); public OrganizationUsersController( IOrganizationRepository organizationRepository, @@ -43,7 +49,9 @@ public class OrganizationUsersController : Controller ICurrentContext currentContext, ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery, IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, - IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand) + IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand, + IFeatureService featureService, + IAuthorizationService authorizationService) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -56,6 +64,8 @@ public class OrganizationUsersController : Controller _countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery; _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; _updateOrganizationUserGroupsCommand = updateOrganizationUserGroupsCommand; + _featureService = featureService; + _authorizationService = authorizationService; } [HttpGet("{id}")] @@ -78,18 +88,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 = FlexibleCollectionsIsEnabled ? + (await _authorizationService.AuthorizeAsync(User, orgId, OrganizationUserOperations.Read(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/Vault/AuthorizationHandlers/OrganizationUsers/OrganizationUserAuthorizationHandler.cs b/src/Api/Vault/AuthorizationHandlers/OrganizationUsers/OrganizationUserAuthorizationHandler.cs new file mode 100644 index 0000000000..27614a062e --- /dev/null +++ b/src/Api/Vault/AuthorizationHandlers/OrganizationUsers/OrganizationUserAuthorizationHandler.cs @@ -0,0 +1,76 @@ +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; + + public OrganizationUserAuthorizationHandler( + ICurrentContext currentContext, + IFeatureService featureService) + { + _currentContext = currentContext; + _featureService = featureService; + } + + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + OrganizationUserOperationRequirement requirement) + { + 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."); + } + + if (!_currentContext.UserId.HasValue) + { + context.Fail(); + return; + } + + var targetOrganizationId = requirement.OrganizationId; + + // 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.Name == nameof(OrganizationUserOperations.Read): + await CanReadAsync(context, requirement, org); + break; + } + } + + private async Task CanReadAsync(AuthorizationHandlerContext context, OrganizationUserOperationRequirement 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/OrganizationUsers/OrganizationUserOperations.cs b/src/Api/Vault/AuthorizationHandlers/OrganizationUsers/OrganizationUserOperations.cs new file mode 100644 index 0000000000..ed0f71a50a --- /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 Read(Guid organizationId) + { + return new OrganizationUserOperationRequirement(nameof(Read), organizationId); + } +}