diff --git a/src/Api/AdminConsole/Authorization/AuthorizeAttribute.cs b/src/Api/AdminConsole/Authorization/AuthorizeAttribute.cs new file mode 100644 index 0000000000..1f864ece66 --- /dev/null +++ b/src/Api/AdminConsole/Authorization/AuthorizeAttribute.cs @@ -0,0 +1,21 @@ +#nullable enable + +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Api.AdminConsole.Authorization; + +/// +/// An attribute which requires authorization using the specified requirement. +/// This uses the standard ASP.NET authorization middleware. +/// +/// The IAuthorizationRequirement that will be used to authorize the user. +public class AuthorizeAttribute + : AuthorizeAttribute, IAuthorizationRequirementData + where T : IAuthorizationRequirement, new() +{ + public IEnumerable GetRequirements() + { + var requirement = new T(); + return [requirement]; + } +} diff --git a/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs b/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs new file mode 100644 index 0000000000..ba00ea6c18 --- /dev/null +++ b/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs @@ -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]' must include a route value named 'orgId' either through the [Controller] attribute or through a '[Http*]' attribute."; + + /// + /// 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. + /// + public static async Task WithFeaturesCacheAsync(this HttpContext httpContext, Func> callback) + { + var cachedResult = httpContext.Features.Get(); + if (cachedResult != null) + { + return cachedResult; + } + + var result = await callback(); + httpContext.Features.Set(result); + + return result; + } + + /// + /// Returns true if the user is a ProviderUser for a Provider which manages the specified organization, otherwise false. + /// + /// + /// This data is fetched from the database and cached as a HttpContext Feature for the lifetime of the request. + /// + public static async Task IsProviderUserForOrgAsync( + this HttpContext httpContext, + IProviderUserRepository providerUserRepository, + Guid userId, + Guid organizationId) + { + var organizations = await httpContext.GetProviderUserOrganizationsAsync(providerUserRepository, userId); + return organizations.Any(o => o.OrganizationId == organizationId); + } + + /// + /// Returns the ProviderUserOrganizations for a user. These are the organizations the ProviderUser manages via their Provider, if any. + /// + /// + /// This data is fetched from the database and cached as a HttpContext Feature for the lifetime of the request. + /// + private static async Task> GetProviderUserOrganizationsAsync( + this HttpContext httpContext, + IProviderUserRepository providerUserRepository, + Guid userId) + => await httpContext.WithFeaturesCacheAsync(() => + providerUserRepository.GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed)); + + + /// + /// Parses the {orgId} route parameter into a Guid, or throws if the {orgId} is not present or not a valid guid. + /// + /// + /// + /// + 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; + } +} diff --git a/src/Api/AdminConsole/Authorization/IOrganizationRequirement.cs b/src/Api/AdminConsole/Authorization/IOrganizationRequirement.cs new file mode 100644 index 0000000000..007647f4c0 --- /dev/null +++ b/src/Api/AdminConsole/Authorization/IOrganizationRequirement.cs @@ -0,0 +1,31 @@ +#nullable enable + +using Bit.Core.Context; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Api.AdminConsole.Authorization; + +/// +/// A requirement that implements this interface will be handled by , +/// 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. +/// +public interface IOrganizationRequirement : IAuthorizationRequirement +{ + /// + /// Whether to authorize a request that has this requirement. + /// + /// + /// The CurrentContextOrganization for the user if they are a member of the organization. + /// This is null if they are not a member. + /// + /// + /// 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. + /// + /// True if the requirement has been satisfied, otherwise false. + public Task AuthorizeAsync( + CurrentContextOrganization? organizationClaims, + Func> isProviderUserForOrg); +} diff --git a/src/Api/AdminConsole/Authorization/OrganizationClaimsExtensions.cs b/src/Api/AdminConsole/Authorization/OrganizationClaimsExtensions.cs new file mode 100644 index 0000000000..e21d153bab --- /dev/null +++ b/src/Api/AdminConsole/Authorization/OrganizationClaimsExtensions.cs @@ -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 +{ + /// + /// Parses a user's claims and returns an object representing their claims for the specified organization. + /// + /// The user who has the claims. + /// The organizationId to look for in the claims. + /// + /// A representing the user's claims for that organization, or null + /// if the user does not have any claims for that organization. + /// + 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() + }; + } + + /// + /// Returns a function for evaluating claims for the specified user and organizationId. + /// The function returns true if the claim type exists and false otherwise. + /// + private static Func 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); + } + + /// + /// Parses the provided claims into proper Guids, or ignore them if they are not valid guids. + /// + private static IEnumerable ParseGuids(this IEnumerable claims) + { + foreach (var claim in claims) + { + if (Guid.TryParse(claim.Value, out var guid)) + { + yield return guid; + } + } + } + + private static OrganizationUserType? GetRoleFromClaims(Func 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 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), + }; +} diff --git a/src/Api/AdminConsole/Authorization/OrganizationRequirementHandler.cs b/src/Api/AdminConsole/Authorization/OrganizationRequirementHandler.cs new file mode 100644 index 0000000000..178090fadf --- /dev/null +++ b/src/Api/AdminConsole/Authorization/OrganizationRequirementHandler.cs @@ -0,0 +1,49 @@ +#nullable enable + +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Services; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Api.AdminConsole.Authorization; + +/// +/// Handles any requirement that implements . +/// Retrieves the Organization ID from the route and then passes it to the requirement's AuthorizeAsync callback to +/// determine whether the action is authorized. +/// +public class OrganizationRequirementHandler( + IHttpContextAccessor httpContextAccessor, + IProviderUserRepository providerUserRepository, + IUserService userService) + : AuthorizationHandler +{ + 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 IsProviderUserForOrg() => httpContext.IsProviderUserForOrgAsync(providerUserRepository, userId.Value, organizationId); + + var authorized = await requirement.AuthorizeAsync(organizationClaims, IsProviderUserForOrg); + + if (authorized) + { + context.Succeed(requirement); + } + } +} diff --git a/src/Api/AdminConsole/Authorization/Requirements/ManageUsersRequirement.cs b/src/Api/AdminConsole/Authorization/Requirements/ManageUsersRequirement.cs new file mode 100644 index 0000000000..84f38e36c2 --- /dev/null +++ b/src/Api/AdminConsole/Authorization/Requirements/ManageUsersRequirement.cs @@ -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 AuthorizeAsync( + CurrentContextOrganization? organizationClaims, + Func> isProviderUserForOrg) + => organizationClaims switch + { + { Type: OrganizationUserType.Owner } => true, + { Type: OrganizationUserType.Admin } => true, + { Permissions.ManageUsers: true } => true, + _ => await isProviderUserForOrg() + }; +} diff --git a/src/Api/AdminConsole/Authorization/Requirements/MemberOrProviderRequirement.cs b/src/Api/AdminConsole/Authorization/Requirements/MemberOrProviderRequirement.cs new file mode 100644 index 0000000000..030509adef --- /dev/null +++ b/src/Api/AdminConsole/Authorization/Requirements/MemberOrProviderRequirement.cs @@ -0,0 +1,16 @@ +#nullable enable + +using Bit.Core.Context; + +namespace Bit.Api.AdminConsole.Authorization.Requirements; + +/// +/// Requires that the user is a member of the organization or a provider for the organization. +/// +public class MemberOrProviderRequirement : IOrganizationRequirement +{ + public async Task AuthorizeAsync( + CurrentContextOrganization? organizationClaims, + Func> isProviderUserForOrg) + => organizationClaims is not null || await isProviderUserForOrg(); +} diff --git a/src/Api/Utilities/ServiceCollectionExtensions.cs b/src/Api/Utilities/ServiceCollectionExtensions.cs index feeac03e54..4c8589657e 100644 --- a/src/Api/Utilities/ServiceCollectionExtensions.cs +++ b/src/Api/Utilities/ServiceCollectionExtensions.cs @@ -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(); services.AddScoped(); services.AddScoped(); + + services.AddScoped(); } } diff --git a/src/Core/AdminConsole/Models/Data/Permissions.cs b/src/Core/AdminConsole/Models/Data/Permissions.cs index 9edc3f1d50..def468f18d 100644 --- a/src/Core/AdminConsole/Models/Data/Permissions.cs +++ b/src/Core/AdminConsole/Models/Data/Permissions.cs @@ -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), }; } diff --git a/src/Core/Identity/Claims.cs b/src/Core/Identity/Claims.cs index 65d5eb210a..fad7b37b5f 100644 --- a/src/Core/Identity/Claims.cs +++ b/src/Core/Identity/Claims.cs @@ -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"; + } } diff --git a/test/Api.Test/AdminConsole/Authorization/HttpContextExtensionsTests.cs b/test/Api.Test/AdminConsole/Authorization/HttpContextExtensionsTests.cs new file mode 100644 index 0000000000..1901742777 --- /dev/null +++ b/test/Api.Test/AdminConsole/Authorization/HttpContextExtensionsTests.cs @@ -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>>(); + 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(); + } + +} diff --git a/test/Api.Test/AdminConsole/Authorization/OrganizationClaimsExtensionsTests.cs b/test/Api.Test/AdminConsole/Authorization/OrganizationClaimsExtensionsTests.cs new file mode 100644 index 0000000000..7e2e51a6e1 --- /dev/null +++ b/test/Api.Test/AdminConsole/Authorization/OrganizationClaimsExtensionsTests.cs @@ -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 GetTestOrganizations() + { + var roles = new List { 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 + } + ]; + } + } +} diff --git a/test/Api.Test/AdminConsole/Authorization/OrganizationRequirementHandlerTests.cs b/test/Api.Test/AdminConsole/Authorization/OrganizationRequirementHandlerTests.cs new file mode 100644 index 0000000000..117630cd74 --- /dev/null +++ b/test/Api.Test/AdminConsole/Authorization/OrganizationRequirementHandlerTests.cs @@ -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 sutProvider) + { + // Arrange + ArrangeRouteAndUser(sutProvider, orgId, userId); + var testRequirement = Substitute.For(); + var authContext = new AuthorizationHandlerContext([testRequirement], new ClaimsPrincipal(), null); + + // Act + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.HandleAsync(authContext)); + Assert.Contains(HttpContextExtensions.NoOrgIdError, exception.Message); + Assert.False(authContext.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task IfHttpContextIsNull_Throws(SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency().HttpContext = null; + var testRequirement = Substitute.For(); + var authContext = new AuthorizationHandlerContext([testRequirement], new ClaimsPrincipal(), null); + + // Act + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.HandleAsync(authContext)); + Assert.Contains(OrganizationRequirementHandler.NoHttpContextError, exception.Message); + Assert.False(authContext.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task IfUserIdIsNull_Throws(Guid orgId, SutProvider sutProvider) + { + // Arrange + ArrangeRouteAndUser(sutProvider, orgId.ToString(), null); + var testRequirement = Substitute.For(); + var authContext = new AuthorizationHandlerContext([testRequirement], new ClaimsPrincipal(), null); + + // Act + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.HandleAsync(authContext)); + Assert.Contains(OrganizationRequirementHandler.NoUserIdError, exception.Message); + Assert.False(authContext.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task DoesNotAuthorize_IfAuthorizeAsync_ReturnsFalse( + SutProvider sutProvider, Guid organizationId, Guid userId) + { + // Arrange route values + ArrangeRouteAndUser(sutProvider, organizationId.ToString(), userId); + + // Arrange requirement + var testRequirement = Substitute.For(); + testRequirement + .AuthorizeAsync(null, Arg.Any>>()) + .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>>()); + Assert.False(authContext.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task Authorizes_IfAuthorizeAsync_ReturnsTrue( + SutProvider sutProvider, Guid organizationId, Guid userId) + { + // Arrange route values + ArrangeRouteAndUser(sutProvider, organizationId.ToString(), userId); + + // Arrange requirement + var testRequirement = Substitute.For(); + testRequirement + .AuthorizeAsync(null, Arg.Any>>()) + .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>>()); + Assert.True(authContext.HasSucceeded); + } + + private static void ArrangeRouteAndUser(SutProvider sutProvider, string orgIdRouteValue, + Guid? userId) + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues["orgId"] = orgIdRouteValue; + sutProvider.GetDependency().HttpContext = httpContext; + sutProvider.GetDependency().GetProperUserId(Arg.Any()).Returns(userId); + } +} diff --git a/test/Api.Test/Tools/Authorization/VaultExportAuthorizationHandlerTests.cs b/test/Api.Test/Tools/Authorization/VaultExportAuthorizationHandlerTests.cs index 6c42205b1a..c876e1925b 100644 --- a/test/Api.Test/Tools/Authorization/VaultExportAuthorizationHandlerTests.cs +++ b/test/Api.Test/Tools/Authorization/VaultExportAuthorizationHandlerTests.cs @@ -64,7 +64,7 @@ public class VaultExportAuthorizationHandlerTests } public static IEnumerable CanExportManagedCollections => - AuthorizationHelpers.AllRoles().Select(o => new[] { o }); + PermissionsHelpers.AllRoles().Select(o => new[] { o }); [Theory] [BitMemberAutoData(nameof(CanExportManagedCollections))] diff --git a/test/Core.Test/AdminConsole/Helpers/AuthorizationHelpers.cs b/test/Core.Test/AdminConsole/Helpers/PermissionsHelpers.cs similarity index 74% rename from test/Core.Test/AdminConsole/Helpers/AuthorizationHelpers.cs rename to test/Core.Test/AdminConsole/Helpers/PermissionsHelpers.cs index 854cdcb3c8..f346c47624 100644 --- a/test/Core.Test/AdminConsole/Helpers/AuthorizationHelpers.cs +++ b/test/Core.Test/AdminConsole/Helpers/PermissionsHelpers.cs @@ -4,7 +4,7 @@ using Bit.Core.Models.Data; namespace Bit.Core.Test.AdminConsole.Helpers; -public static class AuthorizationHelpers +public static class PermissionsHelpers { /// /// Return a new Permission object with inverted permissions. @@ -36,6 +36,24 @@ public static class AuthorizationHelpers return result; } + /// + /// Returns a sequence of Permission objects, where each Permission object has a different permission flag set. + /// + public static IEnumerable 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; + } + } + /// /// Returns a sequence of all possible roles and permissions represented as CurrentContextOrganization objects. /// Used largely for authorization testing. diff --git a/test/Core.Test/AdminConsole/Helpers/AuthorizationHelpersTests.cs b/test/Core.Test/AdminConsole/Helpers/PermissionsHelpersTests.cs similarity index 95% rename from test/Core.Test/AdminConsole/Helpers/AuthorizationHelpersTests.cs rename to test/Core.Test/AdminConsole/Helpers/PermissionsHelpersTests.cs index db128ffc4b..0873f045bc 100644 --- a/test/Core.Test/AdminConsole/Helpers/AuthorizationHelpersTests.cs +++ b/test/Core.Test/AdminConsole/Helpers/PermissionsHelpersTests.cs @@ -3,7 +3,7 @@ using Xunit; namespace Bit.Core.Test.AdminConsole.Helpers; -public class AuthorizationHelpersTests +public class PermissionsHelpersTests { [Fact] public void Permissions_Invert_InvertsAllPermissions()