diff --git a/src/Shared/LayoutRenderers/AspNetUserClaimLayoutRenderer.cs b/src/Shared/LayoutRenderers/AspNetUserClaimLayoutRenderer.cs index b895111a..e0d2ee2b 100644 --- a/src/Shared/LayoutRenderers/AspNetUserClaimLayoutRenderer.cs +++ b/src/Shared/LayoutRenderers/AspNetUserClaimLayoutRenderer.cs @@ -9,6 +9,8 @@ using NLog.Common; using NLog.Config; using NLog.LayoutRenderers; +using NLog.Layouts; +using NLog.Web.Enums; namespace NLog.Web.LayoutRenderers { @@ -28,11 +30,16 @@ public class AspNetUserClaimLayoutRenderer : AspNetLayoutMultiValueRendererBase /// /// /// When value is prefixed with "ClaimTypes." (Remember dot) then ít will lookup in well-known claim types from . 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 /// [DefaultParameter] public string ClaimType { get; set; } + /// + /// If this is true, then all string properties of the are rendered as well the values in its Properties property. + /// + public bool Verbose { get; set; } + /// protected override void InitializeLayoutRenderer() { @@ -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}, builder, logEvent); + } + else + { + builder.Append(claim.Value); + + } } } } @@ -82,6 +108,174 @@ protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent) } } + private void SerializeVerbose(IEnumerable 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 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 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(']'); + } + } + } + + /// + /// Separator between objects, like cookies. Only used for + /// + /// Render with + public string ObjectSeparator { get => _objectSeparatorLayout?.OriginalText; set => _objectSeparatorLayout = new SimpleLayout(value ?? ""); } + private SimpleLayout _objectSeparatorLayout = new SimpleLayout(";"); + + /// + /// Get the rendered + /// + private string GetRenderedObjectSeparator(LogEventInfo logEvent) + { + return logEvent != null ? _objectSeparatorLayout.Render(logEvent) : ObjectSeparator; + } + + /// + /// Append the quoted name and value separated by a colon + /// + 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; + } + + /// + /// Append the quoted name and value separated by a value separator + /// and ended by item separator + /// + 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> GetAllClaims(System.Security.Principal.IPrincipal claimsPrincipal) { diff --git a/tests/Shared/LayoutRenderers/AspNetUserClaimLayoutRendererTests.cs b/tests/Shared/LayoutRenderers/AspNetUserClaimLayoutRendererTests.cs index becd3abd..612391a2 100644 --- a/tests/Shared/LayoutRenderers/AspNetUserClaimLayoutRendererTests.cs +++ b/tests/Shared/LayoutRenderers/AspNetUserClaimLayoutRendererTests.cs @@ -4,6 +4,7 @@ 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; @@ -11,6 +12,7 @@ using NLog.Web.LayoutRenderers; using NSubstitute; using Xunit; +using static System.Net.WebRequestMethods; namespace NLog.Web.Tests.LayoutRenderers { @@ -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(); + + 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() + { + 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(); + + 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() + { + 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(); + + 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() + { + claim1, claim2, claim3 + } + ); + + httpContext.User.Returns(principal); + + // Act + string result = renderer.Render(new LogEventInfo()); + + // Assert + Assert.Equal(expectedResult, result); + } } }