diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index deee408c23..a2033260b4 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization.OrganizationUserDetails; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization.OrganizationUserGroups; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.AdminConsole.Repositories; @@ -186,11 +187,18 @@ public class OrganizationUsersController : Controller } [HttpGet("{id}/groups")] - public async Task> GetGroups(string orgId, string id) + public async Task> GetGroups([FromRoute] Guid orgId, [FromRoute] Guid id) { - var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id)); - if (organizationUser == null || (!await _currentContext.ManageGroups(organizationUser.OrganizationId) && - !await _currentContext.ManageUsers(organizationUser.OrganizationId))) + var authorized = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), + [OrganizationUserGroupOperations.ReadAllIds]); + + if (authorized.Succeeded is false) + { + throw new NotFoundException(); + } + + var organizationUser = await _organizationUserRepository.GetByIdAsync(id); + if (organizationUser == null) { throw new NotFoundException(); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserGroups/OrganizationUserGroupOperations.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserGroups/OrganizationUserGroupOperations.cs new file mode 100644 index 0000000000..c2671a8184 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserGroups/OrganizationUserGroupOperations.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization.OrganizationUserGroups; + +public class OrganizationUserGroupOperationRequirement : OperationAuthorizationRequirement; + +public static class OrganizationUserGroupOperations +{ + public static readonly OrganizationUserGroupOperationRequirement ReadAllIds = new() { Name = nameof(ReadAllIds) }; +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserGroups/OrganizationUserGroupsAuthorizationHandler.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserGroups/OrganizationUserGroupsAuthorizationHandler.cs new file mode 100644 index 0000000000..e70c5c270d --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserGroups/OrganizationUserGroupsAuthorizationHandler.cs @@ -0,0 +1,32 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; +using Bit.Core.Context; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization.OrganizationUserGroups; + +public class OrganizationUserGroupsAuthorizationHandler(ICurrentContext currentContext) + : AuthorizationHandler +{ + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + OrganizationUserGroupOperationRequirement requirement, + OrganizationScope resource) + { + var authorized = requirement switch + { + not null when requirement.Name == nameof(OrganizationUserGroupOperations.ReadAllIds) => + await CanReadGroupIdsAsync(resource), + _ => false + }; + + if (authorized) + { + context.Succeed(requirement!); + return; + } + + context.Fail(); + } + + private async Task CanReadGroupIdsAsync(OrganizationScope organizationId) => + await currentContext.ManageUsers(organizationId) || await currentContext.ManageGroups(organizationId); +} diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 74da607286..1f5ebba10b 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization.OrganizationUserDetails; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization.OrganizationUserGroups; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.Models.Business.Tokenables; using Bit.Core.OrganizationFeatures.OrganizationCollections; @@ -171,6 +172,8 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } diff --git a/test/Core.Test/AdminConsole/Authorization/OrganizationUserGroupsAuthorizationHandlerTests.cs b/test/Core.Test/AdminConsole/Authorization/OrganizationUserGroupsAuthorizationHandlerTests.cs new file mode 100644 index 0000000000..60e0d4b86f --- /dev/null +++ b/test/Core.Test/AdminConsole/Authorization/OrganizationUserGroupsAuthorizationHandlerTests.cs @@ -0,0 +1,56 @@ +using System.Security.Claims; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization.OrganizationUserGroups; +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; +using Bit.Core.Context; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.Authorization; + +[SutProviderCustomize] +public class OrganizationUserGroupsAuthorizationHandlerTests +{ + [Theory] + [BitAutoData(true, false)] + [BitAutoData(false, true)] + [BitAutoData(true, true)] + public async Task ReadAllIds_UserCanManageUsersOrGroups_ShouldReturnSuccess( + bool canManageUsers, + bool canManageGroups, + CurrentContextOrganization contextOrganization, + SutProvider sutProvider) + { + sutProvider.GetDependency().ManageUsers(contextOrganization.Id).Returns(canManageUsers); + sutProvider.GetDependency().ManageGroups(contextOrganization.Id).Returns(canManageGroups); + + var context = new AuthorizationHandlerContext( + [OrganizationUserGroupOperations.ReadAllIds], + new ClaimsPrincipal(), + new OrganizationScope(contextOrganization.Id)); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + public async Task ReadAllIds_UserCannotManageUsersNorGroups_ShouldReturnFailure(CurrentContextOrganization contextOrganization, + SutProvider sutProvider) + { + sutProvider.GetDependency().ManageUsers(contextOrganization.Id).Returns(false); + sutProvider.GetDependency().ManageGroups(contextOrganization.Id).Returns(false); + + var context = new AuthorizationHandlerContext( + [OrganizationUserGroupOperations.ReadAllIds], + new ClaimsPrincipal(), + new OrganizationScope(contextOrganization.Id)); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasFailed); + } +}