diff --git a/src/Api/AdminConsole/Authorization/AuthorizationHandlerCollectionExtensions.cs b/src/Api/AdminConsole/Authorization/AuthorizationHandlerCollectionExtensions.cs index 612c406518df..b0c4f1cf34ec 100644 --- a/src/Api/AdminConsole/Authorization/AuthorizationHandlerCollectionExtensions.cs +++ b/src/Api/AdminConsole/Authorization/AuthorizationHandlerCollectionExtensions.cs @@ -1,4 +1,5 @@ -using Bit.Api.Vault.AuthorizationHandlers.Collections; +using Bit.Api.AdminConsole.Authorization.Providers; +using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -17,6 +18,7 @@ public static void AddAdminConsoleAuthorizationHandlers(this IServiceCollection ServiceDescriptor.Scoped(), ServiceDescriptor.Scoped(), ServiceDescriptor.Scoped(), + ServiceDescriptor.Scoped(), ServiceDescriptor.Scoped(), ]); } diff --git a/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs b/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs index 5cb261b41d2a..4292a15c656f 100644 --- a/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs +++ b/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs @@ -7,7 +7,10 @@ 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' or 'organizationId' either through the [Controller] attribute or through a '[Http*]' attribute."; + "A route decorated with with '[Authorize]' must include a route value named 'orgId' or 'organizationId' either through the [Controller] attribute or through a '[Http*]' attribute."; + + public const string NoProviderIdError = + "A route decorated with '[Authorize]' must include a route value named 'providerId' 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. @@ -82,4 +85,22 @@ public static Guid GetOrganizationId(this HttpContext httpContext) throw new InvalidOperationException(NoOrgIdError); } + + /// + /// Parses the {providerId} route parameter into a Guid, or throws if it is not present or is not a valid Guid. + /// + /// + public static Guid GetProviderId(this HttpContext httpContext) + { + var routeValues = httpContext.GetRouteData().Values; + + if (routeValues.TryGetValue("providerId", out var providerIdParam) && + providerIdParam != null && + Guid.TryParse(providerIdParam.ToString(), out var providerId)) + { + return providerId; + } + + throw new InvalidOperationException(NoProviderIdError); + } } diff --git a/src/Api/AdminConsole/Authorization/OrgUserLinkedToUserIdHandler.cs b/src/Api/AdminConsole/Authorization/Organizations/Handlers/OrgUserLinkedToUserIdHandler.cs similarity index 100% rename from src/Api/AdminConsole/Authorization/OrgUserLinkedToUserIdHandler.cs rename to src/Api/AdminConsole/Authorization/Organizations/Handlers/OrgUserLinkedToUserIdHandler.cs diff --git a/src/Api/AdminConsole/Authorization/RecoverAccountAuthorizationHandler.cs b/src/Api/AdminConsole/Authorization/Organizations/Handlers/RecoverAccountAuthorizationHandler.cs similarity index 100% rename from src/Api/AdminConsole/Authorization/RecoverAccountAuthorizationHandler.cs rename to src/Api/AdminConsole/Authorization/Organizations/Handlers/RecoverAccountAuthorizationHandler.cs diff --git a/src/Api/AdminConsole/Authorization/IOrganizationRequirement.cs b/src/Api/AdminConsole/Authorization/Organizations/IOrganizationRequirement.cs similarity index 100% rename from src/Api/AdminConsole/Authorization/IOrganizationRequirement.cs rename to src/Api/AdminConsole/Authorization/Organizations/IOrganizationRequirement.cs diff --git a/src/Api/AdminConsole/Authorization/OrganizationClaimsExtensions.cs b/src/Api/AdminConsole/Authorization/Organizations/OrganizationClaimsExtensions.cs similarity index 100% rename from src/Api/AdminConsole/Authorization/OrganizationClaimsExtensions.cs rename to src/Api/AdminConsole/Authorization/Organizations/OrganizationClaimsExtensions.cs diff --git a/src/Api/AdminConsole/Authorization/OrganizationContext.cs b/src/Api/AdminConsole/Authorization/Organizations/OrganizationContext.cs similarity index 100% rename from src/Api/AdminConsole/Authorization/OrganizationContext.cs rename to src/Api/AdminConsole/Authorization/Organizations/OrganizationContext.cs diff --git a/src/Api/AdminConsole/Authorization/OrganizationRequirementHandler.cs b/src/Api/AdminConsole/Authorization/Organizations/OrganizationRequirementHandler.cs similarity index 100% rename from src/Api/AdminConsole/Authorization/OrganizationRequirementHandler.cs rename to src/Api/AdminConsole/Authorization/Organizations/OrganizationRequirementHandler.cs diff --git a/src/Api/AdminConsole/Authorization/Requirements/BasePermissionRequirement.cs b/src/Api/AdminConsole/Authorization/Organizations/Requirements/BasePermissionRequirement.cs similarity index 100% rename from src/Api/AdminConsole/Authorization/Requirements/BasePermissionRequirement.cs rename to src/Api/AdminConsole/Authorization/Organizations/Requirements/BasePermissionRequirement.cs diff --git a/src/Api/AdminConsole/Authorization/Requirements/ManageGroupsOrUsersRequirement.cs b/src/Api/AdminConsole/Authorization/Organizations/Requirements/ManageGroupsOrUsersRequirement.cs similarity index 100% rename from src/Api/AdminConsole/Authorization/Requirements/ManageGroupsOrUsersRequirement.cs rename to src/Api/AdminConsole/Authorization/Organizations/Requirements/ManageGroupsOrUsersRequirement.cs diff --git a/src/Api/AdminConsole/Authorization/Requirements/MemberOrProviderRequirement.cs b/src/Api/AdminConsole/Authorization/Organizations/Requirements/MemberOrProviderRequirement.cs similarity index 100% rename from src/Api/AdminConsole/Authorization/Requirements/MemberOrProviderRequirement.cs rename to src/Api/AdminConsole/Authorization/Organizations/Requirements/MemberOrProviderRequirement.cs diff --git a/src/Api/AdminConsole/Authorization/Requirements/MemberRequirement.cs b/src/Api/AdminConsole/Authorization/Organizations/Requirements/MemberRequirement.cs similarity index 100% rename from src/Api/AdminConsole/Authorization/Requirements/MemberRequirement.cs rename to src/Api/AdminConsole/Authorization/Organizations/Requirements/MemberRequirement.cs diff --git a/src/Api/AdminConsole/Authorization/Requirements/PermissionRequirements.cs b/src/Api/AdminConsole/Authorization/Organizations/Requirements/PermissionRequirements.cs similarity index 100% rename from src/Api/AdminConsole/Authorization/Requirements/PermissionRequirements.cs rename to src/Api/AdminConsole/Authorization/Organizations/Requirements/PermissionRequirements.cs diff --git a/src/Api/AdminConsole/Authorization/Providers/IProviderRequirement.cs b/src/Api/AdminConsole/Authorization/Providers/IProviderRequirement.cs new file mode 100644 index 000000000000..310c0ad174f2 --- /dev/null +++ b/src/Api/AdminConsole/Authorization/Providers/IProviderRequirement.cs @@ -0,0 +1,23 @@ +using Bit.Core.AdminConsole.Context; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Api.AdminConsole.Authorization.Providers; + +/// +/// A requirement that implements this interface will be handled by , +/// which calls AuthorizeAsync with the provider details from the route. +/// This is used for simple role-based checks. +/// This may only be used on endpoints with {providerId} in their path. +/// +public interface IProviderRequirement : IAuthorizationRequirement +{ + /// + /// Whether to authorize a request that has this requirement. + /// + /// + /// The CurrentContextProvider for the user if they are a member of the provider. + /// This is null if they are not a member. + /// + /// True if the requirement has been satisfied, otherwise false. + public bool Authorize(CurrentContextProvider? providerClaims); +} diff --git a/src/Api/AdminConsole/Authorization/Providers/ProviderClaimsExtensions.cs b/src/Api/AdminConsole/Authorization/Providers/ProviderClaimsExtensions.cs new file mode 100644 index 000000000000..db74f685bcf1 --- /dev/null +++ b/src/Api/AdminConsole/Authorization/Providers/ProviderClaimsExtensions.cs @@ -0,0 +1,43 @@ +using System.Security.Claims; +using Bit.Core.AdminConsole.Context; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.Auth.Identity; + +namespace Bit.Api.AdminConsole.Authorization.Providers; + +public static class ProviderClaimsExtensions +{ + /// + /// Parses a user's claims and returns an object representing their claims for the specified provider. + /// + /// The user who has the claims. + /// The providerId to look for in the claims. + /// + /// A representing the user's claims for that provider, or null + /// if the user does not have any claims for that provider. + /// + public static CurrentContextProvider? GetCurrentContextProvider(this ClaimsPrincipal user, Guid providerId) + { + var claimsDict = user.Claims + .GroupBy(c => c.Type) + .ToDictionary( + c => c.Key, + c => c.ToList()); + + bool hasClaim(string claimType) => + claimsDict.TryGetValue(claimType, out var claims) && + claims.Any(c => Guid.TryParse(c.Value, out var id) && id == providerId); + + if (hasClaim(Claims.ProviderAdmin)) + { + return new CurrentContextProvider { Id = providerId, Type = ProviderUserType.ProviderAdmin }; + } + + if (hasClaim(Claims.ProviderServiceUser)) + { + return new CurrentContextProvider { Id = providerId, Type = ProviderUserType.ServiceUser }; + } + + return null; + } +} diff --git a/src/Api/AdminConsole/Authorization/Providers/ProviderRequirementHandler.cs b/src/Api/AdminConsole/Authorization/Providers/ProviderRequirementHandler.cs new file mode 100644 index 000000000000..96dc9ec91ec9 --- /dev/null +++ b/src/Api/AdminConsole/Authorization/Providers/ProviderRequirementHandler.cs @@ -0,0 +1,43 @@ +using Bit.Core.Services; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Api.AdminConsole.Authorization.Providers; + +/// +/// Handles any requirement that implements . +/// Retrieves the Provider ID from the route and then passes the provider claims to the requirement's AuthorizeAsync +/// callback to determine whether the action is authorized. +/// +public class ProviderRequirementHandler( + IHttpContextAccessor httpContextAccessor, + IUserService userService) + : AuthorizationHandler +{ + public const string NoHttpContextError = "This method should only be called in the context of an HTTP Request."; + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IProviderRequirement requirement) + { + var httpContext = httpContextAccessor.HttpContext; + if (httpContext == null) + { + throw new InvalidOperationException(NoHttpContextError); + } + + var providerId = httpContext.GetProviderId(); + + var userId = userService.GetProperUserId(httpContext.User); + if (userId == null) + { + return Task.CompletedTask; + } + + var providerClaims = httpContext.User.GetCurrentContextProvider(providerId); + + if (requirement.Authorize(providerClaims)) + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } +} diff --git a/src/Api/AdminConsole/Authorization/Providers/Requirements/ManageProviderUsersRequirement.cs b/src/Api/AdminConsole/Authorization/Providers/Requirements/ManageProviderUsersRequirement.cs new file mode 100644 index 000000000000..dbd606835ac0 --- /dev/null +++ b/src/Api/AdminConsole/Authorization/Providers/Requirements/ManageProviderUsersRequirement.cs @@ -0,0 +1,6 @@ +namespace Bit.Api.AdminConsole.Authorization.Providers.Requirements; + +/// +/// Authorizes users who can manage provider users (ProviderAdmin only). +/// +public class ManageProviderUsersRequirement : ProviderAdminRequirement; diff --git a/src/Api/AdminConsole/Authorization/Providers/Requirements/ProviderAdminRequirement.cs b/src/Api/AdminConsole/Authorization/Providers/Requirements/ProviderAdminRequirement.cs new file mode 100644 index 000000000000..d98b2de6e5c9 --- /dev/null +++ b/src/Api/AdminConsole/Authorization/Providers/Requirements/ProviderAdminRequirement.cs @@ -0,0 +1,13 @@ +using Bit.Core.AdminConsole.Context; +using Bit.Core.AdminConsole.Enums.Provider; + +namespace Bit.Api.AdminConsole.Authorization.Providers.Requirements; + +/// +/// Authorizes ProviderAdmin users only. +/// +public class ProviderAdminRequirement : IProviderRequirement +{ + public bool Authorize(CurrentContextProvider? providerClaims) + => providerClaims?.Type == ProviderUserType.ProviderAdmin; +} diff --git a/src/Api/AdminConsole/Authorization/Providers/Requirements/ProviderUserRequirement.cs b/src/Api/AdminConsole/Authorization/Providers/Requirements/ProviderUserRequirement.cs new file mode 100644 index 000000000000..3d9e991f843c --- /dev/null +++ b/src/Api/AdminConsole/Authorization/Providers/Requirements/ProviderUserRequirement.cs @@ -0,0 +1,12 @@ +using Bit.Core.AdminConsole.Context; + +namespace Bit.Api.AdminConsole.Authorization.Providers.Requirements; + +/// +/// Authorizes any provider member (ProviderAdmin or ServiceUser). +/// +public class ProviderUserRequirement : IProviderRequirement +{ + public bool Authorize(CurrentContextProvider? providerClaims) + => providerClaims != null; +} diff --git a/test/Api.Test/AdminConsole/Authorization/RecoverAccountAuthorizationHandlerTests.cs b/test/Api.Test/AdminConsole/Authorization/Organizations/Handlers/RecoverAccountAuthorizationHandlerTests.cs similarity index 100% rename from test/Api.Test/AdminConsole/Authorization/RecoverAccountAuthorizationHandlerTests.cs rename to test/Api.Test/AdminConsole/Authorization/Organizations/Handlers/RecoverAccountAuthorizationHandlerTests.cs diff --git a/test/Api.Test/AdminConsole/Authorization/OrganizationClaimsExtensionsTests.cs b/test/Api.Test/AdminConsole/Authorization/Organizations/OrganizationClaimsExtensionsTests.cs similarity index 100% rename from test/Api.Test/AdminConsole/Authorization/OrganizationClaimsExtensionsTests.cs rename to test/Api.Test/AdminConsole/Authorization/Organizations/OrganizationClaimsExtensionsTests.cs diff --git a/test/Api.Test/AdminConsole/Authorization/OrganizationContextTests.cs b/test/Api.Test/AdminConsole/Authorization/Organizations/OrganizationContextTests.cs similarity index 100% rename from test/Api.Test/AdminConsole/Authorization/OrganizationContextTests.cs rename to test/Api.Test/AdminConsole/Authorization/Organizations/OrganizationContextTests.cs diff --git a/test/Api.Test/AdminConsole/Authorization/OrganizationRequirementHandlerTests.cs b/test/Api.Test/AdminConsole/Authorization/Organizations/OrganizationRequirementHandlerTests.cs similarity index 100% rename from test/Api.Test/AdminConsole/Authorization/OrganizationRequirementHandlerTests.cs rename to test/Api.Test/AdminConsole/Authorization/Organizations/OrganizationRequirementHandlerTests.cs diff --git a/test/Api.Test/AdminConsole/Authorization/Requirements/BasePermissionRequirementTests.cs b/test/Api.Test/AdminConsole/Authorization/Organizations/Requirements/BasePermissionRequirementTests.cs similarity index 100% rename from test/Api.Test/AdminConsole/Authorization/Requirements/BasePermissionRequirementTests.cs rename to test/Api.Test/AdminConsole/Authorization/Organizations/Requirements/BasePermissionRequirementTests.cs diff --git a/test/Api.Test/AdminConsole/Authorization/Requirements/ManageGroupsOrUsersRequirementTests.cs b/test/Api.Test/AdminConsole/Authorization/Organizations/Requirements/ManageGroupsOrUsersRequirementTests.cs similarity index 100% rename from test/Api.Test/AdminConsole/Authorization/Requirements/ManageGroupsOrUsersRequirementTests.cs rename to test/Api.Test/AdminConsole/Authorization/Organizations/Requirements/ManageGroupsOrUsersRequirementTests.cs diff --git a/test/Api.Test/AdminConsole/Authorization/Requirements/MemberRequirementTests.cs b/test/Api.Test/AdminConsole/Authorization/Organizations/Requirements/MemberRequirementTests.cs similarity index 100% rename from test/Api.Test/AdminConsole/Authorization/Requirements/MemberRequirementTests.cs rename to test/Api.Test/AdminConsole/Authorization/Organizations/Requirements/MemberRequirementTests.cs diff --git a/test/Api.Test/AdminConsole/Authorization/Requirements/PermissionRequirementsTests.cs b/test/Api.Test/AdminConsole/Authorization/Organizations/Requirements/PermissionRequirementsTests.cs similarity index 100% rename from test/Api.Test/AdminConsole/Authorization/Requirements/PermissionRequirementsTests.cs rename to test/Api.Test/AdminConsole/Authorization/Organizations/Requirements/PermissionRequirementsTests.cs diff --git a/test/Api.Test/AdminConsole/Authorization/Providers/ProviderClaimsExtensionsTests.cs b/test/Api.Test/AdminConsole/Authorization/Providers/ProviderClaimsExtensionsTests.cs new file mode 100644 index 000000000000..617e5b959932 --- /dev/null +++ b/test/Api.Test/AdminConsole/Authorization/Providers/ProviderClaimsExtensionsTests.cs @@ -0,0 +1,65 @@ +using System.Security.Claims; +using Bit.Api.AdminConsole.Authorization.Providers; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.Auth.Identity; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Api.Test.AdminConsole.Authorization.Providers; + +public class ProviderClaimsExtensionsTests +{ + [Theory, BitAutoData] + public void GetCurrentContextProvider_WhenUserIsProviderAdmin_ReturnsProviderAdminClaims(Guid providerId) + { + var claims = new[] { new Claim(Claims.ProviderAdmin, providerId.ToString()) }; + var claimsPrincipal = MakeClaimsPrincipal(claims); + + var result = claimsPrincipal.GetCurrentContextProvider(providerId); + + Assert.NotNull(result); + Assert.Equal(providerId, result.Id); + Assert.Equal(ProviderUserType.ProviderAdmin, result.Type); + } + + [Theory, BitAutoData] + public void GetCurrentContextProvider_WhenUserIsServiceUser_ReturnsServiceUserClaims(Guid providerId) + { + var claims = new[] { new Claim(Claims.ProviderServiceUser, providerId.ToString()) }; + var claimsPrincipal = MakeClaimsPrincipal(claims); + + var result = claimsPrincipal.GetCurrentContextProvider(providerId); + + Assert.NotNull(result); + Assert.Equal(providerId, result.Id); + Assert.Equal(ProviderUserType.ServiceUser, result.Type); + } + + [Theory, BitAutoData] + public void GetCurrentContextProvider_WhenUserIsNotProviderMember_ReturnsNull(Guid providerId) + { + var claimsPrincipal = MakeClaimsPrincipal([]); + + var result = claimsPrincipal.GetCurrentContextProvider(providerId); + + Assert.Null(result); + } + + [Theory, BitAutoData] + public void GetCurrentContextProvider_WhenClaimsContainDifferentProviderId_ReturnsNull(Guid providerId, Guid otherProviderId) + { + var claims = new[] { new Claim(Claims.ProviderAdmin, otherProviderId.ToString()) }; + var claimsPrincipal = MakeClaimsPrincipal(claims); + + var result = claimsPrincipal.GetCurrentContextProvider(providerId); + + Assert.Null(result); + } + + private static ClaimsPrincipal MakeClaimsPrincipal(IEnumerable claims) + { + var principal = new ClaimsPrincipal(); + principal.AddIdentities([new ClaimsIdentity(claims)]); + return principal; + } +} diff --git a/test/Api.Test/AdminConsole/Authorization/Providers/ProviderRequirementHandlerTests.cs b/test/Api.Test/AdminConsole/Authorization/Providers/ProviderRequirementHandlerTests.cs new file mode 100644 index 000000000000..64f5741a716a --- /dev/null +++ b/test/Api.Test/AdminConsole/Authorization/Providers/ProviderRequirementHandlerTests.cs @@ -0,0 +1,110 @@ +using System.Security.Claims; +using Bit.Api.AdminConsole.Authorization; +using Bit.Api.AdminConsole.Authorization.Providers; +using Bit.Core.AdminConsole.Context; +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.Providers; + +[SutProviderCustomize] +public class ProviderRequirementHandlerTests +{ + [Theory] + [BitAutoData((string)null)] + [BitAutoData("malformed guid")] + public async Task IfInvalidProviderId_Throws(string providerId, Guid userId, SutProvider sutProvider) + { + // Arrange + ArrangeRouteAndUser(sutProvider, providerId, 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.NoProviderIdError, 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(ProviderRequirementHandler.NoHttpContextError, exception.Message); + Assert.False(authContext.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task IfUserIdIsNull_DoesNotAuthorize(Guid providerId, SutProvider sutProvider) + { + // Arrange + ArrangeRouteAndUser(sutProvider, providerId.ToString(), null); + var testRequirement = Substitute.For(); + var authContext = new AuthorizationHandlerContext([testRequirement], new ClaimsPrincipal(), null); + + // Act + await sutProvider.Sut.HandleAsync(authContext); + + // Assert — requirement is not invoked and context has not succeeded + testRequirement.DidNotReceive().Authorize(Arg.Any()); + Assert.False(authContext.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task DoesNotAuthorize_IfAuthorizeAsync_ReturnsFalse( + SutProvider sutProvider, Guid providerId, Guid userId) + { + // Arrange + ArrangeRouteAndUser(sutProvider, providerId.ToString(), userId); + + var testRequirement = Substitute.For(); + testRequirement.Authorize(null).ReturnsForAnyArgs(false); + var authContext = new AuthorizationHandlerContext([testRequirement], new ClaimsPrincipal(), null); + + // Act + await sutProvider.Sut.HandleAsync(authContext); + + // Assert + testRequirement.Received(1).Authorize(null); + Assert.False(authContext.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task Authorizes_IfAuthorizeAsync_ReturnsTrue( + SutProvider sutProvider, Guid providerId, Guid userId) + { + // Arrange + ArrangeRouteAndUser(sutProvider, providerId.ToString(), userId); + + var testRequirement = Substitute.For(); + testRequirement.Authorize(null).ReturnsForAnyArgs(true); + var authContext = new AuthorizationHandlerContext([testRequirement], new ClaimsPrincipal(), null); + + // Act + await sutProvider.Sut.HandleAsync(authContext); + + // Assert + testRequirement.Received(1).Authorize(null); + Assert.True(authContext.HasSucceeded); + } + + private static void ArrangeRouteAndUser(SutProvider sutProvider, string providerIdRouteValue, + Guid? userId) + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues["providerId"] = providerIdRouteValue; + sutProvider.GetDependency().HttpContext = httpContext; + sutProvider.GetDependency().GetProperUserId(Arg.Any()).Returns(userId); + } +} diff --git a/test/Api.Test/AdminConsole/Authorization/Providers/Requirements/ManageProviderUsersRequirementTests.cs b/test/Api.Test/AdminConsole/Authorization/Providers/Requirements/ManageProviderUsersRequirementTests.cs new file mode 100644 index 000000000000..27becdba6500 --- /dev/null +++ b/test/Api.Test/AdminConsole/Authorization/Providers/Requirements/ManageProviderUsersRequirementTests.cs @@ -0,0 +1,43 @@ +using Bit.Api.AdminConsole.Authorization.Providers.Requirements; +using Bit.Core.AdminConsole.Context; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Api.Test.AdminConsole.Authorization.Providers.Requirements; + +[SutProviderCustomize] +public class ManageProviderUsersRequirementTests +{ + [Theory, BitAutoData] + public void Authorize_WhenUserIsProviderAdmin_ThenRequestShouldBeAuthorized( + SutProvider sutProvider) + { + var providerClaims = new CurrentContextProvider { Id = Guid.NewGuid(), Type = ProviderUserType.ProviderAdmin }; + + var actual = sutProvider.Sut.Authorize(providerClaims); + + Assert.True(actual); + } + + [Theory, BitAutoData] + public void Authorize_WhenUserIsServiceUser_ThenRequestShouldBeDenied( + SutProvider sutProvider) + { + var providerClaims = new CurrentContextProvider { Id = Guid.NewGuid(), Type = ProviderUserType.ServiceUser }; + + var actual = sutProvider.Sut.Authorize(providerClaims); + + Assert.False(actual); + } + + [Theory, BitAutoData] + public void Authorize_WhenUserIsNotProviderMember_ThenRequestShouldBeDenied( + SutProvider sutProvider) + { + var actual = sutProvider.Sut.Authorize(null); + + Assert.False(actual); + } +} diff --git a/test/Api.Test/AdminConsole/Authorization/Providers/Requirements/ProviderAdminRequirementTests.cs b/test/Api.Test/AdminConsole/Authorization/Providers/Requirements/ProviderAdminRequirementTests.cs new file mode 100644 index 000000000000..49ebf2d4c3da --- /dev/null +++ b/test/Api.Test/AdminConsole/Authorization/Providers/Requirements/ProviderAdminRequirementTests.cs @@ -0,0 +1,43 @@ +using Bit.Api.AdminConsole.Authorization.Providers.Requirements; +using Bit.Core.AdminConsole.Context; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Api.Test.AdminConsole.Authorization.Providers.Requirements; + +[SutProviderCustomize] +public class ProviderAdminRequirementTests +{ + [Theory, BitAutoData] + public void Authorize_WhenUserIsProviderAdmin_ThenRequestShouldBeAuthorized( + SutProvider sutProvider) + { + var providerClaims = new CurrentContextProvider { Id = Guid.NewGuid(), Type = ProviderUserType.ProviderAdmin }; + + var actual = sutProvider.Sut.Authorize(providerClaims); + + Assert.True(actual); + } + + [Theory, BitAutoData] + public void Authorize_WhenUserIsServiceUser_ThenRequestShouldBeDenied( + SutProvider sutProvider) + { + var providerClaims = new CurrentContextProvider { Id = Guid.NewGuid(), Type = ProviderUserType.ServiceUser }; + + var actual = sutProvider.Sut.Authorize(providerClaims); + + Assert.False(actual); + } + + [Theory, BitAutoData] + public void Authorize_WhenUserIsNotProviderMember_ThenRequestShouldBeDenied( + SutProvider sutProvider) + { + var actual = sutProvider.Sut.Authorize(null); + + Assert.False(actual); + } +} diff --git a/test/Api.Test/AdminConsole/Authorization/Providers/Requirements/ProviderUserRequirementTests.cs b/test/Api.Test/AdminConsole/Authorization/Providers/Requirements/ProviderUserRequirementTests.cs new file mode 100644 index 000000000000..9455be2867d6 --- /dev/null +++ b/test/Api.Test/AdminConsole/Authorization/Providers/Requirements/ProviderUserRequirementTests.cs @@ -0,0 +1,43 @@ +using Bit.Api.AdminConsole.Authorization.Providers.Requirements; +using Bit.Core.AdminConsole.Context; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Api.Test.AdminConsole.Authorization.Providers.Requirements; + +[SutProviderCustomize] +public class ProviderUserRequirementTests +{ + [Theory, BitAutoData] + public void Authorize_WhenUserIsProviderAdmin_ThenRequestShouldBeAuthorized( + SutProvider sutProvider) + { + var providerClaims = new CurrentContextProvider { Id = Guid.NewGuid(), Type = ProviderUserType.ProviderAdmin }; + + var actual = sutProvider.Sut.Authorize(providerClaims); + + Assert.True(actual); + } + + [Theory, BitAutoData] + public void Authorize_WhenUserIsServiceUser_ThenRequestShouldBeAuthorized( + SutProvider sutProvider) + { + var providerClaims = new CurrentContextProvider { Id = Guid.NewGuid(), Type = ProviderUserType.ServiceUser }; + + var actual = sutProvider.Sut.Authorize(providerClaims); + + Assert.True(actual); + } + + [Theory, BitAutoData] + public void Authorize_WhenUserIsNotProviderMember_ThenRequestShouldBeDenied( + SutProvider sutProvider) + { + var actual = sutProvider.Sut.Authorize(null); + + Assert.False(actual); + } +}