1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-07 04:32:20 -05:00

Moved auth logic for GET /organizations/{orgId}/users/{id} to auth handler. Updated tests as well.

This commit is contained in:
jrmccannon 2025-03-13 11:50:46 -05:00
parent d40fbe3217
commit 589af12f8f
No known key found for this signature in database
GPG Key ID: CF03F3DB01CE96A6
6 changed files with 113 additions and 7 deletions

View File

@ -7,6 +7,7 @@ using Bit.Core;
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.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
using Bit.Core.AdminConsole.Repositories;
@ -107,10 +108,18 @@ public class OrganizationUsersController : Controller
}
[HttpGet("{id}")]
public async Task<OrganizationUserDetailsResponseModel> Get(Guid id, bool includeGroups = false)
public async Task<OrganizationUserDetailsResponseModel> Get([FromRoute] Guid orgId, Guid id, bool includeGroups = false)
{
var authorizationResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId),
OrganizationUserDetailsOperations.Read);
if (authorizationResult.Succeeded is false)
{
throw new NotFoundException();
}
var (organizationUser, collections) = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id);
if (organizationUser == null || !await _currentContext.ManageUsers(organizationUser.OrganizationId))
if (organizationUser == null)
{
throw new NotFoundException();
}
@ -707,7 +716,7 @@ public class OrganizationUsersController : Controller
{
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
{
return userIds.ToDictionary(kvp => kvp, kvp => false);
return userIds.ToDictionary(kvp => kvp, _ => false);
}
var usersOrganizationManagementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(orgId, userIds);

View File

@ -0,0 +1,30 @@
#nullable enable
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
using Bit.Core.Context;
using Microsoft.AspNetCore.Authorization;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization.OrganizationUserDetails;
public class OrganizationUserDetailsAuthorizationHandler(ICurrentContext currentContext) :
AuthorizationHandler<OrganizationUserDetailsOperationRequirement, OrganizationScope>
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
OrganizationUserDetailsOperationRequirement requirement,
OrganizationScope organizationScope)
{
var authorized = requirement switch
{
not null when requirement.Name == nameof(OrganizationUserDetailsOperations.Read) => await currentContext
.ManageUsers(organizationScope),
_ => false
};
if (authorized)
{
context.Succeed(requirement!);
return;
}
context.Fail();
}
}

View File

@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization.OrganizationUserDetails;
public class OrganizationUserDetailsOperationRequirement : OperationAuthorizationRequirement;
public static class OrganizationUserDetailsOperations
{
public static readonly OrganizationUserDetailsOperationRequirement Read = new() { Name = nameof(Read) };
}

View File

@ -12,6 +12,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
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.Interfaces;
using Bit.Core.Models.Business.Tokenables;
using Bit.Core.OrganizationFeatures.OrganizationCollections;
@ -169,6 +170,7 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped<IAuthorizationHandler, OrganizationUserUserMiniDetailsAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, OrganizationUserUserDetailsAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, OrganizationUserDetailsAuthorizationHandler>();
services.AddScoped<IHasConfirmedOwnersExceptQuery, HasConfirmedOwnersExceptQuery>();
}

View File

@ -6,7 +6,9 @@ using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization.OrganizationUserDetails;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Repositories;
@ -250,9 +252,13 @@ public class OrganizationUsersControllerTests
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(accountDeprovisioningEnabled);
sutProvider.GetDependency<ICurrentContext>()
.ManageUsers(organizationUser.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(
user: Arg.Any<ClaimsPrincipal>(),
resource: Arg.Is<OrganizationScope>(x => x == organizationUser.OrganizationId),
requirements: Arg.Is<IEnumerable<IAuthorizationRequirement>>(x =>
x.Any(y => y == OrganizationUserDetailsOperations.Read)))
.Returns(AuthorizationResult.Success());
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetDetailsByIdWithCollectionsAsync(organizationUser.Id)
@ -262,7 +268,7 @@ public class OrganizationUsersControllerTests
.GetUsersOrganizationManagementStatusAsync(organizationUser.OrganizationId, Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)))
.Returns(new Dictionary<Guid, bool> { { organizationUser.Id, true } });
var response = await sutProvider.Sut.Get(organizationUser.Id, false);
var response = await sutProvider.Sut.Get(organizationUser.OrganizationId, organizationUser.Id, false);
Assert.Equal(organizationUser.Id, response.Id);
Assert.Equal(accountDeprovisioningEnabled, response.ManagedByOrganization);

View File

@ -0,0 +1,49 @@
using System.Security.Claims;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization.OrganizationUserDetails;
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 OrganizationUserDetailsAuthorizationHandlerTests
{
[Theory]
[BitAutoData]
public async Task Read_UserCanManageUsers_ShouldReturnSuccess(CurrentContextOrganization contextOrganization,
SutProvider<OrganizationUserDetailsAuthorizationHandler> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(contextOrganization.Id).Returns(true);
var context = new AuthorizationHandlerContext(
[OrganizationUserDetailsOperations.Read],
new ClaimsPrincipal(),
new OrganizationScope(contextOrganization.Id));
await sutProvider.Sut.HandleAsync(context);
Assert.True(context.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task Read_UserCannotManageUsers_ShouldReturnFailure(CurrentContextOrganization contextOrganization,
SutProvider<OrganizationUserDetailsAuthorizationHandler> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(contextOrganization.Id).Returns(false);
var context = new AuthorizationHandlerContext(
[OrganizationUserDetailsOperations.Read],
new ClaimsPrincipal(),
new OrganizationScope(contextOrganization.Id));
await sutProvider.Sut.HandleAsync(context);
Assert.True(context.HasFailed);
}
}