Skip to content

Commit

Permalink
Added Security package
Browse files Browse the repository at this point in the history
(%release-note:

- Added DfE.CoreLibs.Security, including Authorization extension which faciliates creation of Policies, Roles and Claims

%)
  • Loading branch information
Farshad DASHTI authored and Farshad DASHTI committed Nov 6, 2024
1 parent 904e6c0 commit cebb793
Show file tree
Hide file tree
Showing 15 changed files with 761 additions and 0 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/build-test-security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Build DfE.CoreLibs.Security

on:
push:
branches:
- main
paths:
- 'src/DfE.CoreLibs.Security/**'
pull_request:
branches:
- main
paths:
- 'src/DfE.CoreLibs.Security/**'

jobs:
build-and-test:
uses: ./.github/workflows/build-test-template.yml
with:
project_name: DfE.CoreLibs.Security
project_path: src/DfE.CoreLibs.Security
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
16 changes: 16 additions & 0 deletions .github/workflows/pack-security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Pack DfE.CoreLibs.Security

on:
workflow_run:
workflows: ["Build DfE.CoreLibs.Security"]
types:
- completed

jobs:
build-and-package:
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main' && github.event.workflow_run.event != 'pull_request' }}
uses: ./.github/workflows/nuget-package-template.yml
with:
project_name: DfE.CoreLibs.Security
project_path: src/DfE.CoreLibs.Security
nuget_package_name: DfE.CoreLibs.Security
13 changes: 13 additions & 0 deletions DfE.CoreLibs.sln
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DfE.CoreLibs.Caching.Tests"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DfE.CoreLibs.AsyncProcessing.Tests", "src\Tests\DfE.CoreLibs.AsyncProcessing.Tests\DfE.CoreLibs.AsyncProcessing.Tests.csproj", "{5ABF8802-0C35-42D3-B2BB-83BD7159124F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DfE.CoreLibs.Security", "src\DfE.CoreLibs.Security\DfE.CoreLibs.Security.csproj", "{374F44BE-DAC1-4974-BCA8-06ADB72C9BF0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DfE.CoreLibs.Security.Tests", "src\Tests\DfE.CoreLibs.Security.Tests\DfE.CoreLibs.Security.Tests.csproj", "{45F6BF60-A2AC-4084-AB62-1CAD172E95AE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -71,6 +75,14 @@ Global
{5ABF8802-0C35-42D3-B2BB-83BD7159124F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5ABF8802-0C35-42D3-B2BB-83BD7159124F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5ABF8802-0C35-42D3-B2BB-83BD7159124F}.Release|Any CPU.Build.0 = Release|Any CPU
{374F44BE-DAC1-4974-BCA8-06ADB72C9BF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{374F44BE-DAC1-4974-BCA8-06ADB72C9BF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{374F44BE-DAC1-4974-BCA8-06ADB72C9BF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{374F44BE-DAC1-4974-BCA8-06ADB72C9BF0}.Release|Any CPU.Build.0 = Release|Any CPU
{45F6BF60-A2AC-4084-AB62-1CAD172E95AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{45F6BF60-A2AC-4084-AB62-1CAD172E95AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{45F6BF60-A2AC-4084-AB62-1CAD172E95AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{45F6BF60-A2AC-4084-AB62-1CAD172E95AE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -80,6 +92,7 @@ Global
{69529D73-DD34-43A2-9D06-F3783F68F05C} = {3F89DCAD-8EC7-41ED-A08F-A9EFAE263EB4}
{807147EB-9B76-42F6-B249-A0F0CF3C3462} = {3F89DCAD-8EC7-41ED-A08F-A9EFAE263EB4}
{5ABF8802-0C35-42D3-B2BB-83BD7159124F} = {3F89DCAD-8EC7-41ED-A08F-A9EFAE263EB4}
{45F6BF60-A2AC-4084-AB62-1CAD172E95AE} = {3F89DCAD-8EC7-41ED-A08F-A9EFAE263EB4}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {01D11FBC-6C66-43E4-8F1F-46B105EDD95C}
Expand Down
123 changes: 123 additions & 0 deletions src/DfE.CoreLibs.Security/Authorization/AuthorizationExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using DfE.CoreLibs.Security.Configurations;
using DfE.CoreLibs.Security.Interfaces;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace DfE.CoreLibs.Security.Authorization
{
/// <summary>
/// Extension methods for configuring application authorization and custom claim providers.
/// </summary>
public static class AuthorizationExtensions
{
/// <summary>
/// Configures authorization policies based on roles, claims, and custom requirements.
/// Loads policies from the provided configuration.
/// </summary>
/// <param name="services">The service collection to add authorization services to.</param>
/// <param name="configuration">The configuration object containing policy definitions.</param>
/// <param name="policyCustomizations">The the customizations such as Requirements which can be added to the policy after it is created.</param>
/// <returns>The modified service collection.</returns>
public static IServiceCollection AddApplicationAuthorization(

Check warning on line 23 in src/DfE.CoreLibs.Security/Authorization/AuthorizationExtensions.cs

View workflow job for this annotation

GitHub Actions / build-and-test / build-and-test

Refactor this method to reduce its Cognitive Complexity from 34 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 23 in src/DfE.CoreLibs.Security/Authorization/AuthorizationExtensions.cs

View workflow job for this annotation

GitHub Actions / build-and-test / build-and-test

Refactor this method to reduce its Cognitive Complexity from 34 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
this IServiceCollection services,
IConfiguration configuration,
Dictionary<string, Action<AuthorizationPolicyBuilder>>? policyCustomizations = null)
{
services.AddAuthorization(options =>
{
var policies = configuration.GetSection("Authorization:Policies").Get<List<PolicyDefinition>>();

foreach (var policyConfig in policies ?? [])
{
options.AddPolicy(policyConfig.Name, policyBuilder =>
{
policyBuilder.RequireAuthenticatedUser();

if (policyConfig.Roles.Any())
{
if (string.Equals(policyConfig.Operator, "AND", StringComparison.OrdinalIgnoreCase))
{
// Use AND logic: require each role individually
foreach (var role in policyConfig.Roles)
{
policyBuilder.RequireRole(role);
}
}
else
{
// Use OR logic: require any of the roles
policyBuilder.RequireRole(policyConfig.Roles.ToArray());
}
}

if (policyConfig.Claims != null && policyConfig.Claims.Any())
{
foreach (var claim in policyConfig.Claims)
{
policyBuilder.RequireClaim(claim.Type, claim.Values.ToArray());
}
}
});
}

if (policyCustomizations != null)
{
foreach (var (policyName, customization) in policyCustomizations)
{
if (options.GetPolicy(policyName) is not null)
{
// If the policy already exists, modify it
UpdateExistingPolicy(options, policyName, customization);
}
else
{
// If the policy does not exist, create a new one
options.AddPolicy(policyName, customization);
}
}
}
});

services.AddTransient<IClaimsTransformation, CustomClaimsTransformation>();

return services;
}

/// <summary>
/// Registers a custom claim provider to retrieve claims dynamically.
/// </summary>
/// <typeparam name="TProvider">The custom claim provider implementing ICustomClaimProvider.</typeparam>
/// <param name="services">The service collection to add the claim provider to.</param>
/// <returns>The modified service collection.</returns>
public static IServiceCollection AddCustomClaimProvider<TProvider>(this IServiceCollection services)
where TProvider : class, ICustomClaimProvider
{
services.AddTransient<ICustomClaimProvider, TProvider>();
return services;
}

/// <summary>
/// Updates an existing policy with additional requirements from a customization action.
/// </summary>
private static void UpdateExistingPolicy(AuthorizationOptions options, string policyName, Action<AuthorizationPolicyBuilder> customization)
{
var existingPolicyBuilder = new AuthorizationPolicyBuilder();

// Copy existing policy requirements
var existingPolicy = options.GetPolicy(policyName)!;
foreach (var requirement in existingPolicy.Requirements)
{
existingPolicyBuilder.Requirements.Add(requirement);
}

// Apply the new customization
customization(existingPolicyBuilder);

// Replace the policy with the updated one
options.AddPolicy(policyName, existingPolicyBuilder.Build());
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using DfE.CoreLibs.Security.Interfaces;
using Microsoft.AspNetCore.Authentication;
using System.Security.Claims;

namespace DfE.CoreLibs.Security.Authorization
{
/// <summary>
/// Transforms the claims of the current user by adding custom claims from registered claim providers.
/// </summary>
public class CustomClaimsTransformation(IEnumerable<ICustomClaimProvider> claimProviders) : IClaimsTransformation
{
/// <summary>
/// Transforms the user's claims by adding custom claims retrieved from each registered claim provider.
/// </summary>
/// <param name="principal">The current user's ClaimsPrincipal.</param>
/// <returns>The modified ClaimsPrincipal with additional claims.</returns>
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var identity = (ClaimsIdentity)principal.Identity!;

foreach (var provider in claimProviders)
{
var claims = await provider.GetClaimsAsync(principal);

foreach (var claim in claims)
{
if (!identity.HasClaim(c => c.Type == claim.Type))
{
identity.AddClaim(claim);
}
}
}

return principal;
}
}
}
8 changes: 8 additions & 0 deletions src/DfE.CoreLibs.Security/Configurations/ClaimDefinition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace DfE.DomainDrivenDesignTemplate.Infrastructure.Security.Configurations
{
public class ClaimDefinition
{
public required string Type { get; set; }
public required List<string> Values { get; set; }
}
}
15 changes: 15 additions & 0 deletions src/DfE.CoreLibs.Security/Configurations/PolicyDefinition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using DfE.DomainDrivenDesignTemplate.Infrastructure.Security.Configurations;

namespace DfE.CoreLibs.Security.Configurations
{
/// <summary>
/// Represents a policy definition, including roles, claims, and custom requirements.
/// </summary>
public class PolicyDefinition
{
public required string Name { get; set; }
public required string Operator { get; set; } = "OR"; // "AND" or "OR"
public required List<string> Roles { get; set; }
public List<ClaimDefinition>? Claims { get; set; }
}
}
30 changes: 30 additions & 0 deletions src/DfE.CoreLibs.Security/DfE.CoreLibs.Security.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageReadmeFile>readme.md</PackageReadmeFile>
<Title>DfE.CoreLibs.Security</Title>
<Description>A library providing flexible foundation for managing security in .NET projects, including role-based and claim-based policies, custom requirements, and dynamic claims. It enables consistent, configurable security across applications.
</Description>
<Authors>DFE-Digital</Authors>
</PropertyGroup>

<ItemGroup>
<None Include="readme.md" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.10" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Identity.Web" Version="3.3.0" />
</ItemGroup>

<ItemGroup>
<None Update="readme.md">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Authorization;

namespace DfE.CoreLibs.Security.Interfaces
{
/// <summary>
/// Represents a custom authorization requirement with a unique type.
/// </summary>
public interface ICustomAuthorizationRequirement : IAuthorizationRequirement
{
string Type { get; }
}
}
17 changes: 17 additions & 0 deletions src/DfE.CoreLibs.Security/Interfaces/ICustomClaimProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Security.Claims;

namespace DfE.CoreLibs.Security.Interfaces
{
/// <summary>
/// Interface for custom claim providers that retrieve claims for a user.
/// </summary>
public interface ICustomClaimProvider
{
/// <summary>
/// Asynchronously retrieves claims for the specified user.
/// </summary>
/// <param name="principal">The current user's ClaimsPrincipal.</param>
/// <returns>A list of claims to add to the user's identity.</returns>
Task<IEnumerable<Claim>> GetClaimsAsync(ClaimsPrincipal principal);
}
}
Loading

0 comments on commit cebb793

Please sign in to comment.