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.Api.Vault.AuthorizationHandlers.Collections;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
|
||||||
using Bit.Core.IdentityServer;
|
using Bit.Core.IdentityServer;
|
||||||
@ -105,5 +106,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<IAuthorizationHandler, VaultExportAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, VaultExportAuthorizationHandler>();
|
||||||
services.AddScoped<IAuthorizationHandler, SecurityTaskAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, SecurityTaskAuthorizationHandler>();
|
||||||
services.AddScoped<IAuthorizationHandler, SecurityTaskOrganizationAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, SecurityTaskOrganizationAuthorizationHandler>();
|
||||||
|
|
||||||
|
services.AddScoped<IAuthorizationHandler, OrganizationRequirementHandler>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Core.Identity;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Data;
|
namespace Bit.Core.Models.Data;
|
||||||
|
|
||||||
@ -20,17 +21,17 @@ public class Permissions
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public List<(bool Permission, string ClaimName)> ClaimsMap => new()
|
public List<(bool Permission, string ClaimName)> ClaimsMap => new()
|
||||||
{
|
{
|
||||||
(AccessEventLogs, "accesseventlogs"),
|
(AccessEventLogs, Claims.CustomPermissions.AccessEventLogs),
|
||||||
(AccessImportExport, "accessimportexport"),
|
(AccessImportExport, Claims.CustomPermissions.AccessImportExport),
|
||||||
(AccessReports, "accessreports"),
|
(AccessReports, Claims.CustomPermissions.AccessReports),
|
||||||
(CreateNewCollections, "createnewcollections"),
|
(CreateNewCollections, Claims.CustomPermissions.CreateNewCollections),
|
||||||
(EditAnyCollection, "editanycollection"),
|
(EditAnyCollection, Claims.CustomPermissions.EditAnyCollection),
|
||||||
(DeleteAnyCollection, "deleteanycollection"),
|
(DeleteAnyCollection, Claims.CustomPermissions.DeleteAnyCollection),
|
||||||
(ManageGroups, "managegroups"),
|
(ManageGroups, Claims.CustomPermissions.ManageGroups),
|
||||||
(ManagePolicies, "managepolicies"),
|
(ManagePolicies, Claims.CustomPermissions.ManagePolicies),
|
||||||
(ManageSso, "managesso"),
|
(ManageSso, Claims.CustomPermissions.ManageSso),
|
||||||
(ManageUsers, "manageusers"),
|
(ManageUsers, Claims.CustomPermissions.ManageUsers),
|
||||||
(ManageResetPassword, "manageresetpassword"),
|
(ManageResetPassword, Claims.CustomPermissions.ManageResetPassword),
|
||||||
(ManageScim, "managescim"),
|
(ManageScim, Claims.CustomPermissions.ManageScim),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -22,4 +22,21 @@ public static class Claims
|
|||||||
|
|
||||||
// General
|
// General
|
||||||
public const string Type = "type";
|
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 =>
|
public static IEnumerable<object[]> CanExportManagedCollections =>
|
||||||
AuthorizationHelpers.AllRoles().Select(o => new[] { o });
|
PermissionsHelpers.AllRoles().Select(o => new[] { o });
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitMemberAutoData(nameof(CanExportManagedCollections))]
|
[BitMemberAutoData(nameof(CanExportManagedCollections))]
|
||||||
|
@ -4,7 +4,7 @@ using Bit.Core.Models.Data;
|
|||||||
|
|
||||||
namespace Bit.Core.Test.AdminConsole.Helpers;
|
namespace Bit.Core.Test.AdminConsole.Helpers;
|
||||||
|
|
||||||
public static class AuthorizationHelpers
|
public static class PermissionsHelpers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Return a new Permission object with inverted permissions.
|
/// Return a new Permission object with inverted permissions.
|
||||||
@ -36,6 +36,24 @@ public static class AuthorizationHelpers
|
|||||||
return result;
|
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>
|
/// <summary>
|
||||||
/// Returns a sequence of all possible roles and permissions represented as CurrentContextOrganization objects.
|
/// Returns a sequence of all possible roles and permissions represented as CurrentContextOrganization objects.
|
||||||
/// Used largely for authorization testing.
|
/// Used largely for authorization testing.
|
@ -3,7 +3,7 @@ using Xunit;
|
|||||||
|
|
||||||
namespace Bit.Core.Test.AdminConsole.Helpers;
|
namespace Bit.Core.Test.AdminConsole.Helpers;
|
||||||
|
|
||||||
public class AuthorizationHelpersTests
|
public class PermissionsHelpersTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Permissions_Invert_InvertsAllPermissions()
|
public void Permissions_Invert_InvertsAllPermissions()
|
Loading…
x
Reference in New Issue
Block a user