diff --git a/CHANGELOG.md b/CHANGELOG.md index 6acbcd06..032095d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ All notable changes to Audit.NET and its extensions will be documented in this f The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). +## [26.0.1] - 2024-08-22: +- Audit.WebApi.Core: Adding new SkipResponseBodyContent() configuration to allow deciding whether to skip or include the response body content **after** the action is executed (#690) + ## [26.0.0] - 2024-07-19: - Audit.NET.Elasticsearch: Upgrading the elasticsearch data provider to use the new client Elastic.Clients.Elasticsearch instead of the deprecated NEST client. [Migration guide](https://www.elastic.co/guide/en/elasticsearch/client/net-api/current/migration-guide.html) (#682) diff --git a/Directory.Build.props b/Directory.Build.props index 1ba8ae21..bbbc7f21 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 26.0.0 + 26.0.1 false diff --git a/src/Audit.WebApi/AuditMiddleware.cs b/src/Audit.WebApi/AuditMiddleware.cs index c08b9c61..6bc2a45e 100644 --- a/src/Audit.WebApi/AuditMiddleware.cs +++ b/src/Audit.WebApi/AuditMiddleware.cs @@ -155,11 +155,12 @@ private async Task AfterInvoke(HttpContext context, bool includeResponseBody, bo auditAction.ResponseStatus = AuditApiHelper.GetStatusCodeString(statusCode); if (includeResponseBody && auditAction.ResponseBody == null) { + var skipResponse = _config._skipRequestBodyBuilder != null && _config._skipRequestBodyBuilder.Invoke(context); auditAction.ResponseBody = new BodyContent { Type = context.Response.ContentType, Length = context.Response.ContentLength, - Value = await AuditApiHelper.GetResponseBody(context, default) + Value = skipResponse ? null : await AuditApiHelper.GetResponseBody(context, default) }; } } diff --git a/src/Audit.WebApi/ConfigurationApi/AuditMiddlewareConfigurator.cs b/src/Audit.WebApi/ConfigurationApi/AuditMiddlewareConfigurator.cs index cb97bb7e..33d29893 100644 --- a/src/Audit.WebApi/ConfigurationApi/AuditMiddlewareConfigurator.cs +++ b/src/Audit.WebApi/ConfigurationApi/AuditMiddlewareConfigurator.cs @@ -12,6 +12,7 @@ public class AuditMiddlewareConfigurator : IAuditMiddlewareConfigurator internal Func _includeRequestBodyBuilder; internal Func _includeResponseBodyBuilder; internal Func _eventTypeNameBuilder; + internal Func _skipRequestBodyBuilder; public IAuditMiddlewareConfigurator IncludeHeaders(bool include = true) { @@ -61,6 +62,26 @@ public IAuditMiddlewareConfigurator IncludeResponseBody(Func return this; } + public IAuditMiddlewareConfigurator SkipResponseBodyContent(Func skipPredicate) + { + if (_includeResponseBodyBuilder == null) + { + _includeResponseBodyBuilder = _ => true; + } + _skipRequestBodyBuilder = skipPredicate; + return this; + } + + public IAuditMiddlewareConfigurator SkipResponseBodyContent(bool skip) + { + if (_includeResponseBodyBuilder == null) + { + _includeResponseBodyBuilder = _ => true; + } + _skipRequestBodyBuilder = _ => skip; + return this; + } + public IAuditMiddlewareConfigurator FilterByRequest(Func requestPredicate) { _requestFilter = requestPredicate; @@ -73,9 +94,9 @@ public IAuditMiddlewareConfigurator WithEventType(string eventTypeName) return this; } - public IAuditMiddlewareConfigurator WithEventType(Func eventTypeNamePredicate) + public IAuditMiddlewareConfigurator WithEventType(Func eventTypeNameBuilder) { - _eventTypeNameBuilder = eventTypeNamePredicate; + _eventTypeNameBuilder = eventTypeNameBuilder; return this; } } diff --git a/src/Audit.WebApi/ConfigurationApi/IAuditMiddlewareConfigurator.cs b/src/Audit.WebApi/ConfigurationApi/IAuditMiddlewareConfigurator.cs index fb15b0e4..9ee00b8d 100644 --- a/src/Audit.WebApi/ConfigurationApi/IAuditMiddlewareConfigurator.cs +++ b/src/Audit.WebApi/ConfigurationApi/IAuditMiddlewareConfigurator.cs @@ -52,21 +52,38 @@ public interface IAuditMiddlewareConfigurator /// /// Specifies a predicate to determine the event type name on the audit output. /// - /// A function of the executing context to determine the event type name. The following placeholders can be used as part of the string: + /// A function of the executing context to determine the event type name. The following placeholders can be used as part of the string: /// - {url}: replaced with the requst URL. /// - {verb}: replaced with the HTTP verb used (GET, POST, etc). /// - IAuditMiddlewareConfigurator WithEventType(Func eventTypeNamePredicate); + IAuditMiddlewareConfigurator WithEventType(Func eventTypeNameBuilder); /// /// Specifies whether the response body should be included on the audit output. /// /// True to include the response body, false otherwise IAuditMiddlewareConfigurator IncludeResponseBody(bool include = true); + /// /// Specifies a predicate to determine whether the response body should be included on the audit output. + /// The predicate is evaluated before request execution. /// - /// A function of the executed context to determine whether the response body should be included on the audit output + /// A function of the executed context to determine whether the response body should be included on the audit output. + /// This predicate is evaluated before request execution. IAuditMiddlewareConfigurator IncludeResponseBody(Func includePredicate); + + /// + /// Specifies a predicate to determine whether the response body content should be skipped or included in the audit output. + /// The predicate is evaluated after request execution. + /// + /// A function of the executed context to determine whether the response body content should be skipped or included in the audit output. + /// This predicate is evaluated after request execution. + IAuditMiddlewareConfigurator SkipResponseBodyContent(Func skipPredicate); + + /// + /// Specifies whether the response body content should be skipped or included in the audit output. + /// + /// A boolean to determine whether the response body should be skipped or included in the audit output. + IAuditMiddlewareConfigurator SkipResponseBodyContent(bool skip); } } #endif \ No newline at end of file diff --git a/src/Audit.WebApi/README.md b/src/Audit.WebApi/README.md index 9ea452cd..f57e9fea 100644 --- a/src/Audit.WebApi/README.md +++ b/src/Audit.WebApi/README.md @@ -283,7 +283,10 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) - **IncludeHeaders()**: Boolean (or function of the HTTP context that returns a boolean) to indicate whether to include the Http Request Headers or not. Default is false. - **IncludeResponseHeaders()**: Boolean (or function of the HTTP context that returns a boolean) to indicate whether to include the Http Response Headers or not. Default is false. - **IncludeRequestBody()**: Boolean (or function of the HTTP context that returns a boolean) to indicate whether to include or exclude the request body from the logs. Default is false. (Check the following note) -- **IncludeResponseBody()**: Boolean (or function of the HTTP context that returns a boolean) to indicate whether to include response body or not. Default is false. +- **IncludeResponseBody()**: Boolean (or a predicate of the HTTP context before execution) to indicate whether to include response body. +The predicate is evaluated before action execution. Default is false. +- **SkipResponseBodyContent()**: Boolean (or a predicate of the HTTP context after execution) to indicate whether to skip or include the response body content. +The predicate is evaluated after action execution. Default is false. - **WithEventType()**: A string (or a function of the HTTP context that returns a string) that identifies the event type. Can contain the following placeholders (default is "{verb} {url}"): - \{verb}: replaced with the HTTP verb used (GET, POST, etc). - \{url}: replaced with the request URL. diff --git a/test/Audit.UnitTest/EventLogDataProviderTests.cs b/test/Audit.UnitTest/EventLogDataProviderTests.cs index 69aa6a0d..7593f856 100644 --- a/test/Audit.UnitTest/EventLogDataProviderTests.cs +++ b/test/Audit.UnitTest/EventLogDataProviderTests.cs @@ -16,6 +16,7 @@ public void SetUp() Core.Configuration.Reset(); } +#if !NETCOREAPP3_1 [Test] public void TestEventLogDataProvider_InsertReplaceEvent() { @@ -45,6 +46,7 @@ public void TestEventLogDataProvider_InsertReplaceEvent() // Assert Assert.That(eventId1, Is.Null); } +#endif [Test] public void Test_EventLogDataProvider_FluentApi() diff --git a/test/Audit.WebApi.UnitTest/IsolationTests.cs b/test/Audit.WebApi.UnitTest/IsolationTests.cs index ce09436f..817380a4 100644 --- a/test/Audit.WebApi.UnitTest/IsolationTests.cs +++ b/test/Audit.WebApi.UnitTest/IsolationTests.cs @@ -3,16 +3,9 @@ using System.Linq; using System.Net; using System.Threading.Tasks; -using Audit.Core; using Audit.Core.Providers; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using NUnit.Framework; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; namespace Audit.WebApi.UnitTest { @@ -33,31 +26,6 @@ public IActionResult Action_Middleware([FromQuery] string q) } } - public class TestHelper - { - public static TestServer GetTestServer(AuditDataProvider dataProvider) - { - return new TestServer(WebHost.CreateDefaultBuilder() - .ConfigureServices(services => - { - services.AddSingleton(dataProvider); - services.AddControllers(); - }) - .Configure((ctx, app) => - { - app.UseAuditMiddleware(cfg => cfg - .FilterByRequest(r => r.Path.Value?.Contains("Action_Middleware") == true)); - app.UseRouting(); - app.UseEndpoints(e => - { - e.MapControllers(); - }); - }) - .ConfigureLogging(log => log.SetMinimumLevel(LogLevel.Warning)) - ); - } - } - [Parallelizable] public class IsolationTests { @@ -72,7 +40,8 @@ private async Task Test_Isolation_InjectDataProvider_AuditApiAttribute_TestOne() { var guid = Guid.NewGuid(); var dataProvider = new InMemoryDataProvider(); - using var app = TestHelper.GetTestServer(dataProvider); + using var app = TestHelper.GetTestServer(dataProvider, + cfg => cfg.FilterByRequest(r => r.Path.Value?.Contains("Action_Middleware") == true)); using var client = app.CreateClient(); var response = await client.GetAsync($"/TestIsolation/Action_AuditApiAttribute?q={guid}"); @@ -93,7 +62,8 @@ private async Task Test_Isolation_InjectDataProvider_Middleware_TestOne() { var guid = Guid.NewGuid(); var dataProvider = new InMemoryDataProvider(); - using var app = TestHelper.GetTestServer(dataProvider); + using var app = TestHelper.GetTestServer(dataProvider, cfg => cfg + .FilterByRequest(r => r.Path.Value?.Contains("Action_Middleware") == true)); using var client = app.CreateClient(); var response = await client.GetAsync($"/TestIsolation/Action_Middleware?q={guid}"); diff --git a/test/Audit.WebApi.UnitTest/MiddlewareTests.cs b/test/Audit.WebApi.UnitTest/MiddlewareTests.cs new file mode 100644 index 00000000..5a2bada3 --- /dev/null +++ b/test/Audit.WebApi.UnitTest/MiddlewareTests.cs @@ -0,0 +1,72 @@ +#if NETCOREAPP3_1 || NET6_0 +using System.Net; +using System.Threading.Tasks; +using Audit.Core.Providers; +using Microsoft.AspNetCore.Mvc; +using NUnit.Framework; + +namespace Audit.WebApi.UnitTest +{ + [Route("[controller]")] + [ApiController] + public class TestMiddlewareController : ControllerBase + { + [Route("")] + [HttpGet] + public IActionResult Get([FromQuery] int length) + { + return Ok(new string('#', length)); + } + } + + [Parallelizable] + public class MiddlewareTests + { + [TestCase(null, null, true, true)] + [TestCase(null, false, false, false)] + [TestCase(null, true, false, true)] + [TestCase(true, null, false, false)] + [TestCase(true, false, false, false)] + [TestCase(true, true, false, true)] + [TestCase(false, null, true, true)] + [TestCase(false, false, true, true)] + [TestCase(false, true, true, true)] + public async Task Test_IncludeResponseBody(bool? includeResponseBody, bool? skipResponseBody, bool expectNullResponseBody, bool expectNullResponseBodyContent) + { + var dataProvider = new InMemoryDataProvider(); + + using var app = TestHelper.GetTestServer(dataProvider, cfg => + { + cfg.FilterByRequest(r => r.Path.Value?.Contains("TestMiddleware") == true); + if (includeResponseBody.HasValue) + { + cfg.IncludeResponseBody(includeResponseBody!.Value); + } + if (skipResponseBody.HasValue) + { + if (skipResponseBody.GetValueOrDefault()) + { + cfg.SkipResponseBodyContent(_ => skipResponseBody!.Value); + } + else + { + cfg.SkipResponseBodyContent(skipResponseBody!.Value); + } + } + }); + + using var client = app.CreateClient(); + + var response = await client.GetAsync($"/TestMiddleware?length=10"); + + var events = dataProvider.GetAllEvents(); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + + Assert.That(events, Has.Count.EqualTo(1)); + Assert.That(events[0].GetWebApiAuditAction().ResponseBody, expectNullResponseBody ? Is.Null : Is.Not.Null); + Assert.That(events[0].GetWebApiAuditAction().ResponseBody?.Value, expectNullResponseBodyContent ? Is.Null : Is.Not.Null); + } + } +} +#endif \ No newline at end of file diff --git a/test/Audit.WebApi.UnitTest/TestHelper.cs b/test/Audit.WebApi.UnitTest/TestHelper.cs new file mode 100644 index 00000000..a5ca05f0 --- /dev/null +++ b/test/Audit.WebApi.UnitTest/TestHelper.cs @@ -0,0 +1,37 @@ +#if NETCOREAPP3_1 || NET6_0 +using System; +using Audit.Core; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Audit.WebApi.UnitTest +{ + public class TestHelper + { + public static TestServer GetTestServer(AuditDataProvider dataProvider, Action middlewareConfig) + { + return new TestServer(WebHost.CreateDefaultBuilder() + .ConfigureServices(services => + { + services.AddSingleton(dataProvider); + services.AddControllers(); + }) + .Configure((ctx, app) => + { + app.UseAuditMiddleware(middlewareConfig); + app.UseRouting(); + app.UseEndpoints(e => + { + e.MapControllers(); + }); + }) + .ConfigureLogging(log => log.SetMinimumLevel(LogLevel.Warning)) + ); + } + } +} +#endif \ No newline at end of file