1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 23:52:50 -05:00

[PM-19585] Use Authorize attributes for simple role authorization (#5555)

- Add Authorize<T> attribute
- Add IOrganizationRequirement and example implementation
- Add OrganizationRequirementHandler
- Add extension methods (replacing ICurrentContext)
- Move custom permissions claim definitions

---
Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by:  Audrey  <ajensen@bitwarden.com>
This commit is contained in:
Thomas Rittson
2025-04-15 14:36:00 +10:00
committed by GitHub
parent c9a42d861c
commit 84a984a9e6
16 changed files with 590 additions and 16 deletions

View File

@ -0,0 +1,28 @@
using Bit.Api.AdminConsole.Authorization;
using Microsoft.AspNetCore.Http;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Authorization;
public class HttpContextExtensionsTests
{
[Fact]
public async Task WithFeaturesCacheAsync_OnlyExecutesCallbackOnce()
{
var httpContext = new DefaultHttpContext();
var callback = Substitute.For<Func<Task<string>>>();
callback().Returns(Task.FromResult("hello world"));
// Call once
var result1 = await httpContext.WithFeaturesCacheAsync(callback);
Assert.Equal("hello world", result1);
await callback.ReceivedWithAnyArgs(1).Invoke();
// Call again - callback not executed again
var result2 = await httpContext.WithFeaturesCacheAsync(callback);
Assert.Equal("hello world", result2);
await callback.ReceivedWithAnyArgs(1).Invoke();
}
}

View File

@ -0,0 +1,60 @@
using System.Security.Claims;
using Bit.Api.AdminConsole.Authorization;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Test.AdminConsole.Helpers;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Authorization;
public class OrganizationClaimsExtensionsTests
{
[Theory, BitMemberAutoData(nameof(GetTestOrganizations))]
public void GetCurrentContextOrganization_ParsesOrganizationFromClaims(CurrentContextOrganization expected, User user)
{
var claims = CoreHelpers.BuildIdentityClaims(user, [expected], [], false)
.Select(c => new Claim(c.Key, c.Value));
var claimsPrincipal = new ClaimsPrincipal();
claimsPrincipal.AddIdentities([new ClaimsIdentity(claims)]);
var actual = claimsPrincipal.GetCurrentContextOrganization(expected.Id);
AssertHelper.AssertPropertyEqual(expected, actual);
}
public static IEnumerable<object[]> GetTestOrganizations()
{
var roles = new List<OrganizationUserType> { OrganizationUserType.Owner, OrganizationUserType.Admin, OrganizationUserType.User };
foreach (var role in roles)
{
yield return
[
new CurrentContextOrganization
{
Id = Guid.NewGuid(),
Type = role,
AccessSecretsManager = true
}
];
}
var permissions = PermissionsHelpers.GetAllPermissions();
foreach (var permission in permissions)
{
yield return
[
new CurrentContextOrganization
{
Id = Guid.NewGuid(),
Type = OrganizationUserType.Custom,
Permissions = permission
}
];
}
}
}

View File

@ -0,0 +1,112 @@
using System.Security.Claims;
using Bit.Api.AdminConsole.Authorization;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Authorization;
[SutProviderCustomize]
public class OrganizationRequirementHandlerTests
{
[Theory]
[BitAutoData((string)null)]
[BitAutoData("malformed guid")]
public async Task IfInvalidOrganizationId_Throws(string orgId, Guid userId, SutProvider<OrganizationRequirementHandler> sutProvider)
{
// Arrange
ArrangeRouteAndUser(sutProvider, orgId, userId);
var testRequirement = Substitute.For<IOrganizationRequirement>();
var authContext = new AuthorizationHandlerContext([testRequirement], new ClaimsPrincipal(), null);
// Act
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => sutProvider.Sut.HandleAsync(authContext));
Assert.Contains(HttpContextExtensions.NoOrgIdError, exception.Message);
Assert.False(authContext.HasSucceeded);
}
[Theory, BitAutoData]
public async Task IfHttpContextIsNull_Throws(SutProvider<OrganizationRequirementHandler> sutProvider)
{
// Arrange
sutProvider.GetDependency<IHttpContextAccessor>().HttpContext = null;
var testRequirement = Substitute.For<IOrganizationRequirement>();
var authContext = new AuthorizationHandlerContext([testRequirement], new ClaimsPrincipal(), null);
// Act
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => sutProvider.Sut.HandleAsync(authContext));
Assert.Contains(OrganizationRequirementHandler.NoHttpContextError, exception.Message);
Assert.False(authContext.HasSucceeded);
}
[Theory, BitAutoData]
public async Task IfUserIdIsNull_Throws(Guid orgId, SutProvider<OrganizationRequirementHandler> sutProvider)
{
// Arrange
ArrangeRouteAndUser(sutProvider, orgId.ToString(), null);
var testRequirement = Substitute.For<IOrganizationRequirement>();
var authContext = new AuthorizationHandlerContext([testRequirement], new ClaimsPrincipal(), null);
// Act
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => sutProvider.Sut.HandleAsync(authContext));
Assert.Contains(OrganizationRequirementHandler.NoUserIdError, exception.Message);
Assert.False(authContext.HasSucceeded);
}
[Theory, BitAutoData]
public async Task DoesNotAuthorize_IfAuthorizeAsync_ReturnsFalse(
SutProvider<OrganizationRequirementHandler> sutProvider, Guid organizationId, Guid userId)
{
// Arrange route values
ArrangeRouteAndUser(sutProvider, organizationId.ToString(), userId);
// Arrange requirement
var testRequirement = Substitute.For<IOrganizationRequirement>();
testRequirement
.AuthorizeAsync(null, Arg.Any<Func<Task<bool>>>())
.ReturnsForAnyArgs(false);
var authContext = new AuthorizationHandlerContext([testRequirement], new ClaimsPrincipal(), null);
// Act
await sutProvider.Sut.HandleAsync(authContext);
// Assert
await testRequirement.Received(1).AuthorizeAsync(null, Arg.Any<Func<Task<bool>>>());
Assert.False(authContext.HasSucceeded);
}
[Theory, BitAutoData]
public async Task Authorizes_IfAuthorizeAsync_ReturnsTrue(
SutProvider<OrganizationRequirementHandler> sutProvider, Guid organizationId, Guid userId)
{
// Arrange route values
ArrangeRouteAndUser(sutProvider, organizationId.ToString(), userId);
// Arrange requirement
var testRequirement = Substitute.For<IOrganizationRequirement>();
testRequirement
.AuthorizeAsync(null, Arg.Any<Func<Task<bool>>>())
.ReturnsForAnyArgs(true);
var authContext = new AuthorizationHandlerContext([testRequirement], new ClaimsPrincipal(), null);
// Act
await sutProvider.Sut.HandleAsync(authContext);
// Assert
await testRequirement.Received(1).AuthorizeAsync(null, Arg.Any<Func<Task<bool>>>());
Assert.True(authContext.HasSucceeded);
}
private static void ArrangeRouteAndUser(SutProvider<OrganizationRequirementHandler> sutProvider, string orgIdRouteValue,
Guid? userId)
{
var httpContext = new DefaultHttpContext();
httpContext.Request.RouteValues["orgId"] = orgIdRouteValue;
sutProvider.GetDependency<IHttpContextAccessor>().HttpContext = httpContext;
sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
}
}

View File

@ -64,7 +64,7 @@ public class VaultExportAuthorizationHandlerTests
}
public static IEnumerable<object[]> CanExportManagedCollections =>
AuthorizationHelpers.AllRoles().Select(o => new[] { o });
PermissionsHelpers.AllRoles().Select(o => new[] { o });
[Theory]
[BitMemberAutoData(nameof(CanExportManagedCollections))]

View File

@ -4,7 +4,7 @@ using Bit.Core.Models.Data;
namespace Bit.Core.Test.AdminConsole.Helpers;
public static class AuthorizationHelpers
public static class PermissionsHelpers
{
/// <summary>
/// Return a new Permission object with inverted permissions.
@ -36,6 +36,24 @@ public static class AuthorizationHelpers
return result;
}
/// <summary>
/// Returns a sequence of Permission objects, where each Permission object has a different permission flag set.
/// </summary>
public static IEnumerable<Permissions> GetAllPermissions()
{
// Get all boolean properties of input object
var props = typeof(Permissions)
.GetProperties()
.Where(p => p.PropertyType == typeof(bool));
foreach (var prop in props)
{
var result = new Permissions();
prop.SetValue(result, true);
yield return result;
}
}
/// <summary>
/// Returns a sequence of all possible roles and permissions represented as CurrentContextOrganization objects.
/// Used largely for authorization testing.

View File

@ -3,7 +3,7 @@ using Xunit;
namespace Bit.Core.Test.AdminConsole.Helpers;
public class AuthorizationHelpersTests
public class PermissionsHelpersTests
{
[Fact]
public void Permissions_Invert_InvertsAllPermissions()