1
0
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:
Thomas Rittson 2025-04-15 14:36:00 +10:00 committed by GitHub
parent c9a42d861c
commit 84a984a9e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 590 additions and 16 deletions

View 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];
}
}

View 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;
}
}

View File

@ -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);
}

View File

@ -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),
};
}

View File

@ -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);
}
}
}

View File

@ -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()
};
}

View File

@ -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();
}

View File

@ -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>();
} }
} }

View File

@ -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),
}; };
} }

View File

@ -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";
}
} }

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 => 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))]

View File

@ -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.

View File

@ -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()