mirror of
https://github.com/bitwarden/server.git
synced 2025-05-06 12:12:18 -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:
parent
c9a42d861c
commit
84a984a9e6
21
src/Api/AdminConsole/Authorization/AuthorizeAttribute.cs
Normal file
21
src/Api/AdminConsole/Authorization/AuthorizeAttribute.cs
Normal file
@ -0,0 +1,21 @@
|
||||
#nullable enable
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// An attribute which requires authorization using the specified requirement.
|
||||
/// This uses the standard ASP.NET authorization middleware.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The IAuthorizationRequirement that will be used to authorize the user.</typeparam>
|
||||
public class AuthorizeAttribute<T>
|
||||
: AuthorizeAttribute, IAuthorizationRequirementData
|
||||
where T : IAuthorizationRequirement, new()
|
||||
{
|
||||
public IEnumerable<IAuthorizationRequirement> GetRequirements()
|
||||
{
|
||||
var requirement = new T();
|
||||
return [requirement];
|
||||
}
|
||||
}
|
79
src/Api/AdminConsole/Authorization/HttpContextExtensions.cs
Normal file
79
src/Api/AdminConsole/Authorization/HttpContextExtensions.cs
Normal file
@ -0,0 +1,79 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Authorization;
|
||||
|
||||
public static class HttpContextExtensions
|
||||
{
|
||||
public const string NoOrgIdError =
|
||||
"A route decorated with with '[Authorize<Requirement>]' must include a route value named 'orgId' either through the [Controller] attribute or through a '[Http*]' attribute.";
|
||||
|
||||
/// <summary>
|
||||
/// Returns the result of the callback, caching it in HttpContext.Features for the lifetime of the request.
|
||||
/// Subsequent calls will retrieve the cached value.
|
||||
/// Results are stored by type and therefore must be of a unique type.
|
||||
/// </summary>
|
||||
public static async Task<T> WithFeaturesCacheAsync<T>(this HttpContext httpContext, Func<Task<T>> callback)
|
||||
{
|
||||
var cachedResult = httpContext.Features.Get<T>();
|
||||
if (cachedResult != null)
|
||||
{
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
var result = await callback();
|
||||
httpContext.Features.Set(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the user is a ProviderUser for a Provider which manages the specified organization, otherwise false.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This data is fetched from the database and cached as a HttpContext Feature for the lifetime of the request.
|
||||
/// </remarks>
|
||||
public static async Task<bool> IsProviderUserForOrgAsync(
|
||||
this HttpContext httpContext,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
Guid userId,
|
||||
Guid organizationId)
|
||||
{
|
||||
var organizations = await httpContext.GetProviderUserOrganizationsAsync(providerUserRepository, userId);
|
||||
return organizations.Any(o => o.OrganizationId == organizationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the ProviderUserOrganizations for a user. These are the organizations the ProviderUser manages via their Provider, if any.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This data is fetched from the database and cached as a HttpContext Feature for the lifetime of the request.
|
||||
/// </remarks>
|
||||
private static async Task<IEnumerable<ProviderUserOrganizationDetails>> GetProviderUserOrganizationsAsync(
|
||||
this HttpContext httpContext,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
Guid userId)
|
||||
=> await httpContext.WithFeaturesCacheAsync(() =>
|
||||
providerUserRepository.GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed));
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Parses the {orgId} route parameter into a Guid, or throws if the {orgId} is not present or not a valid guid.
|
||||
/// </summary>
|
||||
/// <param name="httpContext"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public static Guid GetOrganizationId(this HttpContext httpContext)
|
||||
{
|
||||
httpContext.GetRouteData().Values.TryGetValue("orgId", out var orgIdParam);
|
||||
if (orgIdParam == null || !Guid.TryParse(orgIdParam.ToString(), out var orgId))
|
||||
{
|
||||
throw new InvalidOperationException(NoOrgIdError);
|
||||
}
|
||||
|
||||
return orgId;
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.Context;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// A requirement that implements this interface will be handled by <see cref="OrganizationRequirementHandler"/>,
|
||||
/// which calls AuthorizeAsync with the organization details from the route.
|
||||
/// This is used for simple role-based checks.
|
||||
/// This may only be used on endpoints with {orgId} in their path.
|
||||
/// </summary>
|
||||
public interface IOrganizationRequirement : IAuthorizationRequirement
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to authorize a request that has this requirement.
|
||||
/// </summary>
|
||||
/// <param name="organizationClaims">
|
||||
/// The CurrentContextOrganization for the user if they are a member of the organization.
|
||||
/// This is null if they are not a member.
|
||||
/// </param>
|
||||
/// <param name="isProviderUserForOrg">
|
||||
/// A callback that returns true if the user is a ProviderUser that manages the organization, otherwise false.
|
||||
/// This requires a database query, call it last.
|
||||
/// </param>
|
||||
/// <returns>True if the requirement has been satisfied, otherwise false.</returns>
|
||||
public Task<bool> AuthorizeAsync(
|
||||
CurrentContextOrganization? organizationClaims,
|
||||
Func<Task<bool>> isProviderUserForOrg);
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Models.Data;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Authorization;
|
||||
|
||||
public static class OrganizationClaimsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a user's claims and returns an object representing their claims for the specified organization.
|
||||
/// </summary>
|
||||
/// <param name="user">The user who has the claims.</param>
|
||||
/// <param name="organizationId">The organizationId to look for in the claims.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="CurrentContextOrganization"/> representing the user's claims for that organization, or null
|
||||
/// if the user does not have any claims for that organization.
|
||||
/// </returns>
|
||||
public static CurrentContextOrganization? GetCurrentContextOrganization(this ClaimsPrincipal user, Guid organizationId)
|
||||
{
|
||||
var hasClaim = GetClaimsParser(user, organizationId);
|
||||
|
||||
var role = GetRoleFromClaims(hasClaim);
|
||||
if (!role.HasValue)
|
||||
{
|
||||
// Not an organization member
|
||||
return null;
|
||||
}
|
||||
|
||||
return new CurrentContextOrganization
|
||||
{
|
||||
Id = organizationId,
|
||||
Type = role.Value,
|
||||
AccessSecretsManager = hasClaim(Claims.SecretsManagerAccess),
|
||||
Permissions = role == OrganizationUserType.Custom
|
||||
? GetPermissionsFromClaims(hasClaim)
|
||||
: new Permissions()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a function for evaluating claims for the specified user and organizationId.
|
||||
/// The function returns true if the claim type exists and false otherwise.
|
||||
/// </summary>
|
||||
private static Func<string, bool> GetClaimsParser(ClaimsPrincipal user, Guid organizationId)
|
||||
{
|
||||
// Group claims by ClaimType
|
||||
var claimsDict = user.Claims
|
||||
.GroupBy(c => c.Type)
|
||||
.ToDictionary(
|
||||
c => c.Key,
|
||||
c => c.ToList());
|
||||
|
||||
return claimType
|
||||
=> claimsDict.TryGetValue(claimType, out var claims) &&
|
||||
claims
|
||||
.ParseGuids()
|
||||
.Any(v => v == organizationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the provided claims into proper Guids, or ignore them if they are not valid guids.
|
||||
/// </summary>
|
||||
private static IEnumerable<Guid> ParseGuids(this IEnumerable<Claim> claims)
|
||||
{
|
||||
foreach (var claim in claims)
|
||||
{
|
||||
if (Guid.TryParse(claim.Value, out var guid))
|
||||
{
|
||||
yield return guid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static OrganizationUserType? GetRoleFromClaims(Func<string, bool> hasClaim)
|
||||
{
|
||||
if (hasClaim(Claims.OrganizationOwner))
|
||||
{
|
||||
return OrganizationUserType.Owner;
|
||||
}
|
||||
|
||||
if (hasClaim(Claims.OrganizationAdmin))
|
||||
{
|
||||
return OrganizationUserType.Admin;
|
||||
}
|
||||
|
||||
if (hasClaim(Claims.OrganizationCustom))
|
||||
{
|
||||
return OrganizationUserType.Custom;
|
||||
}
|
||||
|
||||
if (hasClaim(Claims.OrganizationUser))
|
||||
{
|
||||
return OrganizationUserType.User;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Permissions GetPermissionsFromClaims(Func<string, bool> hasClaim)
|
||||
=> new()
|
||||
{
|
||||
AccessEventLogs = hasClaim(Claims.CustomPermissions.AccessEventLogs),
|
||||
AccessImportExport = hasClaim(Claims.CustomPermissions.AccessImportExport),
|
||||
AccessReports = hasClaim(Claims.CustomPermissions.AccessReports),
|
||||
CreateNewCollections = hasClaim(Claims.CustomPermissions.CreateNewCollections),
|
||||
EditAnyCollection = hasClaim(Claims.CustomPermissions.EditAnyCollection),
|
||||
DeleteAnyCollection = hasClaim(Claims.CustomPermissions.DeleteAnyCollection),
|
||||
ManageGroups = hasClaim(Claims.CustomPermissions.ManageGroups),
|
||||
ManagePolicies = hasClaim(Claims.CustomPermissions.ManagePolicies),
|
||||
ManageSso = hasClaim(Claims.CustomPermissions.ManageSso),
|
||||
ManageUsers = hasClaim(Claims.CustomPermissions.ManageUsers),
|
||||
ManageResetPassword = hasClaim(Claims.CustomPermissions.ManageResetPassword),
|
||||
ManageScim = hasClaim(Claims.CustomPermissions.ManageScim),
|
||||
};
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// Handles any requirement that implements <see cref="IOrganizationRequirement"/>.
|
||||
/// Retrieves the Organization ID from the route and then passes it to the requirement's AuthorizeAsync callback to
|
||||
/// determine whether the action is authorized.
|
||||
/// </summary>
|
||||
public class OrganizationRequirementHandler(
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IUserService userService)
|
||||
: AuthorizationHandler<IOrganizationRequirement>
|
||||
{
|
||||
public const string NoHttpContextError = "This method should only be called in the context of an HTTP Request.";
|
||||
public const string NoUserIdError = "This method should only be called on the private api with a logged in user.";
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, IOrganizationRequirement requirement)
|
||||
{
|
||||
var httpContext = httpContextAccessor.HttpContext;
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new InvalidOperationException(NoHttpContextError);
|
||||
}
|
||||
|
||||
var organizationId = httpContext.GetOrganizationId();
|
||||
var organizationClaims = httpContext.User.GetCurrentContextOrganization(organizationId);
|
||||
|
||||
var userId = userService.GetProperUserId(httpContext.User);
|
||||
if (userId == null)
|
||||
{
|
||||
throw new InvalidOperationException(NoUserIdError);
|
||||
}
|
||||
|
||||
Task<bool> IsProviderUserForOrg() => httpContext.IsProviderUserForOrgAsync(providerUserRepository, userId.Value, organizationId);
|
||||
|
||||
var authorized = await requirement.AuthorizeAsync(organizationClaims, IsProviderUserForOrg);
|
||||
|
||||
if (authorized)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Authorization.Requirements;
|
||||
|
||||
public class ManageUsersRequirement : IOrganizationRequirement
|
||||
{
|
||||
public async Task<bool> AuthorizeAsync(
|
||||
CurrentContextOrganization? organizationClaims,
|
||||
Func<Task<bool>> isProviderUserForOrg)
|
||||
=> organizationClaims switch
|
||||
{
|
||||
{ Type: OrganizationUserType.Owner } => true,
|
||||
{ Type: OrganizationUserType.Admin } => true,
|
||||
{ Permissions.ManageUsers: true } => true,
|
||||
_ => await isProviderUserForOrg()
|
||||
};
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.Context;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Authorization.Requirements;
|
||||
|
||||
/// <summary>
|
||||
/// Requires that the user is a member of the organization or a provider for the organization.
|
||||
/// </summary>
|
||||
public class MemberOrProviderRequirement : IOrganizationRequirement
|
||||
{
|
||||
public async Task<bool> AuthorizeAsync(
|
||||
CurrentContextOrganization? organizationClaims,
|
||||
Func<Task<bool>> isProviderUserForOrg)
|
||||
=> organizationClaims is not null || await isProviderUserForOrg();
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using Bit.Api.Tools.Authorization;
|
||||
using Bit.Api.AdminConsole.Authorization;
|
||||
using Bit.Api.Tools.Authorization;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
|
||||
using Bit.Core.IdentityServer;
|
||||
@ -105,5 +106,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<IAuthorizationHandler, VaultExportAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, SecurityTaskAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, SecurityTaskOrganizationAuthorizationHandler>();
|
||||
|
||||
services.AddScoped<IAuthorizationHandler, OrganizationRequirementHandler>();
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.Identity;
|
||||
|
||||
namespace Bit.Core.Models.Data;
|
||||
|
||||
@ -20,17 +21,17 @@ public class Permissions
|
||||
[JsonIgnore]
|
||||
public List<(bool Permission, string ClaimName)> ClaimsMap => new()
|
||||
{
|
||||
(AccessEventLogs, "accesseventlogs"),
|
||||
(AccessImportExport, "accessimportexport"),
|
||||
(AccessReports, "accessreports"),
|
||||
(CreateNewCollections, "createnewcollections"),
|
||||
(EditAnyCollection, "editanycollection"),
|
||||
(DeleteAnyCollection, "deleteanycollection"),
|
||||
(ManageGroups, "managegroups"),
|
||||
(ManagePolicies, "managepolicies"),
|
||||
(ManageSso, "managesso"),
|
||||
(ManageUsers, "manageusers"),
|
||||
(ManageResetPassword, "manageresetpassword"),
|
||||
(ManageScim, "managescim"),
|
||||
(AccessEventLogs, Claims.CustomPermissions.AccessEventLogs),
|
||||
(AccessImportExport, Claims.CustomPermissions.AccessImportExport),
|
||||
(AccessReports, Claims.CustomPermissions.AccessReports),
|
||||
(CreateNewCollections, Claims.CustomPermissions.CreateNewCollections),
|
||||
(EditAnyCollection, Claims.CustomPermissions.EditAnyCollection),
|
||||
(DeleteAnyCollection, Claims.CustomPermissions.DeleteAnyCollection),
|
||||
(ManageGroups, Claims.CustomPermissions.ManageGroups),
|
||||
(ManagePolicies, Claims.CustomPermissions.ManagePolicies),
|
||||
(ManageSso, Claims.CustomPermissions.ManageSso),
|
||||
(ManageUsers, Claims.CustomPermissions.ManageUsers),
|
||||
(ManageResetPassword, Claims.CustomPermissions.ManageResetPassword),
|
||||
(ManageScim, Claims.CustomPermissions.ManageScim),
|
||||
};
|
||||
}
|
||||
|
@ -22,4 +22,21 @@ public static class Claims
|
||||
|
||||
// General
|
||||
public const string Type = "type";
|
||||
|
||||
// Organization custom permissions
|
||||
public static class CustomPermissions
|
||||
{
|
||||
public const string AccessEventLogs = "accesseventlogs";
|
||||
public const string AccessImportExport = "accessimportexport";
|
||||
public const string AccessReports = "accessreports";
|
||||
public const string CreateNewCollections = "createnewcollections";
|
||||
public const string EditAnyCollection = "editanycollection";
|
||||
public const string DeleteAnyCollection = "deleteanycollection";
|
||||
public const string ManageGroups = "managegroups";
|
||||
public const string ManagePolicies = "managepolicies";
|
||||
public const string ManageSso = "managesso";
|
||||
public const string ManageUsers = "manageusers";
|
||||
public const string ManageResetPassword = "manageresetpassword";
|
||||
public const string ManageScim = "managescim";
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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))]
|
||||
|
@ -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.
|
@ -3,7 +3,7 @@ using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.Helpers;
|
||||
|
||||
public class AuthorizationHelpersTests
|
||||
public class PermissionsHelpersTests
|
||||
{
|
||||
[Fact]
|
||||
public void Permissions_Invert_InvertsAllPermissions()
|
Loading…
x
Reference in New Issue
Block a user