Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verbose rendering for .NET Claim class #982

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 198 additions & 4 deletions src/Shared/LayoutRenderers/AspNetUserClaimLayoutRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
using NLog.Common;
using NLog.Config;
using NLog.LayoutRenderers;
using NLog.Layouts;
using NLog.Web.Enums;

namespace NLog.Web.LayoutRenderers
{
Expand All @@ -28,11 +30,16 @@ public class AspNetUserClaimLayoutRenderer : AspNetLayoutMultiValueRendererBase
/// </summary>
/// <remarks>
/// When value is prefixed with "ClaimTypes." (Remember dot) then ít will lookup in well-known claim types from <see cref="ClaimTypes"/>. Ex. ClaimsTypes.Name
/// If this is null or empty then all claim types are rendered
/// If this is null or empty then the Type and Value properties of all claim types are rendered
/// </remarks>
[DefaultParameter]
public string ClaimType { get; set; }

/// <summary>
/// If this is true, then all string properties of the <see cref="Claim"/> are rendered as well the values in its Properties property.
/// </summary>
public bool Verbose { get; set; }

/// <inheritdoc />
protected override void InitializeLayoutRenderer()
{
Expand Down Expand Up @@ -64,15 +71,34 @@ protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent)

if (string.IsNullOrEmpty(ClaimType))
{
var allClaims = GetAllClaims(claimsPrincipal);
SerializePairs(allClaims, builder, logEvent);
if (Verbose)
{
#if NET46
SerializeVerbose((claimsPrincipal as ClaimsPrincipal)?.Claims, builder, logEvent);
#else
SerializeVerbose(claimsPrincipal.Claims, builder, logEvent);
#endif
}
else
{
var allClaims = GetAllClaims(claimsPrincipal);
SerializePairs(allClaims, builder, logEvent);
}
}
else
{
var claim = GetClaim(claimsPrincipal, ClaimType);
if (claim != null)
{
builder.Append(claim?.Value);
if (Verbose)
{
SerializeVerbose(new List<Claim> {claim}, builder, logEvent);
}
else
{
builder.Append(claim.Value);

}
}
}
}
Expand All @@ -82,6 +108,174 @@ protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent)
}
}

private void SerializeVerbose(IEnumerable<Claim> claims, StringBuilder builder, LogEventInfo logEvent)
{
if (claims == null)
{
return;
}

switch (OutputFormat)
{
case AspNetRequestLayoutOutputFormat.Flat:
SerializeVerboseFlat(claims, builder, logEvent);
break;
case AspNetRequestLayoutOutputFormat.JsonArray:
case AspNetRequestLayoutOutputFormat.JsonDictionary:
SerializeVerboseJson(claims, builder, logEvent);
break;
}
}

private void SerializeVerboseJson(IEnumerable<Claim> claims, StringBuilder builder, LogEventInfo logEvent)
{
var firstItem = true;
var includeSeparator = false;

foreach (var claim in claims)
{
if (firstItem)
{
if (OutputFormat == AspNetRequestLayoutOutputFormat.JsonDictionary)
{
builder.Append('{');
}
else
{
builder.Append('[');
}
}
else
{
builder.Append(',');
}

builder.Append('{');

includeSeparator |= AppendJsonProperty(builder, nameof(claim.Type), claim.Type, false);
includeSeparator |= AppendJsonProperty(builder, nameof(claim.Value), claim.Value, includeSeparator);
includeSeparator |= AppendJsonProperty(builder, nameof(claim.ValueType), claim.ValueType, includeSeparator);

includeSeparator |= AppendJsonProperty(builder, nameof(claim.Issuer), claim.Issuer, includeSeparator);
includeSeparator |= AppendJsonProperty(builder, nameof(claim.OriginalIssuer), claim.OriginalIssuer, includeSeparator);

if (claim.Properties != null && claim.Properties.Count > 0)
{
builder.Append(",\"");
builder.Append(nameof(claim.Properties));
builder.Append("\":");
SerializePairs(claim.Properties.OrderBy(entry => entry.Key).ToList(), builder, logEvent);
}

builder.Append('}');

firstItem = false;
}

if (!firstItem)
{
if (OutputFormat == AspNetRequestLayoutOutputFormat.JsonDictionary)
{
builder.Append('}');
}
else
{
builder.Append(']');
}
}
}

private void SerializeVerboseFlat(IEnumerable<Claim> claims, StringBuilder builder, LogEventInfo logEvent)
{
var propertySeparator = GetRenderedItemSeparator(logEvent);
var valueSeparator = GetRenderedValueSeparator(logEvent);
var objectSeparator = GetRenderedObjectSeparator(logEvent);

var firstObject = true;
var includeSeparator = false;

foreach (var claim in claims)
{
if (!firstObject)
{
builder.Append(objectSeparator);
}

firstObject = false;

includeSeparator |= AppendFlatProperty(builder, nameof(claim.Type), claim.Type, valueSeparator, "");
includeSeparator |= AppendFlatProperty(builder, nameof(claim.Value), claim.Value, valueSeparator, includeSeparator ? propertySeparator : "");
includeSeparator |= AppendFlatProperty(builder, nameof(claim.ValueType), claim.ValueType, valueSeparator, includeSeparator ? propertySeparator : "");

includeSeparator |= AppendFlatProperty(builder, nameof(claim.Issuer), claim.Issuer, valueSeparator, includeSeparator ? propertySeparator : "");
includeSeparator |= AppendFlatProperty(builder, nameof(claim.OriginalIssuer), claim.OriginalIssuer, valueSeparator, includeSeparator ? propertySeparator : "");

if (claim.Properties != null && claim.Properties.Count > 0)
{
builder.Append(propertySeparator);
builder.Append("Properties[");
SerializePairs(claim.Properties.OrderBy(entry => entry.Key).ToList(), builder, logEvent);
builder.Append(']');
}
}
}

/// <summary>
/// Separator between objects, like cookies. Only used for <see cref="AspNetRequestLayoutOutputFormat.Flat" />
/// </summary>
/// <remarks>Render with <see cref="GetRenderedObjectSeparator" /></remarks>
public string ObjectSeparator { get => _objectSeparatorLayout?.OriginalText; set => _objectSeparatorLayout = new SimpleLayout(value ?? ""); }
private SimpleLayout _objectSeparatorLayout = new SimpleLayout(";");

/// <summary>
/// Get the rendered <see cref="ObjectSeparator" />
/// </summary>
private string GetRenderedObjectSeparator(LogEventInfo logEvent)
{
return logEvent != null ? _objectSeparatorLayout.Render(logEvent) : ObjectSeparator;
}

/// <summary>
/// Append the quoted name and value separated by a colon
/// </summary>
private static bool AppendJsonProperty(StringBuilder builder, string name, string value, bool includePropertySeparator)
{
if (!string.IsNullOrEmpty(value))
{
if (includePropertySeparator)
{
builder.Append(',');
}
AppendQuoted(builder, name);
builder.Append(':');
AppendQuoted(builder, value);
return true;
}
return false;
}

/// <summary>
/// Append the quoted name and value separated by a value separator
/// and ended by item separator
/// </summary>
private static bool AppendFlatProperty(
StringBuilder builder,
string name,
string value,
string valueSeparator,
string itemSeparator)
{
if (!string.IsNullOrEmpty(value))
{
builder.Append(itemSeparator);
builder.Append(name);
builder.Append(valueSeparator);
builder.Append(value);
return true;
}
return false;
}

#if NET46
private static IEnumerable<KeyValuePair<string, string>> GetAllClaims(System.Security.Principal.IPrincipal claimsPrincipal)
{
Expand Down
107 changes: 107 additions & 0 deletions tests/Shared/LayoutRenderers/AspNetUserClaimLayoutRendererTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
using System.Collections.Generic;
using System.Security.Claims;
using System.Security.Principal;
using NLog.Web.Enums;
#if ASP_NET_CORE
using Microsoft.Extensions.Primitives;
using HttpContextBase = Microsoft.AspNetCore.Http.HttpContext;
#endif
using NLog.Web.LayoutRenderers;
using NSubstitute;
using Xunit;
using static System.Net.WebRequestMethods;

namespace NLog.Web.Tests.LayoutRenderers
{
Expand Down Expand Up @@ -96,6 +98,111 @@ public void AllRendersAllValue()
// Assert
Assert.Equal(expectedResult, result);
}

[Fact]
public void VerboseMultipleFlatTest()
{
// Arrange
var (renderer, httpContext) = CreateWithHttpContext();
renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Flat;
renderer.Verbose = true;

var expectedResult =
"Type=http://schemas.xmlsoap.org/ws/2009/09/identity/claims/actor,Value=Actorvalue,ValueType=Actorstring,Issuer=Actorissuer,OriginalIssuer=ActororiginalIssuer,Properties[claim1property1=claim1value1,claim1property2=claim1value2];Type=http://schemas.xmlsoap.org/ws/2005/05/identity/claims/anonymous,Value=Anonymousvalue,ValueType=Anonymousstring,Issuer=Anonymousissuer,OriginalIssuer=AnonymousoriginalIssuer;Type=http://schemas.xmlsoap.org/ws/2005/05/identity/claims/authentication,Value=Authenticationvalue,ValueType=Authenticationstring,Issuer=Authenticationissuer,OriginalIssuer=AuthenticationoriginalIssuer";

var principal = Substitute.For<ClaimsPrincipal>();

var claim1 = new Claim(ClaimTypes.Actor, "Actorvalue", "Actorstring", "Actorissuer", "ActororiginalIssuer");
var claim2 = new Claim(ClaimTypes.Anonymous, "Anonymousvalue", "Anonymousstring", "Anonymousissuer", "AnonymousoriginalIssuer");
var claim3 = new Claim(ClaimTypes.Authentication, "Authenticationvalue", "Authenticationstring", "Authenticationissuer", "AuthenticationoriginalIssuer");

claim1.Properties.Add("claim1property1","claim1value1");
claim1.Properties.Add("claim1property2", "claim1value2");

principal.Claims.Returns(new List<Claim>()
{
claim1, claim2, claim3
}
);

httpContext.User.Returns(principal);

// Act
string result = renderer.Render(new LogEventInfo());

// Assert
Assert.Equal(expectedResult, result);
}

[Fact]
public void VerboseMultipleJsonArrayTest()
{
// Arrange
var (renderer, httpContext) = CreateWithHttpContext();
renderer.OutputFormat = AspNetRequestLayoutOutputFormat.JsonArray;
renderer.Verbose = true;

var expectedResult =
"[{\"Type\":\"http://schemas.xmlsoap.org/ws/2009/09/identity/claims/actor\",\"Value\":\"Actorvalue\",\"ValueType\":\"Actorstring\",\"Issuer\":\"Actorissuer\",\"OriginalIssuer\":\"ActororiginalIssuer\",\"Properties\":[{\"claim1property1\":\"claim1value1\"},{\"claim1property2\":\"claim1value2\"}]},{\"Type\":\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/anonymous\",\"Value\":\"Anonymousvalue\",\"ValueType\":\"Anonymousstring\",\"Issuer\":\"Anonymousissuer\",\"OriginalIssuer\":\"AnonymousoriginalIssuer\"},{\"Type\":\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/authentication\",\"Value\":\"Authenticationvalue\",\"ValueType\":\"Authenticationstring\",\"Issuer\":\"Authenticationissuer\",\"OriginalIssuer\":\"AuthenticationoriginalIssuer\"}]";

var principal = Substitute.For<ClaimsPrincipal>();

var claim1 = new Claim(ClaimTypes.Actor, "Actorvalue", "Actorstring", "Actorissuer", "ActororiginalIssuer");
var claim2 = new Claim(ClaimTypes.Anonymous, "Anonymousvalue", "Anonymousstring", "Anonymousissuer", "AnonymousoriginalIssuer");
var claim3 = new Claim(ClaimTypes.Authentication, "Authenticationvalue", "Authenticationstring", "Authenticationissuer", "AuthenticationoriginalIssuer");

claim1.Properties.Add("claim1property1", "claim1value1");
claim1.Properties.Add("claim1property2", "claim1value2");

principal.Claims.Returns(new List<Claim>()
{
claim1, claim2, claim3
}
);

httpContext.User.Returns(principal);

// Act
string result = renderer.Render(new LogEventInfo());

// Assert
Assert.Equal(expectedResult, result);
}

[Fact]
public void VerboseMultipleJsonDictionaryTest()
{
// Arrange
var (renderer, httpContext) = CreateWithHttpContext();
renderer.OutputFormat = AspNetRequestLayoutOutputFormat.JsonDictionary;
renderer.Verbose = true;

var expectedResult =
"{{\"Type\":\"http://schemas.xmlsoap.org/ws/2009/09/identity/claims/actor\",\"Value\":\"Actorvalue\",\"ValueType\":\"Actorstring\",\"Issuer\":\"Actorissuer\",\"OriginalIssuer\":\"ActororiginalIssuer\",\"Properties\":{\"claim1property1\":\"claim1value1\",\"claim1property2\":\"claim1value2\"}},{\"Type\":\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/anonymous\",\"Value\":\"Anonymousvalue\",\"ValueType\":\"Anonymousstring\",\"Issuer\":\"Anonymousissuer\",\"OriginalIssuer\":\"AnonymousoriginalIssuer\"},{\"Type\":\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/authentication\",\"Value\":\"Authenticationvalue\",\"ValueType\":\"Authenticationstring\",\"Issuer\":\"Authenticationissuer\",\"OriginalIssuer\":\"AuthenticationoriginalIssuer\"}}";

var principal = Substitute.For<ClaimsPrincipal>();

var claim1 = new Claim(ClaimTypes.Actor, "Actorvalue", "Actorstring", "Actorissuer", "ActororiginalIssuer");
var claim2 = new Claim(ClaimTypes.Anonymous, "Anonymousvalue", "Anonymousstring", "Anonymousissuer", "AnonymousoriginalIssuer");
var claim3 = new Claim(ClaimTypes.Authentication, "Authenticationvalue", "Authenticationstring", "Authenticationissuer", "AuthenticationoriginalIssuer");

claim1.Properties.Add("claim1property1", "claim1value1");
claim1.Properties.Add("claim1property2", "claim1value2");

principal.Claims.Returns(new List<Claim>()
{
claim1, claim2, claim3
}
);

httpContext.User.Returns(principal);

// Act
string result = renderer.Render(new LogEventInfo());

// Assert
Assert.Equal(expectedResult, result);
}
}
}

Expand Down