Skip to content

Commit

Permalink
Add customer request headers to audit log (#637)
Browse files Browse the repository at this point in the history
Add a feature to allow end users to add data to the audit logs by way of a custom HTTP header.
  • Loading branch information
feordin authored Sep 19, 2019
1 parent 3c75df6 commit a91e8eb
Show file tree
Hide file tree
Showing 25 changed files with 636 additions and 174 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ public class FhirServerConfiguration
public virtual CorsConfiguration Cors { get; } = new CorsConfiguration();

public OperationsConfiguration Operations { get; } = new OperationsConfiguration();

public AuditConfiguration Audit { get; } = new AuditConfiguration();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ public ActionResult Authorize(
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Redirect URL passed to Authorize failed to resolve.");
_logger.LogDebug(ex, "Redirect URL passed to Authorize failed to resolve.");
}

if (!_isAadV2 && !string.IsNullOrEmpty(aud))
Expand Down
16 changes: 16 additions & 0 deletions src/Microsoft.Health.Fhir.Api/Features/Audit/AuditConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

namespace Microsoft.Health.Fhir.Api.Features.Audit
{
public static class AuditConstants
{
public const string CustomAuditHeaderKeyValue = "CustomAuditHeaderCollectionKeyValue";

public const int MaximumNumberOfCustomHeaders = 10;

public const int MaximumLengthOfCustomHeader = 2048;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using Microsoft.Health.Fhir.Core.Exceptions;

namespace Microsoft.Health.Fhir.Api.Features.Audit
{
public class AuditHeaderException : FhirException
{
public AuditHeaderException(string headerName, int size)
: base(string.Format(Resources.CustomAuditHeaderTooLarge, AuditConstants.MaximumLengthOfCustomHeader, headerName, size))
{
}

public AuditHeaderException(int size)
: base(string.Format(Resources.TooManyCustomAuditHeaders, AuditConstants.MaximumNumberOfCustomHeaders, size))
{
}
}
}
63 changes: 63 additions & 0 deletions src/Microsoft.Health.Fhir.Api/Features/Audit/AuditHeaderReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using EnsureThat;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Health.Fhir.Core.Configs;

namespace Microsoft.Health.Fhir.Api.Features.Audit
{
public class AuditHeaderReader : IAuditHeaderReader
{
private readonly AuditConfiguration _auditConfiguration;

public AuditHeaderReader(IOptions<AuditConfiguration> auditConfiguration)
{
EnsureArg.IsNotNull(auditConfiguration?.Value, nameof(auditConfiguration));

_auditConfiguration = auditConfiguration.Value;
}

public IReadOnlyDictionary<string, string> Read(HttpContext httpContext)
{
EnsureArg.IsNotNull(httpContext, nameof(httpContext));

object cachedCustomHeaders;

if (httpContext.Items.TryGetValue(AuditConstants.CustomAuditHeaderKeyValue, out cachedCustomHeaders))
{
return cachedCustomHeaders as IReadOnlyDictionary<string, string>;
}

var customHeaders = new Dictionary<string, string>();

foreach (KeyValuePair<string, StringValues> header in httpContext.Request.Headers)
{
if (header.Key.StartsWith(_auditConfiguration.CustomAuditHeaderPrefix, StringComparison.OrdinalIgnoreCase))
{
var headerValue = header.Value.ToString();
if (headerValue.Length > AuditConstants.MaximumLengthOfCustomHeader)
{
throw new AuditHeaderException(header.Key, headerValue.Length);
}

customHeaders[header.Key] = headerValue;
}
}

if (customHeaders.Count > AuditConstants.MaximumNumberOfCustomHeaders)
{
throw new AuditHeaderException(customHeaders.Count);
}

httpContext.Items[AuditConstants.CustomAuditHeaderKeyValue] = customHeaders;
return customHeaders;
}
}
}
8 changes: 6 additions & 2 deletions src/Microsoft.Health.Fhir.Api/Features/Audit/AuditHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ public class AuditHelper : IAuditHelper
private readonly IAuditEventTypeMapping _auditEventTypeMapping;
private readonly IAuditLogger _auditLogger;
private readonly ILogger<AuditHelper> _logger;
private readonly IAuditHeaderReader _auditHeaderReader;

public AuditHelper(
IFhirRequestContextAccessor fhirRequestContextAccessor,
IAuditEventTypeMapping auditEventTypeMapping,
IAuditLogger auditLogger,
ILogger<AuditHelper> logger)
ILogger<AuditHelper> logger,
IAuditHeaderReader auditHeaderReader)
{
EnsureArg.IsNotNull(fhirRequestContextAccessor, nameof(fhirRequestContextAccessor));
EnsureArg.IsNotNull(auditEventTypeMapping, nameof(auditEventTypeMapping));
Expand All @@ -37,6 +39,7 @@ public AuditHelper(
_auditEventTypeMapping = auditEventTypeMapping;
_auditLogger = auditLogger;
_logger = logger;
_auditHeaderReader = auditHeaderReader;
}

/// <inheritdoc />
Expand Down Expand Up @@ -74,7 +77,8 @@ private void Log(AuditAction auditAction, string controllerName, string actionNa
statusCode: statusCode,
correlationId: fhirRequestContext.CorrelationId,
callerIpAddress: httpContext.Connection?.RemoteIpAddress?.ToString(),
callerClaims: claimsExtractor.Extract());
callerClaims: claimsExtractor.Extract(),
customHeaders: _auditHeaderReader.Read(httpContext));
}
}
}
Expand Down
12 changes: 10 additions & 2 deletions src/Microsoft.Health.Fhir.Api/Features/Audit/AuditLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,22 @@ public void LogAudit(
HttpStatusCode? statusCode,
string correlationId,
string callerIpAddress,
IReadOnlyCollection<KeyValuePair<string, string>> callerClaims)
IReadOnlyCollection<KeyValuePair<string, string>> callerClaims,
IReadOnlyDictionary<string, string> customerHeaders = null)
{
string claimsInString = null;
string customerHeadersInString = null;

if (callerClaims != null)
{
claimsInString = string.Join(";", callerClaims.Select(claim => $"{claim.Key}={claim.Value}"));
}

if (customerHeaders != null)
{
customerHeadersInString = string.Join(";", customerHeaders.Select(header => $"{header.Key}={header.Value}"));
}

_logger.LogInformation(
AuditMessageFormat,
auditAction,
Expand All @@ -76,7 +83,8 @@ public void LogAudit(
action,
statusCode,
correlationId,
claimsInString);
claimsInString,
customerHeadersInString);
}
}
}
15 changes: 15 additions & 0 deletions src/Microsoft.Health.Fhir.Api/Features/Audit/IAuditHeaderReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System.Collections.Generic;
using Microsoft.AspNetCore.Http;

namespace Microsoft.Health.Fhir.Api.Features.Audit
{
public interface IAuditHeaderReader
{
IReadOnlyDictionary<string, string> Read(HttpContext httpContext);
}
}
4 changes: 3 additions & 1 deletion src/Microsoft.Health.Fhir.Api/Features/Audit/IAuditLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public interface IAuditLogger
/// <param name="correlationId">The correlation ID.</param>
/// <param name="callerIpAddress">The caller IP address.</param>
/// <param name="callerClaims">The claims of the caller.</param>
/// <param name="customHeaders">Headers added by the caller with data to be added to the audit logs.</param>
void LogAudit(
AuditAction auditAction,
string operation,
Expand All @@ -33,6 +34,7 @@ void LogAudit(
HttpStatusCode? statusCode,
string correlationId,
string callerIpAddress,
IReadOnlyCollection<KeyValuePair<string, string>> callerClaims);
IReadOnlyCollection<KeyValuePair<string, string>> callerClaims,
IReadOnlyDictionary<string, string> customHeaders = null);
}
}
2 changes: 2 additions & 0 deletions src/Microsoft.Health.Fhir.Api/Modules/AuditModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public void Load(IServiceCollection services)

services.AddSingleton<IAuditLogger, AuditLogger>();

services.AddSingleton<IAuditHeaderReader, AuditHeaderReader>();

services.Add<AuditHelper>()
.Singleton()
.AsService<IAuditHelper>();
Expand Down
20 changes: 19 additions & 1 deletion src/Microsoft.Health.Fhir.Api/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions src/Microsoft.Health.Fhir.Api/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@
<data name="ContentTypeHeaderRequired" xml:space="preserve">
<value>The "content-type" header is required.</value>
</data>
<data name="CustomAuditHeaderTooLarge" xml:space="preserve">
<value>The maximum length of a custom audit header value is {0}. The supplied custom audit header '{1}' has length of {2}.</value>
<comment>PH0 is a constant defining the max length of a header. PH1 is the name of the header sent in the request. PH2 is the length of the incoming header value. Example: The maximum length of a custom audit header value is 2048. The supplied custom audit header 'X-MS-AZUREFHIR-AUDIT-SITE' has length of 3072.</comment>
</data>
<data name="Forbidden" xml:space="preserve">
<value>Authorization failed.</value>
</data>
Expand Down Expand Up @@ -179,6 +183,10 @@
<data name="ToggleNavigation" xml:space="preserve">
<value>Toggle navigation</value>
</data>
<data name="TooManyCustomAuditHeaders" xml:space="preserve">
<value>The maximum number of custom audit headers allowed is {0}. The number of custom audit headers supplied is {1}.</value>
<comment>PH0 is a constant defining the max number of custom headers. PH1 is the count of custom headers sent in the request. Example: The maximum number of custom audit headers allowed is 10. The number of custom audit headers supplied is 12.</comment>
</data>
<data name="UnableToObtainOpenIdConfiguration" xml:space="preserve">
<value>Unable to obtain OpenID configuration.</value>
</data>
Expand Down
32 changes: 32 additions & 0 deletions src/Microsoft.Health.Fhir.Core/Configs/AuditConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using Microsoft.Health.Fhir.Core.Exceptions;

namespace Microsoft.Health.Fhir.Core.Configs
{
public class AuditConfiguration
{
private string _customAuditHeaderPrefix = "X-MS-AZUREFHIR-AUDIT-";

public string CustomAuditHeaderPrefix
{
get
{
return _customAuditHeaderPrefix;
}

set
{
if (string.IsNullOrEmpty(value))
{
throw new InvalidDefinitionException(Resources.CustomHeaderPrefixCannotBeEmpty);
}

_customAuditHeaderPrefix = value;
}
}
}
}
9 changes: 9 additions & 0 deletions src/Microsoft.Health.Fhir.Core/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Microsoft.Health.Fhir.Core/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@
<value>Found result with Id '{0}', which did not match the provided Id '{1}'.</value>
<comment>{0} is an Id e.g. 123456</comment>
</data>
<data name="CustomHeaderPrefixCannotBeEmpty" xml:space="preserve">
<value>The prefix used to identify custom audit headers cannot be empty.</value>
</data>
<data name="DeleteVersionNotAllowed" xml:space="preserve">
<value>Deleting a specific record version is not supported.</value>
</data>
Expand Down
Loading

0 comments on commit a91e8eb

Please sign in to comment.