diff --git a/KeycloakAuthorizationServicesDotNet.sln b/KeycloakAuthorizationServicesDotNet.sln index 14032f0e..1883ce6a 100644 --- a/KeycloakAuthorizationServicesDotNet.sln +++ b/KeycloakAuthorizationServicesDotNet.sln @@ -62,6 +62,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Keycloak.AuthServices.Sdk.T EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Keycloak.AuthServices.Authentication.Tests", "tests\Keycloak.AuthServices.Authentication.Tests\Keycloak.AuthServices.Authentication.Tests.csproj", "{FE34728A-25AA-44E1-A3A6-AB500307C406}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Keycloak.AuthServices.Authorization.Tests", "tests\Keycloak.AuthServices.Authorization.Tests\Keycloak.AuthServices.Authorization.Tests.csproj", "{DCFB0819-72C9-42DF-8AC6-3FE1E25C4103}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -124,6 +126,10 @@ Global {FE34728A-25AA-44E1-A3A6-AB500307C406}.Debug|Any CPU.Build.0 = Debug|Any CPU {FE34728A-25AA-44E1-A3A6-AB500307C406}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE34728A-25AA-44E1-A3A6-AB500307C406}.Release|Any CPU.Build.0 = Release|Any CPU + {DCFB0819-72C9-42DF-8AC6-3FE1E25C4103}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DCFB0819-72C9-42DF-8AC6-3FE1E25C4103}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DCFB0819-72C9-42DF-8AC6-3FE1E25C4103}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DCFB0819-72C9-42DF-8AC6-3FE1E25C4103}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -144,6 +150,7 @@ Global {5BA4F4CA-9C35-4D9F-A3AC-1E4273DA0807} = {AEBE10B1-96B1-4060-B8C1-1F9BFA7A586C} {A85B6B1E-2030-47BE-9B9F-F645B08E501D} = {96857509-627A-4FD2-AC82-34387619A7B1} {FE34728A-25AA-44E1-A3A6-AB500307C406} = {96857509-627A-4FD2-AC82-34387619A7B1} + {DCFB0819-72C9-42DF-8AC6-3FE1E25C4103} = {96857509-627A-4FD2-AC82-34387619A7B1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E1907BFD-C144-4B48-AA40-972F499D4E08} diff --git a/src/Keycloak.AuthServices.Authorization/UriBasedResourceProtection/UriBasedResourceProtectionMiddleware.cs b/src/Keycloak.AuthServices.Authorization/UriBasedResourceProtection/UriBasedResourceProtectionMiddleware.cs index b4d79d12..bc0e7884 100644 --- a/src/Keycloak.AuthServices.Authorization/UriBasedResourceProtection/UriBasedResourceProtectionMiddleware.cs +++ b/src/Keycloak.AuthServices.Authorization/UriBasedResourceProtection/UriBasedResourceProtectionMiddleware.cs @@ -26,7 +26,7 @@ public class UriBasedResourceProtectionMiddleware /// Thrown, if is null. public UriBasedResourceProtectionMiddleware(RequestDelegate next, IKeycloakProtectionClient client) { - this.next = next; + this.next = next ?? throw new ArgumentNullException(nameof(next)); this.client = client ?? throw new ArgumentNullException(nameof(client)); } @@ -72,10 +72,10 @@ private async Task EvaluateAuthorization(HttpContext context) if (isAuthorized) { - context.Response.StatusCode = StatusCodes.Status401Unauthorized; return true; } + context.Response.StatusCode = StatusCodes.Status401Unauthorized; return false; } diff --git a/tests/Keycloak.AuthServices.Authorization.Tests/Keycloak.AuthServices.Authorization.Tests.csproj b/tests/Keycloak.AuthServices.Authorization.Tests/Keycloak.AuthServices.Authorization.Tests.csproj new file mode 100644 index 00000000..13b89eea --- /dev/null +++ b/tests/Keycloak.AuthServices.Authorization.Tests/Keycloak.AuthServices.Authorization.Tests.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + diff --git a/tests/Keycloak.AuthServices.Authorization.Tests/UriBasedResourceProtection/UriBasedResourceProtectionMiddlewareTests.cs b/tests/Keycloak.AuthServices.Authorization.Tests/UriBasedResourceProtection/UriBasedResourceProtectionMiddlewareTests.cs new file mode 100644 index 00000000..232b3a0b --- /dev/null +++ b/tests/Keycloak.AuthServices.Authorization.Tests/UriBasedResourceProtection/UriBasedResourceProtectionMiddlewareTests.cs @@ -0,0 +1,223 @@ +namespace Keycloak.AuthServices.Authorization.Tests; + +using FluentAssertions; +using Keycloak.AuthServices.Sdk.AuthZ; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Moq; + +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. +[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "")] +public class UriBasedResourceProtectionMiddlewareTests +{ + [Fact] + public void Constructor_DelegateNull_ThrowArgumentNullException() + { + // Arrange + var clientMock = new Mock(); + + // Act & Assert + Assert.Throws(() => _ = new UriBasedResourceProtectionMiddleware(null, clientMock.Object)); + } + + [Fact] + public void Constructor_KeycloakClientNull_ThrowArgumentNullException() + { + // Arrange + var requestDelegateMock = new Mock(); + + // Act & Assert + Assert.Throws(() => _ = new UriBasedResourceProtectionMiddleware(requestDelegateMock.Object, null)); + } + + [Fact] + public async void EvaluateAuthorization_NoAdditionalAttributesGiven_CallAuthorizationClientWithExpectedParameters() + { + // Arrange + var resourceName = "/resourceName"; + var scope = "GET"; + + var clientMock = new Mock(); + var requestDelegateMock = new Mock(); + clientMock.Setup(x => x.VerifyAccessToResource(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + var httpRequestMock = new Mock(); + httpRequestMock.Setup(x => x.Path).Returns(resourceName); + httpRequestMock.Setup(x => x.Method).Returns(scope); + + var httpContextMock = new Mock(); + httpContextMock.Setup(x => x.Request).Returns(httpRequestMock.Object); + + // Act + var target = new UriBasedResourceProtectionMiddleware(requestDelegateMock.Object, clientMock.Object); + await target.InvokeAsync(httpContextMock.Object); + + // Assert + clientMock.Verify(x => x.VerifyAccessToResource(resourceName, scope, CancellationToken.None), Times.Once); + } + + [Fact] + public async void EvaluateAuthorization_DisableAttributesGiven_DontCallAuthorizationClientButCallDelegate() + { + // Arrange + var clientMock = new Mock(); + var requestDelegateMock = new Mock(); + + var httpContextMock = new Mock(); + httpContextMock.Setup(x => x.Request).Returns(new Mock().Object); + SetAttributesOnContextMock(httpContextMock, + new List() { new DisableUriBasedResourceProtectionAttribute() }, + requestDelegateMock.Object); + + // Act + var target = new UriBasedResourceProtectionMiddleware(requestDelegateMock.Object, clientMock.Object); + await target.InvokeAsync(httpContextMock.Object); + + // Assert + clientMock.Verify(x => x.VerifyAccessToResource(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + requestDelegateMock.Verify(x => x.Invoke(httpContextMock.Object), Times.Once); + } + + [Fact] + public async void EvaluateAuthorization_AllowAnonymousAttributesGiven_DontCallAuthorizationClientButCallDelegate() + { + // Arrange + var clientMock = new Mock(); + var requestDelegateMock = new Mock(); + + var httpContextMock = new Mock(); + httpContextMock.Setup(x => x.Request).Returns(new Mock().Object); + SetAttributesOnContextMock(httpContextMock, + new List() { new AllowAnonymousAttribute() }, + requestDelegateMock.Object); + + // Act + var target = new UriBasedResourceProtectionMiddleware(requestDelegateMock.Object, clientMock.Object); + await target.InvokeAsync(httpContextMock.Object); + + // Assert + clientMock.Verify(x => x.VerifyAccessToResource(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + requestDelegateMock.Verify(x => x.Invoke(httpContextMock.Object), Times.Once); + } + + [Fact] + public async void EvaluateAuthorization_DisableAndExplicitAuthAttributesGiven_DontCallAuthorizationClientButCallDelegate() + { + // Arrange + var clientMock = new Mock(); + var requestDelegateMock = new Mock(); + + var httpContextMock = new Mock(); + httpContextMock.Setup(x => x.Request).Returns(new Mock().Object); + SetAttributesOnContextMock(httpContextMock, + new List() { + new DisableUriBasedResourceProtectionAttribute(), + new ExplicitResourceProtectionAttribute(It.IsAny(), It.IsAny()) + }, + requestDelegateMock.Object); + + // Act + var target = new UriBasedResourceProtectionMiddleware(requestDelegateMock.Object, clientMock.Object); + await target.InvokeAsync(httpContextMock.Object); + + // Assert + clientMock.Verify(x => x.VerifyAccessToResource(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + requestDelegateMock.Verify(x => x.Invoke(httpContextMock.Object), Times.Once); + } + + [Fact] + public async void EvaluateAuthorization_ExplicitAuthAttributesGiven_CallAuthorizationClientWithAttributeProperties() + { + // Arrange + var resourceName = "/resourceName"; + var scope = "GET"; + var resourceNameFromAttribute = "/resourceNameFromAttribute"; + var scopeFromAttribute = "GET"; + + var clientMock = new Mock(); + clientMock.Setup(x => x.VerifyAccessToResource(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + var requestDelegateMock = new Mock(); + + var httpRequestMock = new Mock(); + httpRequestMock.Setup(x => x.Path).Returns(resourceName); + httpRequestMock.Setup(x => x.Method).Returns(scope); + + var httpContextMock = new Mock(); + httpContextMock.Setup(x => x.Request).Returns(httpRequestMock.Object); + SetAttributesOnContextMock(httpContextMock, + new List() { + new ExplicitResourceProtectionAttribute(resourceNameFromAttribute, scopeFromAttribute) + }, + requestDelegateMock.Object); + + // Act + var target = new UriBasedResourceProtectionMiddleware(requestDelegateMock.Object, clientMock.Object); + await target.InvokeAsync(httpContextMock.Object); + + // Assert + clientMock.Verify(x => x.VerifyAccessToResource(resourceNameFromAttribute, scopeFromAttribute, CancellationToken.None), Times.Once); + clientMock.Verify(x => x.VerifyAccessToResource(resourceName, scope, CancellationToken.None), Times.Never); + } + + [Fact] + public async void EvaluateAuthorization_ClientReturnsTrue_CallDelegate() + { + // Arrange + var clientMock = new Mock(); + clientMock.Setup(x => x.VerifyAccessToResource(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + var requestDelegateMock = new Mock(); + + var httpContextMock = new Mock(); + httpContextMock.Setup(x => x.Request).Returns(new Mock().Object); + + // Act + var target = new UriBasedResourceProtectionMiddleware(requestDelegateMock.Object, clientMock.Object); + await target.InvokeAsync(httpContextMock.Object); + + // Assert + requestDelegateMock.Verify(x => x.Invoke(httpContextMock.Object), Times.Once); + } + + [Fact] + public async void EvaluateAuthorization_ClientReturnsFalse_DontCallDelegateAndSet401() + { + // Arrange + + var clientMock = new Mock(); + clientMock.Setup(x => x.VerifyAccessToResource(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(false); + var requestDelegateMock = new Mock(); + + var httpResponseMock = new Mock(); + httpResponseMock.SetupAllProperties(); + + var httpContextMock = new Mock(); + httpContextMock.Setup(x => x.Request).Returns(new Mock().Object); + httpContextMock.Setup(x => x.Response).Returns(httpResponseMock.Object); + + // Act + var target = new UriBasedResourceProtectionMiddleware(requestDelegateMock.Object, clientMock.Object); + await target.InvokeAsync(httpContextMock.Object); + + // Assert + requestDelegateMock.Verify(x => x.Invoke(httpContextMock.Object), Times.Never); + httpContextMock.Object.Response.StatusCode.Should().Be(StatusCodes.Status401Unauthorized); + } + + private static void SetAttributesOnContextMock(Mock httpContextMock, IEnumerable attributes, RequestDelegate requestDelegate) + { + var endpoint = new Endpoint(requestDelegate, new EndpointMetadataCollection(attributes), null); + + var endpointFeatureMock = new Mock(); + endpointFeatureMock.Setup(x => x.Endpoint).Returns(endpoint); + + var featuresMock = new Mock(); + featuresMock.Setup(x => x.Get()).Returns(endpointFeatureMock.Object); + + httpContextMock.Setup(x => x.Features).Returns(featuresMock.Object); + } +} \ No newline at end of file