From ea5ce9839fab12d363fe7f4f98ac0d0904c8e7a0 Mon Sep 17 00:00:00 2001 From: "annette.findley" Date: Wed, 8 May 2024 12:24:45 -0400 Subject: [PATCH] Added Auth extension method to check for authorization in the pipeline. --- Directory.Build.targets | 52 ++++----- Hyperbee.Pipeline.sln | 17 ++- .../Hyperbee.Pipeline.Auth.csproj | 42 +++++++ src/Hyperbee.Pipeline.Auth/LICENSE | 23 ++++ .../PipelineAuthExtensions.cs | 68 +++++++++++ src/Hyperbee.Pipeline.Auth/README.md | 48 ++++++++ .../ServiceCollectionExtensions.cs | 47 ++++++++ .../Hyperbee.Pipeline.Auth.Tests.csproj | 32 ++++++ .../PipelineClaimsTests.cs | 107 ++++++++++++++++++ .../TestSupport/ClaimsPrincipalFixture.cs | 15 +++ .../Hyperbee.Pipeline.Caching.Tests.csproj | 12 +- 11 files changed, 434 insertions(+), 29 deletions(-) create mode 100644 src/Hyperbee.Pipeline.Auth/Hyperbee.Pipeline.Auth.csproj create mode 100644 src/Hyperbee.Pipeline.Auth/LICENSE create mode 100644 src/Hyperbee.Pipeline.Auth/PipelineAuthExtensions.cs create mode 100644 src/Hyperbee.Pipeline.Auth/README.md create mode 100644 src/Hyperbee.Pipeline.Auth/ServiceCollectionExtensions.cs create mode 100644 test/Hyperbee.Pipeline.Auth.Tests/Hyperbee.Pipeline.Auth.Tests.csproj create mode 100644 test/Hyperbee.Pipeline.Auth.Tests/PipelineClaimsTests.cs create mode 100644 test/Hyperbee.Pipeline.Auth.Tests/TestSupport/ClaimsPrincipalFixture.cs diff --git a/Directory.Build.targets b/Directory.Build.targets index ab19c8b..bb9d95e 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,38 +1,38 @@  - - - - + + + - - - - $(MajorVersion).$(MinorVersion).$(PatchVersion) - $(VersionPrefix) - $(VersionPrefix)-$(VersionSuffix) - - + + + $(MajorVersion).$(MinorVersion).$(PatchVersion) + $(VersionPrefix) + $(VersionPrefix)-$(VersionSuffix) + + - - - --source $(PackageSource) - - + + + --source $(PackageSource) + + - - - --api-key $(PackageApiKey) - - + + + --api-key $(PackageApiKey) + + \ No newline at end of file diff --git a/Hyperbee.Pipeline.sln b/Hyperbee.Pipeline.sln index e68526a..85b50f8 100644 --- a/Hyperbee.Pipeline.sln +++ b/Hyperbee.Pipeline.sln @@ -41,9 +41,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{884A8242-3 docs\todo.md = docs\todo.md EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hyperbee.Pipeline.Caching", "src\Hyperbee.Pipline.Caching\Hyperbee.Pipeline.Caching.csproj", "{833A7497-542F-4B88-A76A-DA520E000F6F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hyperbee.Pipeline.Caching", "src\Hyperbee.Pipline.Caching\Hyperbee.Pipeline.Caching.csproj", "{833A7497-542F-4B88-A76A-DA520E000F6F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hyperbee.Pipeline.Caching.Tests", "test\Hyperbee.PipelineCaching.Tests\Hyperbee.Pipeline.Caching.Tests.csproj", "{B7E5FBB3-AF2A-4E48-8E6A-10887DC6C4C0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hyperbee.Pipeline.Caching.Tests", "test\Hyperbee.PipelineCaching.Tests\Hyperbee.Pipeline.Caching.Tests.csproj", "{B7E5FBB3-AF2A-4E48-8E6A-10887DC6C4C0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hyperbee.Pipeline.Auth", "src\Hyperbee.Pipeline.Auth\Hyperbee.Pipeline.Auth.csproj", "{85FBEEBD-0E57-4B54-83AF-6A501779CBD5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hyperbee.Pipeline.Auth.Tests", "test\Hyperbee.Pipeline.Auth.Tests\Hyperbee.Pipeline.Auth.Tests.csproj", "{3E5F6864-2BAD-4349-8C7A-D199A715FA3C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -67,6 +71,14 @@ Global {B7E5FBB3-AF2A-4E48-8E6A-10887DC6C4C0}.Debug|Any CPU.Build.0 = Debug|Any CPU {B7E5FBB3-AF2A-4E48-8E6A-10887DC6C4C0}.Release|Any CPU.ActiveCfg = Release|Any CPU {B7E5FBB3-AF2A-4E48-8E6A-10887DC6C4C0}.Release|Any CPU.Build.0 = Release|Any CPU + {85FBEEBD-0E57-4B54-83AF-6A501779CBD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85FBEEBD-0E57-4B54-83AF-6A501779CBD5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85FBEEBD-0E57-4B54-83AF-6A501779CBD5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85FBEEBD-0E57-4B54-83AF-6A501779CBD5}.Release|Any CPU.Build.0 = Release|Any CPU + {3E5F6864-2BAD-4349-8C7A-D199A715FA3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E5F6864-2BAD-4349-8C7A-D199A715FA3C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E5F6864-2BAD-4349-8C7A-D199A715FA3C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E5F6864-2BAD-4349-8C7A-D199A715FA3C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -77,6 +89,7 @@ Global {17DA1657-DF82-440F-B1F1-D888BFA9626B} = {F9B24CD9-E06B-4834-84CB-8C29E5F10BE0} {884A8242-351E-4363-9B34-E8C202CF7787} = {870D9301-BE3D-44EA-BF9C-FCC2E87FE4CD} {B7E5FBB3-AF2A-4E48-8E6A-10887DC6C4C0} = {F9B24CD9-E06B-4834-84CB-8C29E5F10BE0} + {3E5F6864-2BAD-4349-8C7A-D199A715FA3C} = {F9B24CD9-E06B-4834-84CB-8C29E5F10BE0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {32874F5B-B467-4F28-A8E2-82C2536FB228} diff --git a/src/Hyperbee.Pipeline.Auth/Hyperbee.Pipeline.Auth.csproj b/src/Hyperbee.Pipeline.Auth/Hyperbee.Pipeline.Auth.csproj new file mode 100644 index 0000000..aed76d6 --- /dev/null +++ b/src/Hyperbee.Pipeline.Auth/Hyperbee.Pipeline.Auth.csproj @@ -0,0 +1,42 @@ + + + + net8.0 + enable + true + + Stillpoint Software, Inc. + README.md + pipeline;auth + icon.png + https://github.com/Stillpoint-Software/Hyperbee.Pipeline/ + https://github.com/Stillpoint-Software/hyperbee.pipeline/releases/latest + net8.0 + LICENSE + Stillpoint Software, Inc. + Hyperbee Pipline Auth + Auth for Hyperbee.Pipelines async pipelines + https://github.com/Stillpoint-Software/Hyperbee.Pipeline + git + https://github.com/Stillpoint-Software/hyperbee.pipeline/releases/latest + + + + + + + True + \ + + + True + \ + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/src/Hyperbee.Pipeline.Auth/LICENSE b/src/Hyperbee.Pipeline.Auth/LICENSE new file mode 100644 index 0000000..5487bc2 --- /dev/null +++ b/src/Hyperbee.Pipeline.Auth/LICENSE @@ -0,0 +1,23 @@ +## LICENSE + +MIT License + +Copyright (c) 2024 Stillpoint Software, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/Hyperbee.Pipeline.Auth/PipelineAuthExtensions.cs b/src/Hyperbee.Pipeline.Auth/PipelineAuthExtensions.cs new file mode 100644 index 0000000..e8e3c75 --- /dev/null +++ b/src/Hyperbee.Pipeline.Auth/PipelineAuthExtensions.cs @@ -0,0 +1,68 @@ +using System.Security.Claims; +using Hyperbee.Pipeline.Context; +using Microsoft.Extensions.DependencyInjection; + +namespace Hyperbee.Pipeline.Auth; + +public static class PipelineAuthExtensions +{ + private const string ClaimsPrincipalKey = nameof( ClaimsPrincipalKey ); + + public static IPipelineBuilder PipeIfClaim( + this IPipelineBuilder builder, + Claim claim, + Func, IPipelineBuilder> childBuilder ) + { + return (IPipelineBuilder) builder + .PipeIf( ( context, arg ) => + { + var claimsPrincipal = context.GetClaimsPrincipal(); + + return claimsPrincipal.HasClaim( x => x.Type == claim.Type && x.Value == claim.Value ); + } + , childBuilder ); + + } + + public static IPipelineBuilder WithAuth( + this IPipelineStartBuilder builder, + Func validateClaims ) + { + return builder + .WithAuth() + .Call( ( context, argument ) => + { + var claimsPrincipal = context.GetClaimsPrincipal(); + + if ( !validateClaims( context, argument, claimsPrincipal ) ) + context.CancelAfter(); + + } ); + } + + public static IPipelineBuilder WithAuth( + this IPipelineStartBuilder builder ) + { + return builder.HookAsync( async ( context, argument, next ) => + { + context.GetClaimsPrincipal(); + + return await next( context, argument ); + + } ); + } + + public static ClaimsPrincipal GetClaimsPrincipal( this IPipelineContext context ) + { + if ( context.Items.TryGetValue( ClaimsPrincipalKey, out var claimsPrincipal ) ) + return claimsPrincipal; + + var claimsPrincipalAccessor = context.ServiceProvider.GetService(); + claimsPrincipal = claimsPrincipalAccessor.ClaimsPrincipal; + + context.Items.SetValue( ClaimsPrincipalKey, claimsPrincipal ); + return claimsPrincipal; + } + + +} diff --git a/src/Hyperbee.Pipeline.Auth/README.md b/src/Hyperbee.Pipeline.Auth/README.md new file mode 100644 index 0000000..4becc2d --- /dev/null +++ b/src/Hyperbee.Pipeline.Auth/README.md @@ -0,0 +1,48 @@ +# Hyperbee.Pipeline.Claims + +The `Hyperbee.Pipeline.Auth` library is a set of extentsions to `Hyperbee.Pipeline` that adds support for authorization within the pipeline. + + +## Examples + +```csharp +// Will return the claim if available +var command = PipelineFactory + .Start() + .PipeIfClaim( Claim ) + .Build(); + +``` + +```csharp +// WithAuth takes a function to validate the claim. +var command = PipelineFactory + .Start() + .WithAuth( ValidateClaim ) + .Build(); + +private async Task ValidateClaim( IPipelineContext context, string roleValue, ClaimsPrincipal claimsPrincipal ) + { + return claimsPrincipal.HasClaim( x => x.Value == roleValue ); + } +``` + +## Dependacy Injection + +```csharp +// Add httpContextAccessor if using web api + services.AddHttpContextAccessor(); + +// Add with the pipelines +services.AddClaimPrincipalAccessor(); +``` + +Or create your own claims principal use for the pipelines: + +```csharp +services.AddPipeline( (factoryServices, rootProvider) => +{ + factoryServices.AddClaimPrincipalAccessor( IClaimsPrincipal claimsPrincipal ) +} ); +``` + diff --git a/src/Hyperbee.Pipeline.Auth/ServiceCollectionExtensions.cs b/src/Hyperbee.Pipeline.Auth/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..aab3b79 --- /dev/null +++ b/src/Hyperbee.Pipeline.Auth/ServiceCollectionExtensions.cs @@ -0,0 +1,47 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Hyperbee.Pipeline.Auth; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddClaimPrincipalAccessor( + this IServiceCollection services, + T claimsPrincipalAccessor + ) where T : IClaimsPrincipalAccessor + { + return services.AddTransient( _ => claimsPrincipalAccessor ); + + } + + //For web API + public static IServiceCollection AddClaimPrincipalAccessor( + this IServiceCollection services + ) + { + return services.AddTransient(); + } +} + +public interface IClaimsPrincipalAccessor +{ + public ClaimsPrincipal ClaimsPrincipal { get; } +} + +public class HttpClaimsAccessor : IClaimsPrincipalAccessor +{ + private readonly IHttpContextAccessor _httpContextAccessor; + public HttpClaimsAccessor( IHttpContextAccessor httpContextAccessor ) + { + _httpContextAccessor = httpContextAccessor; + } + + public ClaimsPrincipal ClaimsPrincipal + { + get { return _httpContextAccessor.HttpContext.User; } + } +} + + + diff --git a/test/Hyperbee.Pipeline.Auth.Tests/Hyperbee.Pipeline.Auth.Tests.csproj b/test/Hyperbee.Pipeline.Auth.Tests/Hyperbee.Pipeline.Auth.Tests.csproj new file mode 100644 index 0000000..0e8ae94 --- /dev/null +++ b/test/Hyperbee.Pipeline.Auth.Tests/Hyperbee.Pipeline.Auth.Tests.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + enable + enable + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + diff --git a/test/Hyperbee.Pipeline.Auth.Tests/PipelineClaimsTests.cs b/test/Hyperbee.Pipeline.Auth.Tests/PipelineClaimsTests.cs new file mode 100644 index 0000000..89115ef --- /dev/null +++ b/test/Hyperbee.Pipeline.Auth.Tests/PipelineClaimsTests.cs @@ -0,0 +1,107 @@ +using System.ComponentModel.Design; +using System.Security.Claims; +using Hyperbee.Pipeline.Auth.Tests.TestSupport; +using Hyperbee.Pipeline.Context; +using Microsoft.Extensions.Logging; +using NSubstitute; + +namespace Hyperbee.Pipeline.Auth.Tests; + +[TestClass] +public class PipelineClaimsTests +{ + [TestMethod] + public async Task Should_return_claim() + { + var logger = Substitute.For(); + var factory = CreateContextFactory(); + + var command = PipelineFactory + .Start() + .PipeIfClaim( new Claim( "Role", "reader" ), b => b.Pipe( Complex ) ) + .Build(); + + var result = await command( factory.Create( logger ), "reader" ); + + Assert.AreEqual( 6, result ); + } + + [TestMethod] + public async Task Should_return_claim_with_auth() + { + var logger = Substitute.For(); + var factory = CreateContextFactory(); + + var command = PipelineFactory + .Start() + .WithAuth() + .Build(); + + var result = await command( factory.Create( logger ), "reader" ); + + Assert.AreEqual( "reader", result ); + } + + [TestMethod] + public async Task Should_withAuth_does_not_cancel() + { + var logger = Substitute.For(); + var factory = CreateContextFactory(); + + var command = PipelineFactory + .Start() + .WithAuth( ( context, argument, claimsPrincipal ) => + { + return claimsPrincipal.HasClaim( x => x.Value == argument ); + } ) + .Pipe( Complex ) + .Build(); + + var context = factory.Create( logger ); + var result = await command( context, "reader" ); + + Assert.AreEqual( 6, result ); + Assert.IsTrue( context.Success ); + } + + [TestMethod] + public async Task Should_withAuth_cancels_pipeline() + { + var logger = Substitute.For(); + var factory = CreateContextFactory(); + + var command = PipelineFactory + .Start() + .WithAuth( ( context, argument, claimsPrincipal ) => + { + return claimsPrincipal.HasClaim( x => x.Value == argument ); + } ) + .Pipe( Complex ) + .Build(); + + var context = factory.Create( logger ); + var result = await command( context, "test" ); + + Assert.AreEqual( 0, result ); + Assert.IsTrue( context.IsCanceled ); + } + + + private static int Complex( IPipelineContext context, string argument ) => argument.Length; + + private static IPipelineContextFactory CreateContextFactory() + { + var claimsPrincipal = ClaimsPrincipalFixture.Next(); + + var container = new ServiceContainer(); + + container.AddService( typeof( IClaimsPrincipalAccessor ), new TestClaimsPrincipalAccessor( claimsPrincipal ) ); + + return PipelineContextFactory.CreateFactory( container, true ); + } + + private class TestClaimsPrincipalAccessor( ClaimsPrincipal claimsPrincipal ) : IClaimsPrincipalAccessor + { + public ClaimsPrincipal ClaimsPrincipal => claimsPrincipal; + } +} diff --git a/test/Hyperbee.Pipeline.Auth.Tests/TestSupport/ClaimsPrincipalFixture.cs b/test/Hyperbee.Pipeline.Auth.Tests/TestSupport/ClaimsPrincipalFixture.cs new file mode 100644 index 0000000..e308387 --- /dev/null +++ b/test/Hyperbee.Pipeline.Auth.Tests/TestSupport/ClaimsPrincipalFixture.cs @@ -0,0 +1,15 @@ +using System.Security.Claims; + +namespace Hyperbee.Pipeline.Auth.Tests.TestSupport; + +public static class ClaimsPrincipalFixture +{ + public static ClaimsPrincipal Next( ClaimsPrincipal? principal = null ) + { + ClaimsIdentity claimsIdentity = new ClaimsIdentity(); + claimsIdentity.AddClaim( new Claim( "Name", "test@comcast.net" ) ); + claimsIdentity.AddClaim( new Claim( "Role", "reader" ) ); + + return new ClaimsPrincipal( new ClaimsIdentity( claimsIdentity ) ); + } +} diff --git a/test/Hyperbee.PipelineCaching.Tests/Hyperbee.Pipeline.Caching.Tests.csproj b/test/Hyperbee.PipelineCaching.Tests/Hyperbee.Pipeline.Caching.Tests.csproj index b559bfd..84d1ea7 100644 --- a/test/Hyperbee.PipelineCaching.Tests/Hyperbee.Pipeline.Caching.Tests.csproj +++ b/test/Hyperbee.PipelineCaching.Tests/Hyperbee.Pipeline.Caching.Tests.csproj @@ -10,7 +10,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -27,4 +30,11 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + +