Skip to content

Commit

Permalink
Migrating Auditing Code from Fhir to Shared Components (#74)
Browse files Browse the repository at this point in the history
* Initial pass at moving generic Auditing code from Fhir to Healthcare Shared components.
Respective tests have also been added.

* Addressed comments. Removed files that were FHIR specific.

* Removed further FHIR specific files. Deleted an empty folder.

* Added a few more generic files.

* Added respective tests for the files moved.

* Fixed indentation.
  • Loading branch information
v-shaaal authored Aug 12, 2020
1 parent c0c6075 commit 621a95f
Show file tree
Hide file tree
Showing 28 changed files with 872 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// -------------------------------------------------------------------------------------------------
// 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 Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Health.Api.Features.Audit;
using Microsoft.Health.Extensions.DependencyInjection;
using NSubstitute;
using Xunit;

namespace Microsoft.Health.Api.UnitTests.Features.Audit
{
public class AuditEventTypeMappingTests
{
private const string ControllerName = nameof(MockController);
private const string AnonymousMethodName = nameof(MockController.Anonymous);
private const string AudittedMethodName = nameof(MockController.Auditted);
private const string NoAttributeMethodName = nameof(MockController.NoAttribute);
private const string AuditEventType = "audit";

private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider = Substitute.For<IActionDescriptorCollectionProvider>();
private readonly AuditEventTypeMapping _auditEventTypeMapping;

public AuditEventTypeMappingTests()
{
Type mockControllerType = typeof(MockController);

var actionDescriptors = new List<ActionDescriptor>()
{
new ControllerActionDescriptor()
{
ControllerName = ControllerName,
ActionName = AnonymousMethodName,
MethodInfo = mockControllerType.GetMethod(AnonymousMethodName),
},
new ControllerActionDescriptor()
{
ControllerName = ControllerName,
ActionName = AudittedMethodName,
MethodInfo = mockControllerType.GetMethod(AudittedMethodName),
},
new ControllerActionDescriptor()
{
ControllerName = ControllerName,
ActionName = NoAttributeMethodName,
MethodInfo = mockControllerType.GetMethod(NoAttributeMethodName),
},
new PageActionDescriptor()
{
},
};

var actionDescriptorCollection = new ActionDescriptorCollection(actionDescriptors, 1);

_actionDescriptorCollectionProvider.ActionDescriptors.Returns(actionDescriptorCollection);

_auditEventTypeMapping = new AuditEventTypeMapping(_actionDescriptorCollectionProvider);

((IStartable)_auditEventTypeMapping).Start();
}

[Theory]
[InlineData(ControllerName, AnonymousMethodName, null)]
[InlineData(ControllerName, AudittedMethodName, AuditEventType)]
public void GivenControllerNameAndActionName_WhenGetAuditEventTypeIsCalled_ThenAuditEventTypeShouldBeReturned(string controllerName, string actionName, string expectedAuditEventType)
{
string actualAuditEventType = _auditEventTypeMapping.GetAuditEventType(controllerName, actionName);

Assert.Equal(expectedAuditEventType, actualAuditEventType);
}

[Fact]
public void GivenUnknownControllerNameAndActionName_WhenGetAuditEventTypeIsCalled_ThenAuditExceptionShouldBeThrown()
{
Assert.Throws<MissingAuditEventTypeMappingException>(() => _auditEventTypeMapping.GetAuditEventType("test", "action"));
}

private class MockController : Controller
{
[AllowAnonymous]
public IActionResult Anonymous() => new OkResult();

[AuditEventType(AuditEventType)]
public IActionResult Auditted() => new OkResult();

public IActionResult NoAttribute() => new OkResult();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Health.Api.Features.Audit;
using Microsoft.Health.Core.Features.Security;
using NSubstitute;
using Xunit;

namespace Microsoft.Health.Api.UnitTests.Features.Audit
{
public class AuditMiddlewareTests
{
private readonly IClaimsExtractor _claimsExtractor = Substitute.For<IClaimsExtractor>();
private readonly IAuditHelper _auditHelper = Substitute.For<IAuditHelper>();

private readonly AuditMiddleware _auditMiddleware;

private readonly HttpContext _httpContext = new DefaultHttpContext();

public AuditMiddlewareTests()
{
_auditMiddleware = new AuditMiddleware(
httpContext => Task.CompletedTask,
_claimsExtractor,
_auditHelper);
}

[Fact]
public async Task GivenNotAuthXFailure_WhenInvoked_ThenAuditLogShouldNotBeLogged()
{
_httpContext.Response.StatusCode = (int)HttpStatusCode.OK;

await _auditMiddleware.Invoke(_httpContext);

_auditHelper.DidNotReceiveWithAnyArgs().LogExecuted(
httpContext: default,
claimsExtractor: default);
}

[Theory]
[InlineData(HttpStatusCode.Unauthorized)]
public async Task GivenAuthXFailed_WhenInvoked_ThenAuditLogShouldBeLogged(HttpStatusCode statusCode)
{
_httpContext.Response.StatusCode = (int)statusCode;

await _auditMiddleware.Invoke(_httpContext);

_auditHelper.Received(1).LogExecuted(_httpContext, _claimsExtractor);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
Expand Down
24 changes: 24 additions & 0 deletions src/Microsoft.Health.Api/Extensions/MethodInfoExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// -------------------------------------------------------------------------------------------------
// 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 System.Linq;
using System.Reflection;
using EnsureThat;

namespace Microsoft.Health.Api.Extensions
{
public static class MethodInfoExtensions
{
public static IEnumerable<T> GetCustomAttributes<T>(this MethodInfo methodInfo, bool inherit = false)
where T : Attribute
{
EnsureArg.IsNotNull(methodInfo, nameof(methodInfo));

return methodInfo.GetCustomAttributes(typeof(T), inherit)?.Cast<T>();
}
}
}
20 changes: 20 additions & 0 deletions src/Microsoft.Health.Api/Features/Audit/AuditAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// -------------------------------------------------------------------------------------------------
// 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.Api.Features.Audit
{
public enum AuditAction
{
/// <summary>
/// Executing
/// </summary>
Executing,

/// <summary>
/// Executed
/// </summary>
Executed,
}
}
16 changes: 16 additions & 0 deletions src/Microsoft.Health.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.Api.Features.Audit
{
public static class AuditConstants
{
public const string CustomAuditHeaderKeyValue = "CustomAuditHeaderCollectionKeyValue";

public const int MaximumNumberOfCustomHeaders = 10;

public const int MaximumLengthOfCustomHeader = 2048;
}
}
22 changes: 22 additions & 0 deletions src/Microsoft.Health.Api/Features/Audit/AuditEventTypeAttribute.cs
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 System;
using EnsureThat;

namespace Microsoft.Health.Api.Features.Audit
{
[AttributeUsage(AttributeTargets.Method)]
public class AuditEventTypeAttribute : Attribute
{
public AuditEventTypeAttribute(string auditEventType)
{
EnsureArg.IsNotNull(auditEventType, nameof(auditEventType));
AuditEventType = auditEventType;
}

public string AuditEventType { get; }
}
}
80 changes: 80 additions & 0 deletions src/Microsoft.Health.Api/Features/Audit/AuditEventTypeMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// -------------------------------------------------------------------------------------------------
// 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 System.Linq;
using EnsureThat;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Health.Api.Extensions;
using Microsoft.Health.Extensions.DependencyInjection;

namespace Microsoft.Health.Api.Features.Audit
{
/// <summary>
/// Provides the ability to lookup audit event type.
/// </summary>
/// <remarks>
/// <para>
/// Normally, the MVC middleware handles the routing which maps the controller name and action name to a method within the controller.
/// </para>
/// <para>
/// The <see cref="AuditEventTypeAttribute"/> that contains the audit event type information is defined on the method so we need the method
/// in order to be able to retrieve the attribute. However, since authentication middleware runs before the MVC middleware, if the authentication
/// rejects the call for whatever reason, we will not be able to retrieve the attribute and therefore, will not be able to get the corresponding audit event type.
/// </para>
/// <para>
/// This class builds the mapping ahead of time so that we can lookup the audit event type any time during the pipeline given the controller name and action name.
/// </para>
/// </remarks>
public class AuditEventTypeMapping : IAuditEventTypeMapping, IStartable
{
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;

private IReadOnlyDictionary<(string ControllerName, string ActionName), Attribute> _attributeDictionary;

public AuditEventTypeMapping(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider)
{
EnsureArg.IsNotNull(actionDescriptorCollectionProvider, nameof(actionDescriptorCollectionProvider));

_actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
}

/// <inheritdoc />
public string GetAuditEventType(string controllerName, string actionName)
{
if (!_attributeDictionary.TryGetValue((controllerName, actionName), out Attribute attribute))
{
throw new MissingAuditEventTypeMappingException(controllerName, actionName);
}

if (attribute is AuditEventTypeAttribute auditEventTypeAttribute)
{
return auditEventTypeAttribute.AuditEventType;
}

return null;
}

void IStartable.Start()
{
_attributeDictionary = _actionDescriptorCollectionProvider.ActionDescriptors.Items
.OfType<ControllerActionDescriptor>()
.Select(ad =>
{
Attribute attribute = ad.MethodInfo?.GetCustomAttributes<AllowAnonymousAttribute>().FirstOrDefault() ??
(Attribute)ad.MethodInfo?.GetCustomAttributes<AuditEventTypeAttribute>().FirstOrDefault();

return (ad.ControllerName, ad.ActionName, Attribute: attribute);
})
.Where(item => item.Attribute != null)
.ToDictionary(
item => (item.ControllerName, item.ActionName),
item => item.Attribute);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// -------------------------------------------------------------------------------------------------
// 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 EnsureThat;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Health.Core.Features.Security;

namespace Microsoft.Health.Api.Features.Audit
{
[AttributeUsage(AttributeTargets.Class)]
public class AuditLoggingFilterAttribute : ActionFilterAttribute
{
private readonly IClaimsExtractor _claimsExtractor;
private readonly IAuditHelper _auditHelper;

public AuditLoggingFilterAttribute(
IClaimsExtractor claimsExtractor,
IAuditHelper auditHelper)
{
EnsureArg.IsNotNull(claimsExtractor, nameof(claimsExtractor));
EnsureArg.IsNotNull(auditHelper, nameof(auditHelper));

_claimsExtractor = claimsExtractor;
_auditHelper = auditHelper;
}

public override void OnActionExecuting(ActionExecutingContext context)
{
EnsureArg.IsNotNull(context, nameof(context));

_auditHelper.LogExecuting(context.HttpContext, _claimsExtractor);

base.OnActionExecuting(context);
}

public override void OnResultExecuted(ResultExecutedContext context)
{
EnsureArg.IsNotNull(context, nameof(context));

_auditHelper.LogExecuted(context.HttpContext, _claimsExtractor);

base.OnResultExecuted(context);
}
}
}
Loading

0 comments on commit 621a95f

Please sign in to comment.