mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 16:12:49 -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:
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user