From bc755e6be3623dece6ac4de14135c3bde564e118 Mon Sep 17 00:00:00 2001 From: thepirat000 Date: Sat, 9 Dec 2023 17:23:55 -0600 Subject: [PATCH] - Audit.MongoClient: New extension to audit `MongoClient` instances. Generate Audit Logs by adding a Command Event Subscriber into the configuration of the MongoDB Driver (#640, #641) --- Audit.NET.sln | 24 +- CHANGELOG.md | 4 + Directory.Build.props | 2 +- README.md | 4 +- src/Audit.EntityFramework/DbContextHelper.cs | 4 +- src/Audit.FileSystem/FileSystemMonitor.cs | 2 +- .../AuditHttpClientHandler.cs | 2 +- .../Audit.MongoClient.csproj | 49 +++ .../AuditEventMongoCommand.cs | 15 + .../AuditMongoConfigurator.cs | 72 ++++ .../ClusterBuilderExtensions.cs | 20 ++ .../IAuditMongoConfigurator.cs | 57 +++ .../MongoClientSettingsExtensions.cs | 28 ++ .../MongoAuditEventSubscriber.cs | 193 ++++++++++ src/Audit.MongoClient/MongoCommandEvent.cs | 107 ++++++ src/Audit.MongoClient/MongoConnection.cs | 25 ++ .../Properties/AssemblyInfo.cs | 25 ++ src/Audit.MongoClient/README.md | 194 ++++++++++ src/Audit.MongoClient/images/icon.png | Bin 0 -> 8392 bytes src/Audit.Mvc/AuditAttribute.Core.cs | 4 +- src/Audit.Mvc/AuditAttribute.cs | 12 +- src/Audit.Mvc/AuditPageFilter.cs | 5 +- src/Audit.NET/AuditScope.cs | 337 +++++++++--------- src/Audit.NET/Configuration.cs | 6 + src/Audit.NET/IAuditScope.cs | 1 + src/Audit.SignalR/AuditPipelineModule.cs | 8 +- src/Audit.WCF.Client/AuditMessageInspector.cs | 2 +- src/Audit.WCF/AuditOperationInvoker.cs | 12 +- src/Audit.WebApi/AuditApiAdapter.Core.cs | 2 +- src/Audit.WebApi/AuditApiAdapter.cs | 2 +- src/Audit.WebApi/AuditMiddleware.cs | 2 +- src/pack.cmd | 3 + src/push.cmd | 4 +- .../EfChangeTrackerTests.cs | 2 +- .../EfChangeTrackerTests.cs | 2 +- test/Audit.IntegrationTest/PostgreSqlTests.cs | 2 +- .../Audit.MongoClient.UnitTest.csproj | 37 ++ .../AuditMongoClientIntegrationTests.cs | 65 ++++ .../MongoAuditEventSubscriberTests.cs | 311 ++++++++++++++++ .../RavenDbDataProviderTests.cs | 2 +- .../SqlServerTests.cs | 4 +- .../InMemoryDataProviderTests.cs | 4 +- test/Run-Tests.ps1 | 1 + test/testns.bat | 5 + 44 files changed, 1457 insertions(+), 205 deletions(-) create mode 100644 src/Audit.MongoClient/Audit.MongoClient.csproj create mode 100644 src/Audit.MongoClient/AuditEventMongoCommand.cs create mode 100644 src/Audit.MongoClient/ConfigurationApi/AuditMongoConfigurator.cs create mode 100644 src/Audit.MongoClient/ConfigurationApi/ClusterBuilderExtensions.cs create mode 100644 src/Audit.MongoClient/ConfigurationApi/IAuditMongoConfigurator.cs create mode 100644 src/Audit.MongoClient/ConfigurationApi/MongoClientSettingsExtensions.cs create mode 100644 src/Audit.MongoClient/MongoAuditEventSubscriber.cs create mode 100644 src/Audit.MongoClient/MongoCommandEvent.cs create mode 100644 src/Audit.MongoClient/MongoConnection.cs create mode 100644 src/Audit.MongoClient/Properties/AssemblyInfo.cs create mode 100644 src/Audit.MongoClient/README.md create mode 100644 src/Audit.MongoClient/images/icon.png create mode 100644 test/Audit.MongoClient.UnitTest/Audit.MongoClient.UnitTest.csproj create mode 100644 test/Audit.MongoClient.UnitTest/AuditMongoClientIntegrationTests.cs create mode 100644 test/Audit.MongoClient.UnitTest/MongoAuditEventSubscriberTests.cs diff --git a/Audit.NET.sln b/Audit.NET.sln index 2327a396..39b1f77d 100644 --- a/Audit.NET.sln +++ b/Audit.NET.sln @@ -138,7 +138,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Audit.AzureStorageTables.Un EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Audit.NET.Serilog", "src\Audit.NET.Serilog\Audit.NET.Serilog.csproj", "{BFB6AB47-0ECC-447E-BD37-3D3B8AA5ACE9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Audit.SqlServer.UnitTest", "test\Audit.SqlServer.UnitTest\Audit.SqlServer.UnitTest.csproj", "{B9F11735-5303-47C6-A8FA-EF9EDAD5FBE8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Audit.SqlServer.UnitTest", "test\Audit.SqlServer.UnitTest\Audit.SqlServer.UnitTest.csproj", "{B9F11735-5303-47C6-A8FA-EF9EDAD5FBE8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Audit.MongoClient", "src\Audit.MongoClient\Audit.MongoClient.csproj", "{49E52BFD-5E59-4397-93EB-09DCF16FA9CA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Audit.MongoClient.UnitTest", "test\Audit.MongoClient.UnitTest\Audit.MongoClient.UnitTest.csproj", "{5B3DADC7-981D-42A0-A155-4C3F26DB1991}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -596,6 +600,22 @@ Global {B9F11735-5303-47C6-A8FA-EF9EDAD5FBE8}.Release|Any CPU.Build.0 = Release|Any CPU {B9F11735-5303-47C6-A8FA-EF9EDAD5FBE8}.Release|x64.ActiveCfg = Release|Any CPU {B9F11735-5303-47C6-A8FA-EF9EDAD5FBE8}.Release|x64.Build.0 = Release|Any CPU + {49E52BFD-5E59-4397-93EB-09DCF16FA9CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {49E52BFD-5E59-4397-93EB-09DCF16FA9CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {49E52BFD-5E59-4397-93EB-09DCF16FA9CA}.Debug|x64.ActiveCfg = Debug|Any CPU + {49E52BFD-5E59-4397-93EB-09DCF16FA9CA}.Debug|x64.Build.0 = Debug|Any CPU + {49E52BFD-5E59-4397-93EB-09DCF16FA9CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {49E52BFD-5E59-4397-93EB-09DCF16FA9CA}.Release|Any CPU.Build.0 = Release|Any CPU + {49E52BFD-5E59-4397-93EB-09DCF16FA9CA}.Release|x64.ActiveCfg = Release|Any CPU + {49E52BFD-5E59-4397-93EB-09DCF16FA9CA}.Release|x64.Build.0 = Release|Any CPU + {5B3DADC7-981D-42A0-A155-4C3F26DB1991}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B3DADC7-981D-42A0-A155-4C3F26DB1991}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B3DADC7-981D-42A0-A155-4C3F26DB1991}.Debug|x64.ActiveCfg = Debug|Any CPU + {5B3DADC7-981D-42A0-A155-4C3F26DB1991}.Debug|x64.Build.0 = Debug|Any CPU + {5B3DADC7-981D-42A0-A155-4C3F26DB1991}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B3DADC7-981D-42A0-A155-4C3F26DB1991}.Release|Any CPU.Build.0 = Release|Any CPU + {5B3DADC7-981D-42A0-A155-4C3F26DB1991}.Release|x64.ActiveCfg = Release|Any CPU + {5B3DADC7-981D-42A0-A155-4C3F26DB1991}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -659,6 +679,8 @@ Global {97F2CC5D-6AE6-40E9-AFAC-D4F513C63330} = {A54B4BB6-3439-432B-AFD9-FE62D6528D42} {BFB6AB47-0ECC-447E-BD37-3D3B8AA5ACE9} = {E62475E8-0BE1-4464-BBD9-FD06CC546593} {B9F11735-5303-47C6-A8FA-EF9EDAD5FBE8} = {A54B4BB6-3439-432B-AFD9-FE62D6528D42} + {49E52BFD-5E59-4397-93EB-09DCF16FA9CA} = {E62475E8-0BE1-4464-BBD9-FD06CC546593} + {5B3DADC7-981D-42A0-A155-4C3F26DB1991} = {A54B4BB6-3439-432B-AFD9-FE62D6528D42} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B1989475-43D6-4AA5-9717-6DBF734B0C6E} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7912e2fc..ec16fbe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ 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/). +## [22.1.0] - 2023-12-09: +- Audit.MongoClient: New extension to audit `MongoClient` instances. +Generate Audit Logs by adding a Command Event Subscriber into the configuration of the MongoDB Driver (#640, #641) + ## [22.0.2] - 2023-12-01: - Audit.NET: Support to include the [Activity](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-instrumentation-walkthroughs) distributed trace information into the Audit Event. - Audit.NET: Adding `Configuration.Reset()` method to reset the global settings to the default. diff --git a/Directory.Build.props b/Directory.Build.props index ac2025fe..e4c64e26 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 22.0.2 + 22.1.0 false diff --git a/README.md b/README.md index 90fbc954..0c902651 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ With Audit.NET you can generate tracking information about operations being exec [WebAPI](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.WebApi/README.md), [WCF](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.WCF/README.md), [File System](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.FileSystem/README.md), -[SignalR](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.SignalR/README.md) +[SignalR](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.SignalR/README.md), +[MongoClient](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.MongoClient/README.md) and [HttpClient](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.HttpClient/README.md). ## [NuGet](https://www.nuget.org/packages/Audit.NET/) @@ -910,6 +911,7 @@ The following packages are extensions to log interactions with different systems icon | **[Audit.WCF](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.WCF/README.md)** | Generate detailed **server-side** audit logs for **Windows Communication Foundation (WCF)** service calls, by configuring a provided behavior. icon | **[Audit.WCF.Client](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.WCF.Client/README.md)** | Generate detailed **client-side** audit logs for **Windows Communication Foundation (WCF)** service calls, by configuring a provided behavior. icon | **[Audit.WebApi](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.WebApi/README.md)** | Generate detailed audit logs by decorating **Web API** Methods and Controllers with an action filter attribute, or by using a middleware. Includes support for ASP.NET Core. +icon | **[Audit.MongoClient](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.MongoClient/README.md)** | Generate detailed audit logs by adding a [Command Event Subscriber](https://mongodb.github.io/mongo-csharp-driver/2.8/reference/driver_core/events/) into the configuration of the MongoDB Driver. # Storage providers diff --git a/src/Audit.EntityFramework/DbContextHelper.cs b/src/Audit.EntityFramework/DbContextHelper.cs index 3a85f998..40c5a993 100644 --- a/src/Audit.EntityFramework/DbContextHelper.cs +++ b/src/Audit.EntityFramework/DbContextHelper.cs @@ -139,7 +139,7 @@ public static string GetStateName(EntityState state) public void SaveScope(IAuditDbContext context, IAuditScope scope, EntityFrameworkEvent @event) { UpdateAuditEvent(@event, context); - ((AuditEventEntityFramework)scope.Event).EntityFrameworkEvent = @event; + scope.EventAs().EntityFrameworkEvent = @event; context.OnScopeSaving(scope); scope.Save(); context.OnScopeSaved(scope); @@ -151,7 +151,7 @@ public void SaveScope(IAuditDbContext context, IAuditScope scope, EntityFramewor public async Task SaveScopeAsync(IAuditDbContext context, IAuditScope scope, EntityFrameworkEvent @event, CancellationToken cancellationToken = default) { UpdateAuditEvent(@event, context); - ((AuditEventEntityFramework)scope.Event).EntityFrameworkEvent = @event; + scope.EventAs().EntityFrameworkEvent = @event; context.OnScopeSaving(scope); await scope.SaveAsync(cancellationToken); context.OnScopeSaved(scope); diff --git a/src/Audit.FileSystem/FileSystemMonitor.cs b/src/Audit.FileSystem/FileSystemMonitor.cs index 2c6602b4..9e30cce7 100644 --- a/src/Audit.FileSystem/FileSystemMonitor.cs +++ b/src/Audit.FileSystem/FileSystemMonitor.cs @@ -157,7 +157,7 @@ private void ProcessEvent(FileSystemEventArgs e, FileSystemEventType type) { fsEvent.Errors = null; } - (auditScope.Event as AuditEventFileSystem).FileSystemEvent = fsEvent; + auditScope.EventAs().FileSystemEvent = fsEvent; } } } diff --git a/src/Audit.HttpClient/AuditHttpClientHandler.cs b/src/Audit.HttpClient/AuditHttpClientHandler.cs index ceff5ca9..3b530252 100644 --- a/src/Audit.HttpClient/AuditHttpClientHandler.cs +++ b/src/Audit.HttpClient/AuditHttpClientHandler.cs @@ -151,7 +151,7 @@ protected override async Task SendAsync(HttpRequestMessage { // Update the response and save action.Response = await GetResponseAudit(response, cancellationToken); - ((AuditEventHttpClient)scope.Event).Action = action; + scope.EventAs().Action = action; await SaveDispose(scope, cancellationToken); } return response; diff --git a/src/Audit.MongoClient/Audit.MongoClient.csproj b/src/Audit.MongoClient/Audit.MongoClient.csproj new file mode 100644 index 00000000..c1823635 --- /dev/null +++ b/src/Audit.MongoClient/Audit.MongoClient.csproj @@ -0,0 +1,49 @@ + + + Generate detailed Audit Logs for operations executed within MongoDB. + Copyright 2023 + Audit.MongoClient + Federico Colombo + net472;netstandard2.0;net5.0 + $(DefineConstants);STRONG_NAME + $(NoWarn);1591 + true + Audit.MongoClient + Audit.MongoClient + Audit;Trail;Log;MongoDB;Client + icon.png + https://github.com/thepirat000/Audit.NET + 2.0.3 + false + false + false + MIT + https://github.com/thepirat000/Audit.NET + README.md + + + + + + + + + + + + + $(DefineConstants);IS_NK_JSON + + + + $(DefineConstants);IS_TEXT_JSON + + + + + + + + + + diff --git a/src/Audit.MongoClient/AuditEventMongoCommand.cs b/src/Audit.MongoClient/AuditEventMongoCommand.cs new file mode 100644 index 00000000..ce87aee1 --- /dev/null +++ b/src/Audit.MongoClient/AuditEventMongoCommand.cs @@ -0,0 +1,15 @@ +using Audit.Core; + +namespace Audit.MongoClient +{ + /// + /// Represents the output of the audit process for a Mongo command event + /// + public class AuditEventMongoCommand : AuditEvent + { + /// + /// Gets or sets the Mongo Command details. + /// + public MongoCommandEvent Command { get; set; } + } +} diff --git a/src/Audit.MongoClient/ConfigurationApi/AuditMongoConfigurator.cs b/src/Audit.MongoClient/ConfigurationApi/AuditMongoConfigurator.cs new file mode 100644 index 00000000..8875ad6c --- /dev/null +++ b/src/Audit.MongoClient/ConfigurationApi/AuditMongoConfigurator.cs @@ -0,0 +1,72 @@ +using System; +using Audit.Core; +using MongoDB.Driver.Core.Events; + +namespace Audit.MongoClient.ConfigurationApi +{ + public class AuditMongoConfigurator : IAuditMongoConfigurator + { + internal Func _includeReplyPredicate; + internal Func _commandFilter; + internal Func _eventTypePredicate; + internal EventCreationPolicy? _eventCreationPolicy; + internal AuditDataProvider _auditDataProvider; + internal IAuditScopeFactory _auditScopeFactory; + + /// + public IAuditMongoConfigurator IncludeReply(Func includeReplyPredicate) + { + _includeReplyPredicate = includeReplyPredicate; + return this; + } + + /// + public IAuditMongoConfigurator IncludeReply(bool include = true) + { + _includeReplyPredicate = _ => include; + return this; + } + + /// + public IAuditMongoConfigurator EventType(Func eventTypePredicate) + { + _eventTypePredicate = eventTypePredicate; + return this; + } + + /// + public IAuditMongoConfigurator EventType(string eventType) + { + _eventTypePredicate = _ => eventType; + return this; + } + + /// + public IAuditMongoConfigurator CommandFilter(Func commandFilter) + { + _commandFilter = commandFilter; + return this; + } + + /// + public IAuditMongoConfigurator AuditScopeFactory(IAuditScopeFactory auditScopeFactory) + { + _auditScopeFactory = auditScopeFactory; + return this; + } + + /// + public IAuditMongoConfigurator CreationPolicy(EventCreationPolicy eventCreationPolicy) + { + _eventCreationPolicy = eventCreationPolicy; + return this; + } + + /// + public IAuditMongoConfigurator AuditDataProvider(AuditDataProvider auditDataProvider) + { + _auditDataProvider = auditDataProvider; + return this; + } + } +} diff --git a/src/Audit.MongoClient/ConfigurationApi/ClusterBuilderExtensions.cs b/src/Audit.MongoClient/ConfigurationApi/ClusterBuilderExtensions.cs new file mode 100644 index 00000000..b0f7f6e0 --- /dev/null +++ b/src/Audit.MongoClient/ConfigurationApi/ClusterBuilderExtensions.cs @@ -0,0 +1,20 @@ +using System; +using Audit.MongoClient.ConfigurationApi; +using MongoDB.Driver.Core.Configuration; + +namespace Audit.MongoClient +{ + public static class ClusterBuilderExtensions + { + /// + /// Adds an Event Subscriber to the MongoDB ClusterBuilder, using the given audit configuration. + /// + /// The cluster builder + /// The audit configuration. Null to use the default configuration + public static ClusterBuilder AddAuditSubscriber(this ClusterBuilder clusterBuilder, Action config = null) + { + clusterBuilder.Subscribe(new MongoAuditEventSubscriber(config)); + return clusterBuilder; + } + } +} \ No newline at end of file diff --git a/src/Audit.MongoClient/ConfigurationApi/IAuditMongoConfigurator.cs b/src/Audit.MongoClient/ConfigurationApi/IAuditMongoConfigurator.cs new file mode 100644 index 00000000..4bc587b3 --- /dev/null +++ b/src/Audit.MongoClient/ConfigurationApi/IAuditMongoConfigurator.cs @@ -0,0 +1,57 @@ +using System; +using Audit.Core; +using MongoDB.Driver.Core.Events; + +namespace Audit.MongoClient.ConfigurationApi +{ + public interface IAuditMongoConfigurator + { + /// + /// Specifies a predicate to determine whether the audit event should include the server reply. The reply is not included by default. + /// + /// A function of the Command Succeeded Event to determine whether to include the the server reply in the audit event + IAuditMongoConfigurator IncludeReply(Func includeReplyPredicate); + + /// + /// Specifies whether the audit event should include the server reply. The reply is not included by default. + /// + /// True to include the server reply, false otherwise + IAuditMongoConfigurator IncludeReply(bool include = true); + + /// + /// Specifies a predicate to determine the event type name on the audit output. + /// + /// A function of the Command Start Event to determine the event type name. The following placeholders can be used as part of the string: + /// - {command}: replaced with the command name. + /// + IAuditMongoConfigurator EventType(Func eventTypeNamePredicate); + /// + /// Specifies the event type name to use in the audit output. + /// + /// The event type name to use. The following placeholders can be used as part of the string: + /// - {command}: replaced with the command name. + /// + IAuditMongoConfigurator EventType(string eventTypeName); + + /// + /// Sets a filter function to determine the events to log as a function of the command. By default all commands are logged. + /// + IAuditMongoConfigurator CommandFilter(Func commandFilter); + + /// + /// Specifies the event creation policy to use for this interception. Default is NULL to use the globally configured creation policy. + /// + /// The creation policy to use + IAuditMongoConfigurator CreationPolicy(EventCreationPolicy eventCreationPolicy); + + /// + /// Specifies the audit data provider to use. Default is NULL to use the globally configured data provider. + /// + IAuditMongoConfigurator AuditDataProvider(AuditDataProvider auditDataProvider); + + /// + /// Specifies the Audit Scope factory to use. Default is NULL to use the default AuditScopeFactory. + /// + IAuditMongoConfigurator AuditScopeFactory(IAuditScopeFactory auditScopeFactory); + } +} \ No newline at end of file diff --git a/src/Audit.MongoClient/ConfigurationApi/MongoClientSettingsExtensions.cs b/src/Audit.MongoClient/ConfigurationApi/MongoClientSettingsExtensions.cs new file mode 100644 index 00000000..3f24eefb --- /dev/null +++ b/src/Audit.MongoClient/ConfigurationApi/MongoClientSettingsExtensions.cs @@ -0,0 +1,28 @@ +using System; +using Audit.MongoClient.ConfigurationApi; +using MongoDB.Driver; + +namespace Audit.MongoClient +{ + public static class MongoClientSettingsExtensions + { + /// + /// Adds an Event Subscriber to the MongoClientSetting's ClusterBuilder, using the given audit configuration. + /// + /// The client settings instance + /// The audit configuration. Null to use the default configuration + public static MongoClientSettings AddAuditSubscriber(this MongoClientSettings clientSettings, Action config = null) + { + + if (clientSettings.ClusterConfigurator == null) + { + clientSettings.ClusterConfigurator = cc => cc.AddAuditSubscriber(config); + } + else + { + throw new ArgumentException("Adding an Audit Subscriber to MongoClientSettings is not possible due to an existing ClusterConfigurator. Instead, utilize the AddAuditSubscriber function provided by the ClusterBuilder."); + } + return clientSettings; + } + } +} \ No newline at end of file diff --git a/src/Audit.MongoClient/MongoAuditEventSubscriber.cs b/src/Audit.MongoClient/MongoAuditEventSubscriber.cs new file mode 100644 index 00000000..7288c2ac --- /dev/null +++ b/src/Audit.MongoClient/MongoAuditEventSubscriber.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Audit.Core; +using Audit.Core.Extensions; +using MongoDB.Bson; +using MongoDB.Driver.Core.Events; + +namespace Audit.MongoClient +{ + /// + /// A MongoDB event subscriber that logs the commands using Audit.NET framework + /// + public class MongoAuditEventSubscriber : IEventSubscriber + { + private readonly ConfigurationApi.AuditMongoConfigurator _config = new ConfigurationApi.AuditMongoConfigurator(); + private readonly IEventSubscriber _subscriber; + private static readonly HashSet IgnoredCommands = new HashSet(new[] { "isMaster", "buildInfo", "getLastError", "saslStart", "saslContinue" }); + + // RequestId -> CommandStartedEvent + internal readonly ConcurrentDictionary _requestBuffer = new ConcurrentDictionary(); + + /// + /// Specifies the event type name to use in the audit output. The following placeholders can be used as part of the string: + /// - {command}: replaced with the command name. + /// + public string EventType { set => _config._eventTypePredicate = _ => value; } + + /// + /// Specifies whether the audit event should include the server reply. The reply is not included by default. + /// + public bool IncludeReply { set => _config._includeReplyPredicate = _ => value; } + + /// + /// Sets a filter function to determine if a command should be logged. By default all commands are logged. + /// + public Func CommandFilter { set => _config._commandFilter = value; } + + /// + /// Specifies the event creation policy to use for this interception. Default is NULL to use the globally configured creation policy. + /// + public EventCreationPolicy? CreationPolicy { set => _config._eventCreationPolicy = value; } + /// + /// Specifies the audit data provider to use. Default is NULL to use the globally configured data provider. + /// + public AuditDataProvider AuditDataProvider { set => _config._auditDataProvider = value; } + /// + /// Specifies the Audit Scope factory to use. Default is NULL to use the default AuditScopeFactory. + /// + public IAuditScopeFactory AuditScopeFactory { set => _config._auditScopeFactory = value; } + + /// + /// Initializes a new instance of the class with the default configuration. + /// + public MongoAuditEventSubscriber() + { + _subscriber = new ReflectionEventSubscriber(this); + } + + /// + /// Initializes a new instance of the with the given fluent configuration. + /// + public MongoAuditEventSubscriber(Action config) + : this() + { + if (config != null) + { + config.Invoke(_config); + } + } + + public bool TryGetEventHandler(out Action handler) + { + return _subscriber.TryGetEventHandler(out handler); + } + + /// + /// Handles the Command Started Event + /// + public void Handle(CommandStartedEvent ev) + { + if (ShouldSkipEvent(ev.CommandName)) + { + return; + } + + if (_config._commandFilter != null && !_config._commandFilter(ev)) + { + return; + } + + var eventType = (_config._eventTypePredicate?.Invoke(ev) ?? "{command}") + .Replace("{command}", ev.CommandName); + + // Create the audit event + var auditEvent = new AuditEventMongoCommand() + { + Command = new MongoCommandEvent() + { + CommandStartedEvent = ev, + RequestId = ev.RequestId, + Connection = new MongoConnection() + { + ClusterId = ev.ConnectionId.ServerId.ClusterId.Value, + Endpoint = ev.ConnectionId.ServerId.EndPoint.ToString(), + LocalConnectionId = ev.ConnectionId.LongLocalValue, + ServerConnectionId = ev.ConnectionId.LongServerValue + }, + OperationId = ev.OperationId, + CommandName = ev.CommandName, + Body = BsonTypeMapper.MapToDotNetValue(ev.Command), + Timestamp = ev.Timestamp + } + }; + + // Create the audit scope + var scopeOptions = new AuditScopeOptions() + { + EventType = eventType, + AuditEvent = auditEvent, + CreationPolicy = _config._eventCreationPolicy, + DataProvider = _config._auditDataProvider + }; + var auditScopeFactory = _config._auditScopeFactory ?? Configuration.AuditScopeFactory; + var auditScope = auditScopeFactory.Create(scopeOptions); + + // Store the scope in the buffer + _requestBuffer.TryAdd(ev.RequestId, auditScope); + } + + /// + /// Handles the Command Failed Event + /// + public void Handle(CommandFailedEvent ev) + { + if (ShouldSkipEvent(ev.CommandName)) + { + return; + } + + // Get the audit scope from the buffer + if (!_requestBuffer.TryRemove(ev.RequestId, out var auditScope)) + { + return; + } + + // Update the audit event + var auditEvent = auditScope.EventAs(); + + auditEvent.Command.Error = ev.Failure.GetExceptionInfo(); + auditEvent.Command.Duration = Convert.ToInt32(ev.Duration.TotalMilliseconds); + auditEvent.Command.Success = false; + + // Dispose the audit scope to trigger saving + auditScope.Dispose(); + } + + /// + /// Handles the Command Succeeded Event + /// + public void Handle(CommandSucceededEvent ev) + { + if (ShouldSkipEvent(ev.CommandName)) + { + return; + } + + if (!_requestBuffer.TryRemove(ev.RequestId, out var auditScope)) + { + return; + } + + // Update the audit event + var auditEvent = auditScope.EventAs(); + + if (_config._includeReplyPredicate?.Invoke(ev) == true) + { + auditEvent.Command.Reply = BsonTypeMapper.MapToDotNetValue(ev.Reply); + } + + auditEvent.Command.Duration = Convert.ToInt32(ev.Duration.TotalMilliseconds); + auditEvent.Command.Success = true; + + // Dispose the audit scope + auditScope.Dispose(); + } + + private static bool ShouldSkipEvent(string commandName) + { + return Configuration.AuditDisabled || IgnoredCommands.Contains(commandName); + } + } +} diff --git a/src/Audit.MongoClient/MongoCommandEvent.cs b/src/Audit.MongoClient/MongoCommandEvent.cs new file mode 100644 index 00000000..983aa6ca --- /dev/null +++ b/src/Audit.MongoClient/MongoCommandEvent.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using Audit.Core; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver.Core.Events; +#if IS_NK_JSON +using Newtonsoft.Json; +#else +using System.Text.Json.Serialization; +#endif + +namespace Audit.MongoClient +{ + public class MongoCommandEvent : IAuditOutput + { + /// + /// The Request identifier. + /// + public int RequestId { get; set; } + + /// + /// The Connection information. + /// + public MongoConnection Connection { get; set; } + + /// + /// The Operation identifier. + /// + public long? OperationId { get; set; } + + /// + /// The Command name. + /// + public string CommandName { get; set; } + + /// + /// The command body to be executed + /// + public object Body { get; set; } + + /// + /// The duration of the Mongo Event in milliseconds. + /// + public int? Duration { get; set; } + + /// + /// Indicates if the command succeeded. + /// + public bool? Success { get; set; } + + /// + /// The database reply. + /// +#if IS_NK_JSON + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] +#else + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] +#endif + public object Reply { get; set; } + + /// + /// The database error message if an error occurred, otherwise NULL. + /// +#if IS_NK_JSON + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] +#else + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] +#endif + public string Error { get; set; } + + /// + /// The command event Timestamp + /// + public DateTime Timestamp { get; set; } + + [JsonExtensionData] + [BsonExtraElements] + public Dictionary CustomFields { get; set; } = new Dictionary(); + + [JsonIgnore] + internal CommandStartedEvent CommandStartedEvent { get; set; } + + /// + /// Returns the DbContext associated to this event + /// + public CommandStartedEvent GetCommandStartedEvent() + { + return CommandStartedEvent; + } + + /// + /// Serializes this Audit Mongo Command as a JSON string + /// + public string ToJson() + { + return Configuration.JsonAdapter.Serialize(this); + } + /// + /// Parses an Audit Mongo Command from its JSON string representation. + /// + /// JSON string with the Mongo Command representation. + public static MongoCommandEvent FromJson(string json) + { + return Configuration.JsonAdapter.Deserialize(json); + } + } +} \ No newline at end of file diff --git a/src/Audit.MongoClient/MongoConnection.cs b/src/Audit.MongoClient/MongoConnection.cs new file mode 100644 index 00000000..15778f12 --- /dev/null +++ b/src/Audit.MongoClient/MongoConnection.cs @@ -0,0 +1,25 @@ +namespace Audit.MongoClient +{ + public class MongoConnection + { + /// + /// The Connection cluster identifier. + /// + public int ClusterId { get; set; } + + /// + /// The connection endpoint + /// + public string Endpoint { get; set; } + + /// + /// The local connection identifier + /// + public long LocalConnectionId { get; set; } + + /// + /// The server connection identifier + /// + public long? ServerConnectionId { get; set; } + } +} \ No newline at end of file diff --git a/src/Audit.MongoClient/Properties/AssemblyInfo.cs b/src/Audit.MongoClient/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..719800e4 --- /dev/null +++ b/src/Audit.MongoClient/Properties/AssemblyInfo.cs @@ -0,0 +1,25 @@ +using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Audit.MongoClient")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("6e129d24-80bb-4b64-b025-a5b3fb169dd4")] + +[assembly: CLSCompliant(true)] + +[assembly: InternalsVisibleTo("Audit.MongoClient.UnitTest"), + InternalsVisibleTo("Audit.UnitTest, PublicKey=00240000048000009400000006020000002400005253413100040000010001007f14e426ecfadb7a221dd166152164af7395256b474a50d56dad8080bba2edbea4ca85d89401245686311e557c9853c47095c6602a32d0a207ba2a55d282aa69d4f54d58eaf1e876fa18ac58bcd93bbb2aea797d7efda08ca0bc910dca508acc6ab6dbafcf7fd5b63859582ba558bf14d09f0b13d7a18b26688c1b2b98fcddc9")] \ No newline at end of file diff --git a/src/Audit.MongoClient/README.md b/src/Audit.MongoClient/README.md new file mode 100644 index 00000000..0fa7d741 --- /dev/null +++ b/src/Audit.MongoClient/README.md @@ -0,0 +1,194 @@ +# Audit.MongoClient + +**MongoDB client audit extension for [Audit.NET library](https://github.com/thepirat000/Audit.NET).** + +Generate Audit Logs by adding a [Command Event Subscriber](https://mongodb.github.io/mongo-csharp-driver/2.8/reference/driver_core/events/) +into the configuration of the MongoDB Driver. + +Audit.MongoClient provides the infrastructure to intercept a `MongoClient` instance, enabling the generation of audit logs for operations executed within MongoDB. + +> Note: This library is designed to **generate** audit events, not for storing events, If you're aiming to **store** audit events in a Mongo DB collection, you may use the [`Audit.NET.MongoDB`](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.NET.MongoDB/README.md) package. + +## Install + +**NuGet Package** + +To install the package run the following command on the Package Manager Console: + +``` +PM> Install-Package Audit.MongoClient +``` + +[![NuGet Status](https://img.shields.io/nuget/v/Audit.MongoClient.svg?style=flat)](https://www.nuget.org/packages/Audit.MongoClient/) +[![NuGet Count](https://img.shields.io/nuget/dt/Audit.MongoClient.svg)](https://www.nuget.org/packages/Audit.MongoClient/) + +## Usage + +To enable the audit log for a `MongoClient` instance you have to register a `MongoAuditEventSubscriber` instance +to the ClusterBuilder. + +This registration can be done in several ways: + +- Registering an instance of the provided `MongoAuditEventSubscriber`: + +```c +using Audit.MongoClient; +//... + +var mongoSettings = new MongoClientSettings() +{ + Server = new MongoServerAddress("localhost", 27017), + + // Register the audit subscriber: + ClusterConfigurator = clusterBuilder => clusterBuilder.Subscribe(new MongoAuditEventSubscriber() + { + IncludeReply = true + }) +}; + +// Create the audited client +_client = new MongoDB.Driver.MongoClient(mongoSettings); +``` + +- Calling the provided `AddAuditSubscriber()` extension method in `ClusterBuilder`: + +```c# +using Audit.MongoClient; +//... + +var mongoSettings = new MongoClientSettings() +{ + Server = new MongoServerAddress("localhost", 27017), + ClusterConfigurator = clusterBuilder => clusterBuilder.AddAuditSubscriber(auditConfig => auditConfig + .IncludeReply()) +}; + +_client = new MongoDB.Driver.MongoClient(mongoSettings); +``` + +- Reusing an existing `MongoClientSettings` instance by calling the provided `AddAuditSubscriber()` extension method: + +```c# +_client = new MongoDB.Driver.MongoClient(mongoSettings.AddAuditSubscriber(cfg => cfg + .IncludeReply()); +``` + +## Configuration + +### Output + +The audit events are stored using a _Data Provider_. You can use one of the [available data providers](https://github.com/thepirat000/Audit.NET#data-providers-included) or implement your own. Please refer to the [data providers](https://github.com/thepirat000/Audit.NET#data-providers) section on Audit.NET documentation. + +### Settings + +The `MongoAuditEventSubscriber` class allows to configure the following settings: + +- **EventType**: A string that identifies the event type. Default is "\{command}". It can contain the following placeholders: + - \{command}: Replaced by the Command Name (insert, update, delete, find, ...) +- **IncludeReply**: Specifies whether the command audit event should include the server reply. The reply is not included by default. +- **CommandFilter**: Set a filter function to determine which command events to log depending on the command start information. By default all commands are logged. +- **CreationPolicy**: Allows to set a specific event creation policy. By default the globally configured creation policy is used. See [Audit.NET Event Creation Policy](https://github.com/thepirat000/Audit.NET#event-creation-policy) section for more information. +- **AuditDataProvider**: Allows to set a specific audit data provider. By default the globally configured data provider is used. See [Audit.NET Data Providers](https://github.com/thepirat000/Audit.NET/blob/master/README.md#data-providers) section for more information. +- **AuditScopeFactory**: Allows to set a specific audit scope factory. By default the general [`AuditScopeFactory`](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.NET/AuditScopeFactory.cs) is used. + +You can customize these settings using the fluent API provided. Additionally, some settings can be set as functions of the +executed command, allowing you to adapt the behavior based on the specific command, such as including the reply only in specific cases. + +For example, to only audit _insert_ and _delete_ commands, and include the reply only if its length is less than 512 bytes: + +```c# +var mongoSettings = new MongoClientSettings() +{ + Server = new MongoServerAddress("localhost", 27017), + ClusterConfigurator = cc => cc + .AddAuditSubscriber(auditConfig => auditConfig + .IncludeReply(cmd => cmd.Reply.ToBson().Length < 512) + .CommandFilter(cmd => cmd.CommandName is "insert" or "delete")) +}; +``` + +## Output Details + +The following table describes the Audit.MongoClient output fields: + +### [MongoCommandEvent](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.MongoClient/MongoCommandEvent.cs) + +Describes a command call event + +| Field Name | Type | Description | +| ------------ | ---------------- | -------------- | +| RequestId | int | The unique request identifier | +| Connection | MongoConnection | Connection information | +| OperationId | long? | The operation identifier | +| CommandName | string | The Mongo command name (insert, update, delete, ...) | +| Body | object | The command body | +| Duration | int | The duration of the Mongo Event in milliseconds | +| Success | bool? | Indicates if the command succeeded | +| Reply | object | The database reply (optional) | +| Error | string | The database error message if an error occurred, otherwise NULL | +| Timestamp | DateTime | The command event Timestamp | + +### [MongoConnection](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.MongoClient/MongoConnection.cs) + +Contains the command's connection information + +| Field Name | Type | Description | +| ------------ | ---------------- | -------------- | +| ClusterId | int | The Connection cluster identifier | +| Endpoint | string | The Connection endpoint | +| LocalConnectionId | long | The local connection identifier | +| ServerConnectionId | long? | The server connection identifier | + +## Output Sample + +Mongo insert command: + +```javascript +{ + "Command": { + "RequestId": 5, + "Connection": { + "ClusterId": 1, + "Endpoint": "Unspecified/localhost:27017", + "LocalConnectionId": 3, + "ServerConnectionId": 55 + }, + "OperationId": 1, + "CommandName": "insert", + "Body": { + "insert": "MongoClient", + "ordered": true, + "$db": "AuditTest", + "lsid": { + "id": "9498dc51-935d-4e3d-9fc0-0031d993059d" + }, + "documents": [ + { + "_id": "6574dcbbda3ab8f0437d1c75", + "test": "this is a test document" + } + ] + }, + "Duration": 4, + "Success": true, + "Reply": { + "n": 1, + "ok": 1.0 + }, + "Timestamp": "2023-12-09T21:31:40.1286166Z" + }, + "EventType": "3c18aa76-91cb-4c89-b575-342c9158cb44", + "Environment": { + "UserName": "Federico Colombo", + "MachineName": "DESKTOP-ILAR98A", + "DomainName": "DESKTOP-ILAR98A", + "CallingMethodName": "Audit.MongoClient.MongoAuditEventSubscriber.Handle()", + "AssemblyName": "Audit.MongoClient, Version=22.0.2.0, Culture=neutral, PublicKeyToken=null", + "Culture": "en-US" + }, + "StartDate": "2023-12-09T21:31:40.1210357Z", + "EndDate": "2023-12-09T21:31:40.1370355Z", + "Duration": 16 +} +``` + diff --git a/src/Audit.MongoClient/images/icon.png b/src/Audit.MongoClient/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c47a5164da9963c77693b4beca23b33200f46b6e GIT binary patch literal 8392 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.Action = auditAction; if (auditAction?.Exception != null) { // An exception was thrown, save the event since OnResultExecutionAsync will not be triggered. @@ -175,7 +175,7 @@ private async Task AfterResultAsync(ResultExecutedContext filterContext) if (auditScope != null) { // Replace the Action field - ((AuditEventMvcAction)auditScope.Event).Action = auditAction; + auditScope.EventAs().Action = auditAction; if (auditScope.EventCreationPolicy == EventCreationPolicy.Manual) { await auditScope.SaveAsync(); diff --git a/src/Audit.Mvc/AuditAttribute.cs b/src/Audit.Mvc/AuditAttribute.cs index 0b77b9ea..bbecdc96 100644 --- a/src/Audit.Mvc/AuditAttribute.cs +++ b/src/Audit.Mvc/AuditAttribute.cs @@ -121,11 +121,11 @@ public override void OnActionExecuted(ActionExecutedContext filterContext) auditAction.Exception = filterContext.Exception.GetExceptionInfo(); auditAction.ResponseBody = IncludeResponseBody ? GetResponseBody(filterContext.Result) : null; } - var auditScope = filterContext.HttpContext.Items[AuditScopeKey] as AuditScope; - if (auditScope != null) + + if (filterContext.HttpContext.Items[AuditScopeKey] is AuditScope auditScope) { // Replace the Action field - (auditScope.Event as AuditEventMvcAction).Action = auditAction; + auditScope.EventAs().Action = auditAction; if (auditAction?.Exception != null) { // An exception was thrown, save the event since OnResultExecuted will not be triggered. @@ -155,11 +155,11 @@ public override void OnResultExecuted(ResultExecutedContext filterContext) auditAction.Exception = filterContext.Exception.GetExceptionInfo(); auditAction.ResponseBody = IncludeResponseBody ? GetResponseBody(filterContext.Result) : null; } - var auditScope = filterContext.HttpContext.Items[AuditScopeKey] as AuditScope; - if (auditScope != null) + + if (filterContext.HttpContext.Items[AuditScopeKey] is AuditScope auditScope) { // Replace the Action field - (auditScope.Event as AuditEventMvcAction).Action = auditAction; + auditScope.EventAs().Action = auditAction; if (auditScope.EventCreationPolicy == EventCreationPolicy.Manual) { auditScope.Save(); // for backwards compatibility diff --git a/src/Audit.Mvc/AuditPageFilter.cs b/src/Audit.Mvc/AuditPageFilter.cs index a5d099a6..50e55aff 100644 --- a/src/Audit.Mvc/AuditPageFilter.cs +++ b/src/Audit.Mvc/AuditPageFilter.cs @@ -161,11 +161,10 @@ public virtual async Task AfterExecutedAsync(PageHandlerExecutedContext context) } } - var auditScope = httpContext.Items[AuditScopeKey] as AuditScope; - if (auditScope != null) + if (httpContext.Items[AuditScopeKey] is AuditScope auditScope) { // Replace the Action field - ((AuditEventMvcAction)auditScope.Event).Action = auditAction; + auditScope.EventAs().Action = auditAction; // Save the event and dispose the scope await auditScope.DisposeAsync(); } diff --git a/src/Audit.NET/AuditScope.cs b/src/Audit.NET/AuditScope.cs index 9b8d9acb..82c1d537 100644 --- a/src/Audit.NET/AuditScope.cs +++ b/src/Audit.NET/AuditScope.cs @@ -27,6 +27,7 @@ internal AuditScope(AuditScopeOptions options) _targetGetter = options.TargetGetter; _event = options.AuditEvent ?? new AuditEvent(); + _event.StartDate = Configuration.SystemClock.UtcNow; _event.Environment = GetEnvironmentInfo(options); @@ -36,7 +37,6 @@ internal AuditScope(AuditScopeOptions options) _event.Activity = GetActivityTrace(); } #endif - _event.StartDate = Configuration.SystemClock.UtcNow; if (options.EventType != null) { _event.EventType = options.EventType; @@ -56,169 +56,6 @@ internal AuditScope(AuditScopeOptions options) } ProcessExtraFields(options.ExtraFields); } - -#if NET5_0_OR_GREATER - private AuditActivityTrace GetActivityTrace() - { - var activity = Activity.Current; - - if (activity == null) - { - return null; - } - - var spanId = activity.IdFormat switch - { - ActivityIdFormat.Hierarchical => activity.Id, - ActivityIdFormat.W3C => activity.SpanId.ToHexString(), - _ => null - }; - - var traceId = activity.IdFormat switch - { - ActivityIdFormat.Hierarchical => activity.RootId, - ActivityIdFormat.W3C => activity.TraceId.ToHexString(), - _ => null - }; - - var parentId = activity.IdFormat switch - { - ActivityIdFormat.Hierarchical => activity.ParentId, - ActivityIdFormat.W3C => activity.ParentSpanId.ToHexString(), - _ => null - }; - - - var result = new AuditActivityTrace() - { - StartTimeUtc = activity.StartTimeUtc, - SpanId = spanId, - TraceId = traceId, - ParentId = parentId, - Operation = activity.OperationName - }; - - if (activity.Tags.Any()) - { - result.Tags = new List(); - foreach (var tag in activity.Tags) - { - result.Tags.Add(new AuditActivityTag() { Key = tag.Key, Value = tag.Value }); - } - } - - if (activity.Events.Any()) - { - result.Events = new List(); - foreach (var ev in activity.Events) - { - result.Events.Add(new AuditActivityEvent() { Timestamp = ev.Timestamp, Name = ev.Name }); - } - } - - return result; - } -#endif - - [MethodImpl(MethodImplOptions.NoInlining)] - private AuditEventEnvironment GetEnvironmentInfo(AuditScopeOptions options) - { - var environment = new AuditEventEnvironment() - { - Culture = System.Globalization.CultureInfo.CurrentCulture.ToString(), - }; - MethodBase callingMethod = options.CallingMethod; -#if NET45 || NETSTANDARD2_0 || NETSTANDARD2_1 || NET461 || NET5_0_OR_GREATER - environment.UserName = Environment.UserName; - environment.MachineName = Environment.MachineName; - environment.DomainName = Environment.UserDomainName; - if (callingMethod == null) - { - callingMethod = new StackFrame(3 + options.SkipExtraFrames).GetMethod(); - } - if (options.IncludeStackTrace) - { - environment.StackTrace = new StackTrace(options.SkipExtraFrames, true).ToString(); - } -#else - environment.MachineName = Environment.GetEnvironmentVariable("COMPUTERNAME"); - environment.UserName = Environment.GetEnvironmentVariable("USERNAME"); -#endif - if (callingMethod != null) - { - environment.CallingMethodName = (callingMethod.DeclaringType != null ? callingMethod.DeclaringType.FullName + "." : "") + callingMethod.Name + "()"; - environment.AssemblyName = callingMethod.DeclaringType?.GetTypeInfo().Assembly.FullName; - } - - return environment; - } - - /// - /// Starts an audit scope - /// - [MethodImpl(MethodImplOptions.NoInlining)] - internal AuditScope Start() - { - _saveMode = SaveMode.InsertOnStart; - // Execute custom on scope created actions - Configuration.InvokeScopeCustomActions(ActionType.OnScopeCreated, this); - - // Process the event insertion (if applies) - if (_options.IsCreateAndSave) - { - EndEvent(); - SaveEvent(); - _ended = true; - } - else if (_creationPolicy == EventCreationPolicy.InsertOnStartReplaceOnEnd || _creationPolicy == EventCreationPolicy.InsertOnStartInsertOnEnd) - { - SaveEvent(); - _saveMode = _creationPolicy == EventCreationPolicy.InsertOnStartReplaceOnEnd ? SaveMode.ReplaceOnEnd : SaveMode.InsertOnEnd; - } - else if (_creationPolicy == EventCreationPolicy.InsertOnEnd) - { - _saveMode = SaveMode.InsertOnEnd; - } - else if (_creationPolicy == EventCreationPolicy.Manual) - { - _saveMode = SaveMode.Manual; - } - return this; - } - - /// - /// Starts an audit scope asynchronously - /// - /// The Cancellation Token. - [MethodImpl(MethodImplOptions.NoInlining)] - internal async Task StartAsync(CancellationToken cancellationToken = default) - { - _saveMode = SaveMode.InsertOnStart; - // Execute custom on scope created actions - await Configuration.InvokeScopeCustomActionsAsync(ActionType.OnScopeCreated, this, cancellationToken); - - // Process the event insertion (if applies) - if (_options.IsCreateAndSave) - { - EndEvent(); - await SaveEventAsync(false, cancellationToken); - _ended = true; - } - else if (_creationPolicy == EventCreationPolicy.InsertOnStartReplaceOnEnd || _creationPolicy == EventCreationPolicy.InsertOnStartInsertOnEnd) - { - await SaveEventAsync(false, cancellationToken); - _saveMode = _creationPolicy == EventCreationPolicy.InsertOnStartReplaceOnEnd ? SaveMode.ReplaceOnEnd : SaveMode.InsertOnEnd; - } - else if (_creationPolicy == EventCreationPolicy.InsertOnEnd) - { - _saveMode = SaveMode.InsertOnEnd; - } - else if (_creationPolicy == EventCreationPolicy.Manual) - { - _saveMode = SaveMode.Manual; - } - return this; - } #endregion #region Public Properties @@ -452,10 +289,182 @@ public async Task SaveAsync(CancellationToken cancellationToken = default) EndEvent(); await SaveEventAsync(false, cancellationToken); } + + /// + /// Gets the event related to this scope of a known AuditEvent derived type. Returns null if the event is not of the specified type. + /// + /// The AuditEvent derived type + public T EventAs() where T : AuditEvent + { + return _event as T; + } + #endregion #region Private Methods +#if NET5_0_OR_GREATER + private AuditActivityTrace GetActivityTrace() + { + var activity = Activity.Current; + + if (activity == null) + { + return null; + } + + var spanId = activity.IdFormat switch + { + ActivityIdFormat.Hierarchical => activity.Id, + ActivityIdFormat.W3C => activity.SpanId.ToHexString(), + _ => null + }; + + var traceId = activity.IdFormat switch + { + ActivityIdFormat.Hierarchical => activity.RootId, + ActivityIdFormat.W3C => activity.TraceId.ToHexString(), + _ => null + }; + + var parentId = activity.IdFormat switch + { + ActivityIdFormat.Hierarchical => activity.ParentId, + ActivityIdFormat.W3C => activity.ParentSpanId.ToHexString(), + _ => null + }; + + + var result = new AuditActivityTrace() + { + StartTimeUtc = activity.StartTimeUtc, + SpanId = spanId, + TraceId = traceId, + ParentId = parentId, + Operation = activity.OperationName + }; + + if (activity.Tags.Any()) + { + result.Tags = new List(); + foreach (var tag in activity.Tags) + { + result.Tags.Add(new AuditActivityTag() { Key = tag.Key, Value = tag.Value }); + } + } + + if (activity.Events.Any()) + { + result.Events = new List(); + foreach (var ev in activity.Events) + { + result.Events.Add(new AuditActivityEvent() { Timestamp = ev.Timestamp, Name = ev.Name }); + } + } + + return result; + } +#endif + + [MethodImpl(MethodImplOptions.NoInlining)] + private AuditEventEnvironment GetEnvironmentInfo(AuditScopeOptions options) + { + var environment = new AuditEventEnvironment() + { + Culture = System.Globalization.CultureInfo.CurrentCulture.ToString(), + }; + MethodBase callingMethod = options.CallingMethod; +#if NET45 || NETSTANDARD2_0 || NETSTANDARD2_1 || NET461 || NET5_0_OR_GREATER + environment.UserName = Environment.UserName; + environment.MachineName = Environment.MachineName; + environment.DomainName = Environment.UserDomainName; + if (callingMethod == null) + { + callingMethod = new StackFrame(3 + options.SkipExtraFrames).GetMethod(); + } + if (options.IncludeStackTrace) + { + environment.StackTrace = new StackTrace(options.SkipExtraFrames, true).ToString(); + } +#else + environment.MachineName = Environment.GetEnvironmentVariable("COMPUTERNAME"); + environment.UserName = Environment.GetEnvironmentVariable("USERNAME"); +#endif + if (callingMethod != null) + { + environment.CallingMethodName = (callingMethod.DeclaringType != null ? callingMethod.DeclaringType.FullName + "." : "") + callingMethod.Name + "()"; + environment.AssemblyName = callingMethod.DeclaringType?.GetTypeInfo().Assembly.FullName; + } + + return environment; + } + + /// + /// Starts an audit scope + /// + [MethodImpl(MethodImplOptions.NoInlining)] + internal AuditScope Start() + { + _saveMode = SaveMode.InsertOnStart; + // Execute custom on scope created actions + Configuration.InvokeScopeCustomActions(ActionType.OnScopeCreated, this); + + // Process the event insertion (if applies) + if (_options.IsCreateAndSave) + { + EndEvent(); + SaveEvent(); + _ended = true; + } + else if (_creationPolicy == EventCreationPolicy.InsertOnStartReplaceOnEnd || _creationPolicy == EventCreationPolicy.InsertOnStartInsertOnEnd) + { + SaveEvent(); + _saveMode = _creationPolicy == EventCreationPolicy.InsertOnStartReplaceOnEnd ? SaveMode.ReplaceOnEnd : SaveMode.InsertOnEnd; + } + else if (_creationPolicy == EventCreationPolicy.InsertOnEnd) + { + _saveMode = SaveMode.InsertOnEnd; + } + else if (_creationPolicy == EventCreationPolicy.Manual) + { + _saveMode = SaveMode.Manual; + } + return this; + } + + /// + /// Starts an audit scope asynchronously + /// + /// The Cancellation Token. + [MethodImpl(MethodImplOptions.NoInlining)] + internal async Task StartAsync(CancellationToken cancellationToken = default) + { + _saveMode = SaveMode.InsertOnStart; + // Execute custom on scope created actions + await Configuration.InvokeScopeCustomActionsAsync(ActionType.OnScopeCreated, this, cancellationToken); + + // Process the event insertion (if applies) + if (_options.IsCreateAndSave) + { + EndEvent(); + await SaveEventAsync(false, cancellationToken); + _ended = true; + } + else if (_creationPolicy == EventCreationPolicy.InsertOnStartReplaceOnEnd || _creationPolicy == EventCreationPolicy.InsertOnStartInsertOnEnd) + { + await SaveEventAsync(false, cancellationToken); + _saveMode = _creationPolicy == EventCreationPolicy.InsertOnStartReplaceOnEnd ? SaveMode.ReplaceOnEnd : SaveMode.InsertOnEnd; + } + else if (_creationPolicy == EventCreationPolicy.InsertOnEnd) + { + _saveMode = SaveMode.InsertOnEnd; + } + else if (_creationPolicy == EventCreationPolicy.Manual) + { + _saveMode = SaveMode.Manual; + } + return this; + } private bool IsEndedOrDisabled() { if (!_ended && Configuration.AuditDisabled) diff --git a/src/Audit.NET/Configuration.cs b/src/Audit.NET/Configuration.cs index 754eb3d0..36cd2bb8 100644 --- a/src/Audit.NET/Configuration.cs +++ b/src/Audit.NET/Configuration.cs @@ -40,6 +40,12 @@ public static class Configuration /// public static AuditDataProvider DataProvider { get { return DataProviderFactory?.Invoke(); } set { DataProviderFactory = () => value; } } + /// + /// Gets the Default data provider instance as the specified type. Returns null if the data provider is not of the given type. + /// + /// The AuditDataProvider type + public static T DataProviderAs() where T : AuditDataProvider { return DataProviderFactory?.Invoke() as T; } + /// /// Gets or Sets a value that indicates if the logged Type Names should include the namespace. Default is false. /// diff --git a/src/Audit.NET/IAuditScope.cs b/src/Audit.NET/IAuditScope.cs index f129bdad..425a6014 100644 --- a/src/Audit.NET/IAuditScope.cs +++ b/src/Audit.NET/IAuditScope.cs @@ -25,5 +25,6 @@ public interface IAuditScope : IDisposable Task SaveAsync(CancellationToken cancellationToken = default); void SetCustomField(string fieldName, TC value, bool serialize = false); void SetTargetGetter(Func targetGetter); + T EventAs() where T : AuditEvent; } } \ No newline at end of file diff --git a/src/Audit.SignalR/AuditPipelineModule.cs b/src/Audit.SignalR/AuditPipelineModule.cs index 257c7979..26595e85 100644 --- a/src/Audit.SignalR/AuditPipelineModule.cs +++ b/src/Audit.SignalR/AuditPipelineModule.cs @@ -102,7 +102,7 @@ protected override object OnAfterIncoming(object result, IHubIncomingInvokerCont object @return; using (var scope = context.Hub.Context.Request.Environment[AuditScopeIncomingEnvironmentKey] as AuditScope) { - ((scope.Event as AuditEventSignalr).Event as SignalrEventIncoming).Result = result; + (scope.EventAs().Event as SignalrEventIncoming).Result = result; @return = base.OnAfterIncoming(result, context); } context.Hub.Context.Request.Environment.Remove(AuditScopeIncomingEnvironmentKey); @@ -173,7 +173,7 @@ protected override void OnAfterConnect(IHub hub) } using (var scope = hub.Context.Request.Environment[AuditScopeConnectEnvironmentKey] as AuditScope) { - ((scope.Event as AuditEventSignalr).Event as SignalrEventConnect).ConnectionId = hub.Context.ConnectionId; + (scope.EventAs().Event as SignalrEventConnect).ConnectionId = hub.Context.ConnectionId; base.OnAfterConnect(hub); } hub.Context.Request.Environment.Remove(AuditScopeConnectEnvironmentKey); @@ -211,7 +211,7 @@ protected override void OnAfterDisconnect(IHub hub, bool stopCalled) } using (var scope = hub.Context.Request.Environment[AuditScopeDisconnectEnvironmentKey] as AuditScope) { - ((scope.Event as AuditEventSignalr).Event as SignalrEventDisconnect).ConnectionId = hub.Context.ConnectionId; + (scope.EventAs().Event as SignalrEventDisconnect).ConnectionId = hub.Context.ConnectionId; base.OnAfterDisconnect(hub, stopCalled); } hub.Context.Request.Environment.Remove(AuditScopeDisconnectEnvironmentKey); @@ -248,7 +248,7 @@ protected override void OnAfterReconnect(IHub hub) } using (var scope = hub.Context.Request.Environment[AuditScopeReconnectEnvironmentKey] as AuditScope) { - ((scope.Event as AuditEventSignalr).Event as SignalrEventReconnect).ConnectionId = hub.Context.ConnectionId; + (scope.EventAs().Event as SignalrEventReconnect).ConnectionId = hub.Context.ConnectionId; base.OnAfterReconnect(hub); } hub.Context.Request.Environment.Remove(AuditScopeReconnectEnvironmentKey); diff --git a/src/Audit.WCF.Client/AuditMessageInspector.cs b/src/Audit.WCF.Client/AuditMessageInspector.cs index 999b2452..8505a43a 100644 --- a/src/Audit.WCF.Client/AuditMessageInspector.cs +++ b/src/Audit.WCF.Client/AuditMessageInspector.cs @@ -57,7 +57,7 @@ public void AfterReceiveReply(ref Message reply, object correlationState) } // Update the WCF audit event with results - var auditWcfEvent = (auditScope.Event as AuditEventWcfClient).WcfClientEvent; + var auditWcfEvent = auditScope.EventAs().WcfClientEvent; auditWcfEvent.IsFault = reply.IsFault; auditWcfEvent.ResponseAction = reply.Headers?.Action; auditWcfEvent.ResponseBody = reply.ToString(); diff --git a/src/Audit.WCF/AuditOperationInvoker.cs b/src/Audit.WCF/AuditOperationInvoker.cs index 26e331fc..c3c577b1 100644 --- a/src/Audit.WCF/AuditOperationInvoker.cs +++ b/src/Audit.WCF/AuditOperationInvoker.cs @@ -82,13 +82,13 @@ public object Invoke(object instance, object[] inputs, out object[] outputs) AuditBehavior.CurrentAuditScope = null; auditWcfEvent.Fault = GetWcfFaultData(ex); auditWcfEvent.Success = false; - (auditScope.Event as AuditEventWcfAction).WcfEvent = auditWcfEvent; + auditScope.EventAs().WcfEvent = auditWcfEvent; throw; } AuditBehavior.CurrentAuditScope = null; auditWcfEvent.OutputParameters = GetEventElements(outputs); auditWcfEvent.Result = new AuditWcfEventElement(result); - (auditScope.Event as AuditEventWcfAction).WcfEvent = auditWcfEvent; + auditScope.EventAs().WcfEvent = auditWcfEvent; } return result; } @@ -134,7 +134,7 @@ public IAsyncResult InvokeBegin(object instance, object[] inputs, AsyncCallback AuditBehavior.CurrentAuditScope = null; auditWcfEvent.Fault = GetWcfFaultData(ex); auditWcfEvent.Success = false; - (auditScope.Event as AuditEventWcfAction).WcfEvent = auditWcfEvent; + auditScope.EventAs().WcfEvent = auditWcfEvent; auditScope.Dispose(); throw; } @@ -151,7 +151,7 @@ public object InvokeEnd(object instance, out object[] outputs, IAsyncResult resu var auditScopeState = auditAsyncResult.AuditScopeState; object callResult; var auditScope = auditScopeState.AuditScope; - var auditWcfEvent = (auditScope.Event as AuditEventWcfAction).WcfEvent; + var auditWcfEvent = auditScope.EventAs().WcfEvent; try { callResult = _baseInvoker.InvokeEnd(instance, out outputs, auditAsyncResult.OriginalAsyncResult); @@ -161,14 +161,14 @@ public object InvokeEnd(object instance, out object[] outputs, IAsyncResult resu AuditBehavior.CurrentAuditScope = null; auditWcfEvent.Fault = GetWcfFaultData(ex); auditWcfEvent.Success = false; - (auditScope.Event as AuditEventWcfAction).WcfEvent = auditWcfEvent; + auditScope.EventAs().WcfEvent = auditWcfEvent; auditScope.Dispose(); throw; } AuditBehavior.CurrentAuditScope = null; auditWcfEvent.OutputParameters = GetEventElements(outputs); auditWcfEvent.Result = new AuditWcfEventElement(callResult); - (auditScope.Event as AuditEventWcfAction).WcfEvent = auditWcfEvent; + auditScope.EventAs().WcfEvent = auditWcfEvent; auditScope.Dispose(); return callResult; } diff --git a/src/Audit.WebApi/AuditApiAdapter.Core.cs b/src/Audit.WebApi/AuditApiAdapter.Core.cs index 15027d6d..81c46f2d 100644 --- a/src/Audit.WebApi/AuditApiAdapter.Core.cs +++ b/src/Audit.WebApi/AuditApiAdapter.Core.cs @@ -169,7 +169,7 @@ internal async Task AfterExecutedAsync(ActionExecutedContext context, bool inclu } // Replace the Action field - ((AuditEventWebApi)auditScope.Event).Action = auditAction; + auditScope.EventAs().Action = auditAction; // Save, if action was not created by middleware if (!auditAction.IsMiddleware) { diff --git a/src/Audit.WebApi/AuditApiAdapter.cs b/src/Audit.WebApi/AuditApiAdapter.cs index 2a6cb61b..abc56dbe 100644 --- a/src/Audit.WebApi/AuditApiAdapter.cs +++ b/src/Audit.WebApi/AuditApiAdapter.cs @@ -143,7 +143,7 @@ public async Task AfterExecutedAsync(HttpActionExecutedContext actionExecutedCon } // Replace the Action field and save - ((AuditEventWebApi)auditScope.Event).Action = auditAction; + auditScope.EventAs().Action = auditAction; await auditScope.DisposeAsync(); } } diff --git a/src/Audit.WebApi/AuditMiddleware.cs b/src/Audit.WebApi/AuditMiddleware.cs index 359cfd60..c08b9c61 100644 --- a/src/Audit.WebApi/AuditMiddleware.cs +++ b/src/Audit.WebApi/AuditMiddleware.cs @@ -168,7 +168,7 @@ private async Task AfterInvoke(HttpContext context, bool includeResponseBody, bo auditAction.ResponseHeaders = AuditApiHelper.ToDictionary(context.Response.Headers); } // Replace the Action field and save - ((AuditEventWebApi)auditScope.Event).Action = auditAction; + auditScope.EventAs().Action = auditAction; await auditScope.DisposeAsync(); } } diff --git a/src/pack.cmd b/src/pack.cmd index 0c61257c..b4c635f7 100644 --- a/src/pack.cmd +++ b/src/pack.cmd @@ -39,6 +39,7 @@ del "Audit.NET.AmazonQLDB\bin\release\*.nupkg" del "Audit.NET.Kafka\bin\release\*.nupkg" del "Audit.NET.AzureStorageTables\bin\release\*.nupkg" del "Audit.NET.Serilog\bin\release\*.nupkg" +del "Audit.MongoClient\bin\release\*.nupkg" copy ..\docs\Audit.NET.snk .\StrongName\Audit.NET.snk /Y @@ -84,6 +85,8 @@ dotnet pack "Audit.NET.AmazonQLDB/" -c Release dotnet pack "Audit.NET.Kafka/" -c Release dotnet pack "Audit.NET.AzureStorageTables/" -c Release dotnet pack "Audit.NET.Serilog/" -c Release +dotnet pack "Audit.MongoClient/" -c Release + ECHO. ECHO ADD TAG NOW ! diff --git a/src/push.cmd b/src/push.cmd index 4dce9754..bb93b6f9 100644 --- a/src/push.cmd +++ b/src/push.cmd @@ -39,6 +39,7 @@ del "Audit.NET.AmazonQLDB\bin\release\*.symbols.nupkg" del "Audit.NET.Kafka\bin\release\*.symbols.nupkg" del "Audit.NET.AzureStorageTables\bin\release\*.symbols.nupkg" del "Audit.NET.Serilog\bin\release\*.symbols.nupkg" +del "Audit.MongoClient\bin\release\*.symbols.nupkg" nuget push "audit.net\bin\release\*.nupkg" -NoSymbols -source %1 nuget push "Audit.NET.JsonNewtonsoftAdapter\bin\release\*.nupkg" -NoSymbols -source %1 @@ -77,4 +78,5 @@ nuget push "Audit.NET.NLog\bin\release\*.nupkg" -NoSymbols -source %1 nuget push "Audit.NET.AmazonQLDB\bin\release\*.nupkg" -NoSymbols -source %1 nuget push "Audit.NET.Kafka\bin\release\*.nupkg" -NoSymbols -source %1 nuget push "Audit.NET.AzureStorageTables\bin\release\*.nupkg" -NoSymbols -source %1 -nuget push "Audit.NET.Serilog\bin\release\*.nupkg" -NoSymbols -source %1 \ No newline at end of file +nuget push "Audit.NET.Serilog\bin\release\*.nupkg" -NoSymbols -source %1 +nuget push "Audit.MongoClient\bin\release\*.nupkg" -NoSymbols -source %1 \ No newline at end of file diff --git a/test/Audit.EntityFramework.Core.UnitTest/EfChangeTrackerTests.cs b/test/Audit.EntityFramework.Core.UnitTest/EfChangeTrackerTests.cs index bda960cb..0b6e776e 100644 --- a/test/Audit.EntityFramework.Core.UnitTest/EfChangeTrackerTests.cs +++ b/test/Audit.EntityFramework.Core.UnitTest/EfChangeTrackerTests.cs @@ -78,7 +78,7 @@ public async Task EF_Update_ReloadFromDatabase() .UseInMemoryProvider() .WithCreationPolicy(EventCreationPolicy.InsertOnEnd); - var evs = ((InMemoryDataProvider)Audit.Core.Configuration.DataProvider).GetAllEvents(); + var evs = Audit.Core.Configuration.DataProviderAs().GetAllEvents(); var car = new SimpleMemoryContext.Car() { diff --git a/test/Audit.EntityFramework.Full.UnitTest/EfChangeTrackerTests.cs b/test/Audit.EntityFramework.Full.UnitTest/EfChangeTrackerTests.cs index 1cac8d51..5ce991f8 100644 --- a/test/Audit.EntityFramework.Full.UnitTest/EfChangeTrackerTests.cs +++ b/test/Audit.EntityFramework.Full.UnitTest/EfChangeTrackerTests.cs @@ -80,7 +80,7 @@ public async Task EF_Update_ReloadFromDatabase() .UseInMemoryProvider() .WithCreationPolicy(EventCreationPolicy.InsertOnEnd); - var evs = ((InMemoryDataProvider)Audit.Core.Configuration.DataProvider).GetAllEvents(); + var evs = Audit.Core.Configuration.DataProviderAs().GetAllEvents(); var car = new SimpleContext.Car() { diff --git a/test/Audit.IntegrationTest/PostgreSqlTests.cs b/test/Audit.IntegrationTest/PostgreSqlTests.cs index 90ea4ef5..db2d4b7f 100644 --- a/test/Audit.IntegrationTest/PostgreSqlTests.cs +++ b/test/Audit.IntegrationTest/PostgreSqlTests.cs @@ -139,7 +139,7 @@ private static CustomPostgreSqlDataProvider GetConfiguredPostgreSqlDataProvider( .WithCreationPolicy(EventCreationPolicy.InsertOnEnd) .ResetActions(); - var dp = (CustomPostgreSqlDataProvider)Configuration.DataProvider; + var dp = Configuration.DataProviderAs(); return dp; } } diff --git a/test/Audit.MongoClient.UnitTest/Audit.MongoClient.UnitTest.csproj b/test/Audit.MongoClient.UnitTest/Audit.MongoClient.UnitTest.csproj new file mode 100644 index 00000000..8cc4703b --- /dev/null +++ b/test/Audit.MongoClient.UnitTest/Audit.MongoClient.UnitTest.csproj @@ -0,0 +1,37 @@ + + + + net472;netcoreapp2.0;net8.0 + false + true + $(DefineConstants);STRONG_NAME + true + Full + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Audit.MongoClient.UnitTest/AuditMongoClientIntegrationTests.cs b/test/Audit.MongoClient.UnitTest/AuditMongoClientIntegrationTests.cs new file mode 100644 index 00000000..5c824a19 --- /dev/null +++ b/test/Audit.MongoClient.UnitTest/AuditMongoClientIntegrationTests.cs @@ -0,0 +1,65 @@ +using Audit.Core.Providers; +using MongoDB.Bson; +using MongoDB.Driver; +using NUnit.Framework; +using System; + +namespace Audit.MongoClient.UnitTest +{ + [TestFixture] + [Category("Integration")] + public class AuditMongoClientIntegrationTests + { + [SetUp] + public void Setup() + { + Audit.Core.Configuration.Reset(); + } + + [Test] + public void AuditMongo_Integration() + { + // Arrange + Audit.Core.Configuration.Setup().UseInMemoryProvider(); + + var eventType = Guid.NewGuid().ToString(); + + var mongoSettings = new MongoClientSettings() + { + Server = new MongoServerAddress("localhost", 27017), + ClusterConfigurator = cc => cc + // Add the audit subscriber to the cluster config + .AddAuditSubscriber(auditConfig => auditConfig + .EventType(eventType) + .IncludeReply()) + }; + + var client = new MongoDB.Driver.MongoClient(mongoSettings); + + var collection = client.GetDatabase("AuditTest").GetCollection("MongoClient"); + + // Act + var bson = new { test = new string('z', 40000) }.ToBsonDocument(); + + // Insert + collection.InsertOne(bson); + + // Find + var filterById = Builders.Filter.Eq("_id", (BsonObjectId)bson["_id"]); + var item = collection.Find(filterById).FirstOrDefault(); + + // Delete + collection.DeleteOne(filterById); + + // Assert + var evs = Audit.Core.Configuration.DataProviderAs().GetAllEventsOfType(); + + // Assert + Assert.That(evs, Is.Not.Null); + Assert.That(evs.Count, Is.EqualTo(3)); + Assert.That(evs[0].Command.CommandName, Is.EqualTo("insert")); + Assert.That(evs[1].Command.CommandName, Is.EqualTo("find")); + Assert.That(evs[2].Command.CommandName, Is.EqualTo("delete")); + } + } +} diff --git a/test/Audit.MongoClient.UnitTest/MongoAuditEventSubscriberTests.cs b/test/Audit.MongoClient.UnitTest/MongoAuditEventSubscriberTests.cs new file mode 100644 index 00000000..3e39957e --- /dev/null +++ b/test/Audit.MongoClient.UnitTest/MongoAuditEventSubscriberTests.cs @@ -0,0 +1,311 @@ +using System; +using System.Net; +using Audit.Core; +using Audit.Core.Providers; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Core.Clusters; +using MongoDB.Driver.Core.Connections; +using MongoDB.Driver.Core.Events; +using MongoDB.Driver.Core.Servers; +using Moq; +using NUnit.Framework; + +namespace Audit.MongoClient.UnitTest +{ + [TestFixture] + public class MongoAuditEventSubscriberTests + { + private readonly ConnectionId _connectionId = new ConnectionId(new ServerId(new ClusterId(10), new IPEndPoint(IPAddress.Parse("192.168.1.100"), 81)), 20); + + [SetUp] + public void Setup() + { + Audit.Core.Configuration.Reset(); + } + + [Test] + public void Test_Command_Succeeded() + { + // Arrange + Audit.Core.Configuration.Setup().UseInMemoryProvider(); + + var sut = new MongoAuditEventSubscriber(cfg => cfg.IncludeReply(false)) + { + IncludeReply = true + }; + + var testCommandName = $"name-{Guid.NewGuid()}"; + var testCommandValue = $"value-{Guid.NewGuid()}"; + var replyTest = $"reply-{Guid.NewGuid()}"; + var requestId = new Random().Next(); + + var cmdStart = new CommandStartedEvent(testCommandName, new { cmd = testCommandValue }.ToBsonDocument(), DatabaseNamespace.Admin, operationId: 1, requestId, _connectionId); + var cmdSuccess = new CommandSucceededEvent(testCommandName, new { value = replyTest }.ToBsonDocument(), DatabaseNamespace.Admin, 1, requestId, _connectionId, TimeSpan.FromSeconds(1)); + + // Act + sut.Handle(cmdStart); + sut.Handle(cmdSuccess); + + // Assert + var evs = Audit.Core.Configuration.DataProviderAs().GetAllEventsOfType(); + + Assert.That(evs.Count, Is.EqualTo(1)); + Assert.That(evs[0].Command.CommandName, Is.EqualTo(cmdStart.CommandName)); + Assert.That(evs[0].Command.Success, Is.True); + Assert.That(evs[0].Command.CommandName, Is.EqualTo(cmdStart.CommandName)); + Assert.That(evs[0].Command.GetCommandStartedEvent(), Is.Not.Null); + Assert.That(evs[0].Command.GetCommandStartedEvent().CommandName, Is.EqualTo(cmdStart.CommandName)); + Assert.That(evs[0].Command.Duration, Is.EqualTo(1000)); + Assert.That(evs[0].Command.Error, Is.Null); + Assert.That(evs[0].Command.OperationId, Is.EqualTo(1)); + Assert.That(evs[0].Command.Reply, Is.Not.Null); + Assert.That(((dynamic)evs[0].Command.Reply)["value"], Is.EqualTo(replyTest)); + Assert.That(evs[0].Command.RequestId, Is.EqualTo(requestId)); + Assert.That(evs[0].Command.Body, Is.Not.Null); + Assert.That(((dynamic)evs[0].Command.Body)["cmd"], Is.EqualTo(testCommandValue)); + Assert.That(evs[0].Command.Connection.ClusterId, Is.EqualTo(10)); + Assert.That(evs[0].Command.Connection.Endpoint, Is.EqualTo(_connectionId.ServerId.EndPoint.ToString())); + Assert.That(evs[0].Command.Connection.LocalConnectionId, Is.EqualTo(_connectionId.LongLocalValue)); + } + + [Test] + public void Test_Command_Failed() + { + // Arrange + Audit.Core.Configuration.Setup().UseInMemoryProvider(); + + var sut = new MongoAuditEventSubscriber() + { + IncludeReply = true + }; + + var cnnId = new ConnectionId(new ServerId(new ClusterId(10), new IPEndPoint(IPAddress.Parse("192.168.1.100"), 81)), 20); + var testCommandName = $"name-{Guid.NewGuid()}"; + var testCommandValue = $"value-{Guid.NewGuid()}"; + var requestId = new Random().Next(); + + var cmdStart = new CommandStartedEvent(testCommandName, new { cmd = testCommandValue }.ToBsonDocument(), DatabaseNamespace.Admin, operationId: 1, requestId, cnnId); + + var exception = new MongoCommandException(cnnId, "test-exception", cmdStart.Command); + var cmdFailed = new CommandFailedEvent(testCommandName, DatabaseNamespace.Admin, exception, 1, requestId, cnnId, TimeSpan.FromSeconds(1)); + + // Act + sut.Handle(cmdStart); + sut.Handle(cmdFailed); + + // Assert + var evs = Audit.Core.Configuration.DataProviderAs().GetAllEventsOfType(); + + Assert.That(evs.Count, Is.EqualTo(1)); + Assert.That(evs[0].Command.CommandName, Is.EqualTo(cmdStart.CommandName)); + Assert.That(evs[0].Command.Success, Is.False); + Assert.That(evs[0].Command.CommandName, Is.EqualTo(cmdStart.CommandName)); + Assert.That(evs[0].Command.GetCommandStartedEvent(), Is.Not.Null); + Assert.That(evs[0].Command.GetCommandStartedEvent().CommandName, Is.EqualTo(cmdStart.CommandName)); + Assert.That(evs[0].Command.Duration, Is.EqualTo(1000)); + Assert.That(evs[0].Command.Error, Is.Not.Null); + Assert.That(evs[0].Command.Error, Contains.Substring("test-exception")); + Assert.That(evs[0].Command.OperationId, Is.EqualTo(1)); + Assert.That(evs[0].Command.Reply, Is.Null); + Assert.That(evs[0].Command.RequestId, Is.EqualTo(requestId)); + Assert.That(evs[0].Command.Body, Is.Not.Null); + Assert.That(((dynamic)evs[0].Command.Body)["cmd"], Is.EqualTo(testCommandValue)); + Assert.That(evs[0].Command.Connection.ClusterId, Is.EqualTo(10)); + Assert.That(evs[0].Command.Connection.Endpoint, Is.EqualTo(_connectionId.ServerId.EndPoint.ToString())); + Assert.That(evs[0].Command.Connection.LocalConnectionId, Is.EqualTo(_connectionId.LongLocalValue)); + } + + [TestCase(EventCreationPolicy.InsertOnEnd, 1)] + [TestCase(EventCreationPolicy.InsertOnStartInsertOnEnd, 2)] + [TestCase(EventCreationPolicy.InsertOnStartReplaceOnEnd, 1)] + [TestCase(EventCreationPolicy.Manual, 0)] + public void Test_Creation_Policy(EventCreationPolicy policy, int expectedEventsCount) + { + // Arrange + Audit.Core.Configuration.Setup().UseInMemoryProvider(); + + var sut = new MongoAuditEventSubscriber() + { + CreationPolicy = policy + }; + + var requestId = new Random().Next(); + + var cmdStart = new CommandStartedEvent("test", new { cmd = 1 }.ToBsonDocument(), DatabaseNamespace.Admin, 1, requestId, _connectionId); + var cmdSuccess = new CommandSucceededEvent("test", new { value = 1 }.ToBsonDocument(), DatabaseNamespace.Admin, 1, requestId, _connectionId, TimeSpan.FromSeconds(1)); + + // Act + sut.Handle(cmdStart); + sut.Handle(cmdSuccess); + + // Assert + var evs = Audit.Core.Configuration.DataProviderAs().GetAllEventsOfType(); + + Assert.That(evs, Is.Not.Null); + Assert.That(evs.Count, Is.EqualTo(expectedEventsCount)); + if (expectedEventsCount > 0) + { + Assert.That(evs[0].Command.RequestId, Is.EqualTo(requestId)); + } + } + + [Test] + public void Test_Custom_EventTypeName() + { + // Arrange + Audit.Core.Configuration.Setup().UseNullProvider(); + + var commandName = Guid.NewGuid().ToString(); + + var sut = new MongoAuditEventSubscriber() + { + EventType = "test-{command}" + }; + + var requestId = new Random().Next(); + var cmdStart = new CommandStartedEvent(commandName, new { cmd = 1 }.ToBsonDocument(), DatabaseNamespace.Admin, 1, requestId, _connectionId); + + // Act + sut.Handle(cmdStart); + + // Assert + Assert.That(sut._requestBuffer[requestId].Event.EventType, Is.EqualTo($"test-{commandName}")); + } + + [Test] + public void Test_Custom_AuditDataProvider() + { + // Arrange + Audit.Core.Configuration.Setup().UseNullProvider(); + + var commandName = Guid.NewGuid().ToString(); + + var sut = new MongoAuditEventSubscriber() + { + AuditDataProvider = new InMemoryDataProvider() + }; + + var requestId = new Random().Next(); + var cmdStart = new CommandStartedEvent(commandName, new { cmd = 1 }.ToBsonDocument(), DatabaseNamespace.Admin, 1, requestId, _connectionId); + + // Act + sut.Handle(cmdStart); + + // Assert + Assert.That(sut._requestBuffer[requestId].DataProvider, Is.InstanceOf()); + } + + [Test] + public void Test_Custom_AuditScopeFactory() + { + // Arrange + Audit.Core.Configuration.Setup().UseNullProvider(); + + var eventType = Guid.NewGuid().ToString(); + var commandName = Guid.NewGuid().ToString(); + + var mockFactory = new Mock(MockBehavior.Strict); + mockFactory.Setup(x => x.Create(It.IsAny())) + .Returns(AuditScope.Create(new AuditScopeOptions(eventType))); + + var sut = new MongoAuditEventSubscriber() + { + AuditScopeFactory = mockFactory.Object + }; + + var requestId = new Random().Next(); + var cmdStart = new CommandStartedEvent(commandName, new { cmd = 1 }.ToBsonDocument(), DatabaseNamespace.Admin, 1, requestId, _connectionId); + + // Act + sut.Handle(cmdStart); + + // Assert + Assert.That(sut._requestBuffer[requestId].EventType, Is.EqualTo(eventType)); + mockFactory.Verify(x => x.Create(It.IsAny()), Times.Once); + } + + [Test] + public void Test_Custom_CommandFilter() + { + // Arrange + Audit.Core.Configuration.Setup().UseInMemoryProvider(); + + var sut = new MongoAuditEventSubscriber() + { + CommandFilter = cmd => cmd.CommandName == "delete" + }; + + var commandName_1 = Guid.NewGuid().ToString(); + var commandName_2 = "delete"; + + var cmdStart_1 = new CommandStartedEvent(commandName_1, new { cmd = 10 }.ToBsonDocument(), DatabaseNamespace.Admin, 1, 2, _connectionId); + var cmdStart_2 = new CommandStartedEvent(commandName_2, new { cmd = 20 }.ToBsonDocument(), DatabaseNamespace.Admin, 3, 4, _connectionId); + var cmdSuccess_1 = new CommandSucceededEvent(commandName_1, new { value = 1 }.ToBsonDocument(), DatabaseNamespace.Admin, 1, 2, _connectionId, TimeSpan.FromSeconds(1)); + var cmdSuccess_2 = new CommandSucceededEvent(commandName_2, new { value = 2 }.ToBsonDocument(), DatabaseNamespace.Admin, 3, 4, _connectionId, TimeSpan.FromSeconds(1)); + + // Act + sut.Handle(cmdStart_1); + sut.Handle(cmdStart_2); + sut.Handle(cmdSuccess_1); + sut.Handle(cmdSuccess_2); + + // Assert + var evs = Audit.Core.Configuration.DataProviderAs().GetAllEventsOfType(); + Assert.That(evs, Is.Not.Null); + Assert.That(evs.Count, Is.EqualTo(1)); + Assert.That(evs[0].Command.CommandName, Is.EqualTo("delete")); + } + + [Test] + public void Test_IgnoredCommands() + { + // Arrange + Audit.Core.Configuration.Setup().UseInMemoryProvider(); + + var ignoredCommands = new[] { "isMaster", "buildInfo", "getLastError", "saslStart", "saslContinue" }; + + var sut = new MongoAuditEventSubscriber(); + + // Act + foreach (var ignoredCommandName in ignoredCommands) + { + var cmdStart = new CommandStartedEvent(ignoredCommandName, new { cmd = 10 }.ToBsonDocument(), DatabaseNamespace.Admin, 1, 2, _connectionId); + var cmdFailed = new CommandFailedEvent(ignoredCommandName, DatabaseNamespace.Admin, new Exception(), 1, 2, _connectionId, TimeSpan.FromSeconds(1)); + var cmdSuccess = new CommandSucceededEvent(ignoredCommandName, new { value = 1 }.ToBsonDocument(), DatabaseNamespace.Admin, 1, 2, _connectionId, TimeSpan.FromSeconds(1)); + + sut.Handle(cmdStart); + sut.Handle(cmdFailed); + sut.Handle(cmdSuccess); + } + + // Assert + var evs = Audit.Core.Configuration.DataProviderAs().GetAllEventsOfType(); + Assert.That(evs, Is.Not.Null); + Assert.That(evs.Count, Is.EqualTo(0)); + } + + [Test] + public void Test_AuditDisabled() + { + // Arrange + Audit.Core.Configuration.Setup().UseInMemoryProvider(); + Audit.Core.Configuration.AuditDisabled = true; + var sut = new MongoAuditEventSubscriber(); + + var cmdStart = new CommandStartedEvent("insert", new { cmd = 10 }.ToBsonDocument(), DatabaseNamespace.Admin, 1, 2, _connectionId); + var cmdFailed = new CommandFailedEvent("insert", DatabaseNamespace.Admin, new Exception(), 1, 2, _connectionId, TimeSpan.FromSeconds(1)); + var cmdSuccess = new CommandSucceededEvent("insert", new { value = 1 }.ToBsonDocument(), DatabaseNamespace.Admin, 1, 2, _connectionId, TimeSpan.FromSeconds(1)); + + // Act + sut.Handle(cmdStart); + sut.Handle(cmdFailed); + sut.Handle(cmdSuccess); + + // Assert + var evs = Audit.Core.Configuration.DataProviderAs().GetAllEventsOfType(); + Assert.That(evs, Is.Not.Null); + Assert.That(evs.Count, Is.EqualTo(0)); + } + } +} \ No newline at end of file diff --git a/test/Audit.RavenDB.UnitTest/RavenDbDataProviderTests.cs b/test/Audit.RavenDB.UnitTest/RavenDbDataProviderTests.cs index 4b46fc7d..38647f86 100644 --- a/test/Audit.RavenDB.UnitTest/RavenDbDataProviderTests.cs +++ b/test/Audit.RavenDB.UnitTest/RavenDbDataProviderTests.cs @@ -169,7 +169,7 @@ public void Test_RavenDbDataProvider_TextJson_WithNewtonAdapter() .DatabaseDefault(databaseName))) .WithCreationPolicy(EventCreationPolicy.InsertOnEnd); - var rdb = (RavenDbDataProvider)Configuration.DataProvider; + var rdb = Configuration.DataProviderAs(); TryCreateDatabase(rdb.DocumentStore, databaseName); var target = new DailyTemperature() { LowTemp = 20, HighTemp = 100 }; diff --git a/test/Audit.SqlServer.UnitTest/SqlServerTests.cs b/test/Audit.SqlServer.UnitTest/SqlServerTests.cs index 41773c05..4a056e05 100644 --- a/test/Audit.SqlServer.UnitTest/SqlServerTests.cs +++ b/test/Audit.SqlServer.UnitTest/SqlServerTests.cs @@ -45,7 +45,7 @@ public void Test_SqlServer_Provider() var guid = Guid.NewGuid(); AuditScope.Log(nameof(Test_SqlServer_Provider), new { guid }); - var sqlDp = (SqlDataProvider)Audit.Core.Configuration.DataProvider; + var sqlDp = Audit.Core.Configuration.DataProviderAs(); Assert.AreEqual(1, ids.Count); @@ -82,7 +82,7 @@ public void Test_SqlServer_Provider_GuardCondition() scope.SetCustomField("field", "final"); } - var sqlDp = (SqlDataProvider)Audit.Core.Configuration.DataProvider; + var sqlDp = Audit.Core.Configuration.DataProviderAs(); Assert.AreEqual(2, ids.Count); Assert.AreEqual(ids[0], ids[1]); diff --git a/test/Audit.UnitTest/InMemoryDataProviderTests.cs b/test/Audit.UnitTest/InMemoryDataProviderTests.cs index d03c5410..ced7429f 100644 --- a/test/Audit.UnitTest/InMemoryDataProviderTests.cs +++ b/test/Audit.UnitTest/InMemoryDataProviderTests.cs @@ -34,7 +34,7 @@ public void Test_InMemoryDataProvider(EventCreationPolicy creationPolicy) target.Add("final"); } - var dp = (InMemoryDataProvider)Configuration.DataProvider; + var dp = Configuration.DataProviderAs(); var event0 = dp.GetEvent(0); var allEvents = dp.GetAllEvents(); var allCustomEvents = dp.GetAllEventsOfType(); @@ -81,7 +81,7 @@ public async Task Test_InMemoryDataProviderAsync(EventCreationPolicy creationPol target.Add("final"); } - var dp = (InMemoryDataProvider)Configuration.DataProvider; + var dp = Configuration.DataProviderAs(); var event0 = await dp.GetEventAsync(0); var allEvents = dp.GetAllEvents(); var allCustomEvents = dp.GetAllEventsOfType(); diff --git a/test/Run-Tests.ps1 b/test/Run-Tests.ps1 index 046f4c3a..46a9194f 100644 --- a/test/Run-Tests.ps1 +++ b/test/Run-Tests.ps1 @@ -30,6 +30,7 @@ StartDotnetUnitTests 'Audit.RavenDB.UnitTest' 'RavenDB'; StartDotnetUnitTests 'Audit.SqlServer.UnitTest' 'SqlServer'; StartDotnetUnitTests 'Audit.IntegrationTest' 'Integration' '--filter=TestCategory!=AzureDocDb&TestCategory!=AzureBlob&TestCategory!=AzureStorageBlobs&TestCategory!=WCF&TestCategory!=Elasticsearch&TestCategory!=Dynamo&TestCategory!=PostgreSQL&TestCategory!=Kafka&TestCategory!=AmazonQLDB&TestCategory!=Mongo&TestCategory!=MySql'; StartDotnetUnitTests 'Audit.IntegrationTest' 'Mongo' '--filter=TestCategory=Mongo'; +StartDotnetUnitTests 'Audit.MongoClient.UnitTest' 'MongoClient'; StartDotnetUnitTests 'Audit.IntegrationTest' 'MySql' '--filter=TestCategory=MySql'; StartDotnetUnitTests 'Audit.IntegrationTest' 'PostgreSQL' '--filter=TestCategory=PostgreSQL'; StartDotnetUnitTests 'Audit.IntegrationTest' 'AzureDocDb' '--filter=TestCategory=AzureDocDb'; diff --git a/test/testns.bat b/test/testns.bat index 2f2b2bbe..e6569d0b 100644 --- a/test/testns.bat +++ b/test/testns.bat @@ -156,6 +156,11 @@ dotnet test --logger:"console;verbosity=normal" cd .. +echo ---------------------------------------------- RUNNING MONGO CLIENT INTEGRATION TEST (21) ---------------------------------------------- +TITLE RUNNING MONGO CLIENT INTEGRATION TEST (21) +cd Audit.MongoClient.UnitTest +dotnet test --logger:"console;verbosity=normal" + title Audit.NET Unit Tests Runner ECHO.