Skip to content

Commit

Permalink
feat: add custom claim enricher
Browse files Browse the repository at this point in the history
  • Loading branch information
loekensgard committed Nov 22, 2024
1 parent 376966d commit 7eba85f
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 3 deletions.
41 changes: 41 additions & 0 deletions src/Serilog.Enrichers.AzureClaims/Enrichers/CustomClaimEnricher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Http;
using System.Security.Claims;

namespace Serilog.Enrichers.AzureClaims;

/// <summary>
/// Enriches log events with a custom property from the user's claims.
/// </summary>
internal class CustomClaimEnricher : BaseEnricher
{
private readonly string _customClaimType;

/// <summary>
/// Initializes a new instance of the <see cref="CustomClaimEnricher"/> class.
/// </summary>
public CustomClaimEnricher(string claimType, string? customPropertyName = null) : base($"Serilog_{customPropertyName ?? claimType}", customPropertyName ?? claimType)
{
_customClaimType = claimType;
}

/// <summary>
/// Initializes a new instance of the <see cref="CustomClaimEnricher"/> class with the specified HTTP context accessor.
/// </summary>
/// <param name="contextAccessor">The HTTP context accessor to use for retrieving the user's claims.</param>
/// <param name="claimType">The custom claimType to be used to find the claim</param>
/// <param name="customPropertyName">The custom property name to be used in the enriched logs.</param>
internal CustomClaimEnricher(IHttpContextAccessor contextAccessor, string claimType, string? customPropertyName = null) : base(contextAccessor, $"Serilog_{customPropertyName ?? claimType}", customPropertyName ?? claimType)
{
_customClaimType = claimType;
}

/// <summary>
/// Gets the Custom Claim property value from the specified claims principal.
/// </summary>
/// <param name="user">The claims principal representing the user.</param>
/// <returns>The Custom Claim property value, or <c>null</c> if it cannot be found.</returns>
protected override string? GetPropertyValue(ClaimsPrincipal user)
{
return user.FindFirstValue(_customClaimType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,28 @@ public static LoggerConfiguration WithAppId(this LoggerEnrichmentConfiguration e

return enrichmentConfiguration.With<AppIdEnricher>();
}

/// <summary>
/// Adds a custom claim enrichment to the logger configuration.
/// </summary>
/// <param name="enrichmentConfiguration">The logger enrichment configuration.</param>
/// <param name="claimType">The type of the claim to be used for enrichment.</param>
/// <param name="propertyName">The name visible in the enriched logs</param>
/// <returns>The logger configuration with the custom claim enrichment added.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="enrichmentConfiguration"/> or <paramref name="claimType"/> is null.</exception>
public static LoggerConfiguration WithCustomClaim(this LoggerEnrichmentConfiguration enrichmentConfiguration, string claimType, string? propertyName = null)
{
ArgumentNullException.ThrowIfNull(enrichmentConfiguration, nameof(enrichmentConfiguration));

#if (NET6_0)
if (string.IsNullOrWhiteSpace(claimType))
{
throw new ArgumentNullException(nameof(claimType));
}
#else
ArgumentNullException.ThrowIfNullOrEmpty(claimType, nameof(claimType));
#endif

return enrichmentConfiguration.With(new CustomClaimEnricher(claimType, propertyName));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using Microsoft.AspNetCore.Http;
using NSubstitute;
using Serilog.Enrichers.AzureClaims.Tests.Helpers;
using Serilog.Events;
using System.Security.Claims;
using Xunit;

namespace Serilog.Enrichers.AzureClaims.Tests
{
public class CustomClaimEnricherTests
{
private const string _customClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress";
private const string _customClaimPropertyName = "Email";

[Fact]
public void LogEvent_DoesNotContainCustomClaimWhenUserIsNotLoggedIn()
{
// Arrange
var httpContextAccessorMock = Substitute.For<IHttpContextAccessor>();
httpContextAccessorMock.HttpContext.Returns(new DefaultHttpContext());

var CustomClaimEnricher = new CustomClaimEnricher(httpContextAccessorMock, _customClaimType, _customClaimPropertyName);

LogEvent evt = null;
var log = new LoggerConfiguration()
.Enrich.With(CustomClaimEnricher)
.WriteTo.Sink(new DelegatingSink(e => evt = e))
.CreateLogger();

// Act
log.Information(@"Email property is not set when the user is not logged in");

// Assert
Assert.NotNull(evt);
Assert.False(evt.Properties.ContainsKey("Email"));
}

[Fact]
public void LogEvent_ContainsUnknownCustomClaimWhenUserIsLoggedInButCustomClaimIsNotFound()
{
// Arrange
var httpContextAccessorMock = Substitute.For<IHttpContextAccessor>();
var user = new ClaimsPrincipal(TestClaimsProvider.NotValidClaims().GetClaimsPrincipal());

httpContextAccessorMock.HttpContext.Returns(new DefaultHttpContext
{
User = user
});

var CustomClaimEnricher = new CustomClaimEnricher(httpContextAccessorMock, _customClaimType, _customClaimPropertyName);

LogEvent evt = null;
var log = new LoggerConfiguration()
.Enrich.With(CustomClaimEnricher)
.WriteTo.Sink(new DelegatingSink(e => evt = e))
.CreateLogger();

// Act
log.Information(@"Email property is set to unknown when the user is logged in");

// Assert
Assert.NotNull(evt);
Assert.True(evt.Properties.ContainsKey("Email"));
Assert.Equal("unknown", evt.Properties["Email"].LiteralValue().ToString());
}

[Fact]
public void LogEvent_ContainCustomClaimWhenUserIsLoggedIn()
{
// Arrange
var httpContextAccessorMock = Substitute.For<IHttpContextAccessor>();
var user = new ClaimsPrincipal(TestClaimsProvider.ValidClaims().GetClaimsPrincipal());

httpContextAccessorMock.HttpContext.Returns(new DefaultHttpContext
{
User = user
});

var CustomClaimEnricher = new CustomClaimEnricher(httpContextAccessorMock, _customClaimType, _customClaimPropertyName);

LogEvent evt = null;
var log = new LoggerConfiguration()
.Enrich.With(CustomClaimEnricher)
.WriteTo.Sink(new DelegatingSink(e => evt = e))
.CreateLogger();

// Act
log.Information(@"Email property is set when the user is logged in");

// Assert
Assert.NotNull(evt);
Assert.True(evt.Properties.ContainsKey("Email"));
Assert.Equal(TestConstants.EMAIL, evt.Properties["Email"].LiteralValue().ToString());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public void AllEnrichers_AreAddedToTheBuilder()
.Enrich.WithTenantId()
.Enrich.WithObjectId()
.Enrich.WithDisplayName()
.Enrich.WithCustomClaim("test")
.WriteTo.Sink(new DelegatingSink(e => evt = e))
.CreateLogger();

Expand All @@ -43,7 +44,8 @@ public void AllEnrichers_AreAddedToTheBuilder()
item => Assert.Equal(nameof(AppIdEnricher), item.GetType().Name),
item => Assert.Equal(nameof(TenantIdEnricher), item.GetType().Name),
item => Assert.Equal(nameof(ObjectIdEnricher), item.GetType().Name),
item => Assert.Equal(nameof(DisplayNameEnricher), item.GetType().Name));
item => Assert.Equal(nameof(DisplayNameEnricher), item.GetType().Name),
item => Assert.Equal(nameof(CustomClaimEnricher), item.GetType().Name));
}

// Method to count the number of enrichers in the project. Ignores the base enricher
Expand Down Expand Up @@ -167,5 +169,27 @@ public void OIDEnricher_IsAddedToTheBuilder()

Assert.Equal(nameof(ObjectIdEnricher), enricher.Name);
}

[Fact]
public void CustomClaimEnricher_IsAddedToTheBuilder()
{
// Arrange
var httpContextAccessorMock = Substitute.For<IHttpContextAccessor>();
httpContextAccessorMock.HttpContext.Returns(new DefaultHttpContext());

LogEvent evt = null;
var log = new LoggerConfiguration()
.Enrich.WithCustomClaim("test")
.WriteTo.Sink(new DelegatingSink(e => evt = e))
.CreateLogger();

var aggregateEnricherFieldInfo = log.GetType()
.GetField("_enricher", BindingFlags.Instance | BindingFlags.NonPublic);

var aggregateEnricher = aggregateEnricherFieldInfo?.GetValue(log);
var enricher = aggregateEnricher.GetType();

Assert.Equal(nameof(CustomClaimEnricher), enricher.Name);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Serilog.Enrichers.AzureClaims.Tests.Helpers
{
public class TestClaimsProvider
{
private List<Claim> Claims { get; } = new List<Claim>();
private List<Claim> Claims { get; } = [];

public ClaimsPrincipal GetClaimsPrincipal()
{
Expand All @@ -30,7 +30,8 @@ public static TestClaimsProvider ValidClaims()
.AddClaim(ClaimTypes.Name, TestConstants.NAME)
.AddClaim(ClaimConstants.ObjectId, TestConstants.OID)
.AddClaim(ClaimTypes.Upn, TestConstants.UPN)
.AddClaim(ClaimConstants.Tid, TestConstants.TENANTID);
.AddClaim(ClaimConstants.Tid, TestConstants.TENANTID)
.AddClaim(ClaimTypes.Email, TestConstants.EMAIL);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public static class TestConstants
public static string OID = "0f1eddfd-cec1-4444-88f3-07476b7e69ee";
public static string UPN = "[email protected]";
public static string TENANTID = "79ce530d-c819-40ed-9a25-eebabd34edfe";
public static string EMAIL = "[email protected]";
#pragma warning restore CA2211 // Non-constant fields should not be visible
}
}

0 comments on commit 7eba85f

Please sign in to comment.