diff --git a/src/Rsk.DuendeIdentityServer.AuditEventSink/AdapterFactory.cs b/src/Rsk.DuendeIdentityServer.AuditEventSink/AdapterFactory.cs index cee2b83..1491666 100644 --- a/src/Rsk.DuendeIdentityServer.AuditEventSink/AdapterFactory.cs +++ b/src/Rsk.DuendeIdentityServer.AuditEventSink/AdapterFactory.cs @@ -1,4 +1,6 @@ -using Duende.IdentityServer.Events; +using System; +using System.Collections.Generic; +using Duende.IdentityServer.Events; using RSK.Audit; using Rsk.DuendeIdentityServer.AuditEventSink.Adapters; @@ -6,60 +8,59 @@ namespace Rsk.DuendeIdentityServer.AuditEventSink { public class AdapterFactory : IAdapterFactory { + private readonly Dictionary> eventAdapters; + + public AdapterFactory(IDictionary> customEventAdapters = null) + { + eventAdapters = CreateDefaultEventAdapters(); + + if (customEventAdapters == null) return; + + foreach (var mapping in customEventAdapters) + { + eventAdapters[mapping.Key] = mapping.Value; + } + } + public IAuditEventArguments Create(Event evt) { - if (evt != null) + if (evt == null) { - switch (evt) - { - case TokenIssuedSuccessEvent e: - return new TokenIssuedSuccessEventAdapter(e); - case UserLoginSuccessEvent e: - return new UserLoginSuccessEventAdapter(e); - case UserLoginFailureEvent e: - return new UserLoginFailureEventAdapter(e); - case UserLogoutSuccessEvent e: - return new UserLogoutSuccessEventAdapter(e); - case ConsentGrantedEvent e: - return new ConsentGrantedEventAdapter(e); - case ConsentDeniedEvent e: - return new ConsentDeniedEventAdapter(e); - case TokenIssuedFailureEvent e: - return new TokenIssuedFailureEventAdapter(e); - case GrantsRevokedEvent e: - return new GrantsRevokedEventAdapter(e); - case DeviceAuthorizationFailureEvent e: - return new DeviceAuthorizationFailureEventAdapter(e); - case DeviceAuthorizationSuccessEvent e: - return new DeviceAuthorizationSuccessEventAdapter(e); - case TokenRevokedSuccessEvent e: - return new TokenRevokedSuccessEventAdapter(e); - case InvalidClientConfigurationEvent e: - return new InvalidClientConfigurationEventAdapter(e); - case TokenIntrospectionFailureEvent e: - return new TokenIntrospectionFailureEventAdapter(e); - case TokenIntrospectionSuccessEvent e: - return new TokenIntrospectionSuccessEventAdapter(e); - case ClientAuthenticationFailureEvent e: - return new ClientAuthenticationFailureEventAdapter(e); - case ClientAuthenticationSuccessEvent e: - return new ClientAuthenticationSuccessEventAdapter(e); - case ApiAuthenticationFailureEvent e: - return new ApiAuthenticationFailureEventAdapter(e); - case ApiAuthenticationSuccessEvent e: - return new ApiAuthenticationSuccessEventAdapter(e); - case UnhandledExceptionEvent e: - return new UnhandledExceptionEventAdapter(e); - case BackchannelAuthenticationSuccessEvent e: - return new BackchannelAuthenticationSuccessEventAdapter(e); - case BackchannelAuthenticationFailureEvent e: - return new BackchannelAuthenticationFailureEventAdapter(e); - case InvalidIdentityProviderConfiguration e: - return new InvalidIdentityProviderConfigurationAdapter(e); - } + return null; } - return null; + return eventAdapters.TryGetValue(evt.GetType(), out var adapterFactory) + ? adapterFactory(evt) + : null; + } + + private static Dictionary> CreateDefaultEventAdapters() + { + return new Dictionary> + { + [typeof(TokenIssuedSuccessEvent)] = e => new TokenIssuedSuccessEventAdapter((TokenIssuedSuccessEvent)e), + [typeof(UserLoginSuccessEvent)] = e => new UserLoginSuccessEventAdapter((UserLoginSuccessEvent)e), + [typeof(UserLoginFailureEvent)] = e => new UserLoginFailureEventAdapter((UserLoginFailureEvent)e), + [typeof(UserLogoutSuccessEvent)] = e => new UserLogoutSuccessEventAdapter((UserLogoutSuccessEvent)e), + [typeof(ConsentGrantedEvent)] = e => new ConsentGrantedEventAdapter((ConsentGrantedEvent)e), + [typeof(ConsentDeniedEvent)] = e => new ConsentDeniedEventAdapter((ConsentDeniedEvent)e), + [typeof(TokenIssuedFailureEvent)] = e => new TokenIssuedFailureEventAdapter((TokenIssuedFailureEvent)e), + [typeof(GrantsRevokedEvent)] = e => new GrantsRevokedEventAdapter((GrantsRevokedEvent)e), + [typeof(DeviceAuthorizationFailureEvent)] = e => new DeviceAuthorizationFailureEventAdapter((DeviceAuthorizationFailureEvent)e), + [typeof(DeviceAuthorizationSuccessEvent)] = e => new DeviceAuthorizationSuccessEventAdapter((DeviceAuthorizationSuccessEvent)e), + [typeof(TokenRevokedSuccessEvent)] = e => new TokenRevokedSuccessEventAdapter((TokenRevokedSuccessEvent)e), + [typeof(InvalidClientConfigurationEvent)] = e => new InvalidClientConfigurationEventAdapter((InvalidClientConfigurationEvent)e), + [typeof(TokenIntrospectionFailureEvent)] = e => new TokenIntrospectionFailureEventAdapter((TokenIntrospectionFailureEvent)e), + [typeof(TokenIntrospectionSuccessEvent)] = e => new TokenIntrospectionSuccessEventAdapter((TokenIntrospectionSuccessEvent)e), + [typeof(ClientAuthenticationFailureEvent)] = e => new ClientAuthenticationFailureEventAdapter((ClientAuthenticationFailureEvent)e), + [typeof(ClientAuthenticationSuccessEvent)] = e => new ClientAuthenticationSuccessEventAdapter((ClientAuthenticationSuccessEvent)e), + [typeof(ApiAuthenticationFailureEvent)] = e => new ApiAuthenticationFailureEventAdapter((ApiAuthenticationFailureEvent)e), + [typeof(ApiAuthenticationSuccessEvent)] = e => new ApiAuthenticationSuccessEventAdapter((ApiAuthenticationSuccessEvent)e), + [typeof(UnhandledExceptionEvent)] = e => new UnhandledExceptionEventAdapter((UnhandledExceptionEvent)e), + [typeof(BackchannelAuthenticationSuccessEvent)] = e => new BackchannelAuthenticationSuccessEventAdapter((BackchannelAuthenticationSuccessEvent)e), + [typeof(BackchannelAuthenticationFailureEvent)] = e => new BackchannelAuthenticationFailureEventAdapter((BackchannelAuthenticationFailureEvent)e), + [typeof(InvalidIdentityProviderConfiguration)] = e => new InvalidIdentityProviderConfigurationAdapter((InvalidIdentityProviderConfiguration)e) + }; } } } diff --git a/src/Rsk.DuendeIdentityServer.AuditEventSink/AuditSink.cs b/src/Rsk.DuendeIdentityServer.AuditEventSink/AuditSink.cs index 9cdff55..5e36457 100644 --- a/src/Rsk.DuendeIdentityServer.AuditEventSink/AuditSink.cs +++ b/src/Rsk.DuendeIdentityServer.AuditEventSink/AuditSink.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading.Tasks; using Duende.IdentityServer.Events; @@ -7,34 +8,31 @@ [assembly:InternalsVisibleTo("RSK.DuendeIdentityServer.AuditEventSink.Tests")] -namespace Rsk.DuendeIdentityServer.AuditEventSink +namespace Rsk.DuendeIdentityServer.AuditEventSink; + +public class AuditSink( + IRecordAuditableActions auditRecorder, + IDictionary> customEventAdapters = null) + : IEventSink { - public class AuditSink : IEventSink - { - private readonly IRecordAuditableActions auditRecorder; + private readonly IRecordAuditableActions auditRecorder = auditRecorder ?? throw new ArgumentNullException(); - internal IAdapterFactory Factory { get; set; } = new AdapterFactory(); + internal IAdapterFactory Factory { get; init; } = new AdapterFactory(customEventAdapters); - public AuditSink(IRecordAuditableActions auditRecorder) - { - this.auditRecorder = auditRecorder ?? throw new ArgumentNullException(); - } + public Task PersistAsync(Event evt) + { + var auditArgument = Factory.Create(evt); - public Task PersistAsync(Event evt) + if (auditArgument != null) { - var auditArgument = Factory.Create(evt); - - if (auditArgument != null) + if (evt.EventType == EventTypes.Success || evt.EventType == EventTypes.Information) { - if (evt.EventType == EventTypes.Success || evt.EventType == EventTypes.Information) - { - return auditRecorder.RecordSuccess(auditArgument); - } - - return auditRecorder.RecordFailure(auditArgument); + return auditRecorder.RecordSuccess(auditArgument); } - return Task.CompletedTask; + return auditRecorder.RecordFailure(auditArgument); } + + return Task.CompletedTask; } -} +} \ No newline at end of file diff --git a/src/Rsk.DuendeIdentityServer.AuditEventSink/Rsk.DuendeIdentityServer.AuditEventSink.csproj b/src/Rsk.DuendeIdentityServer.AuditEventSink/Rsk.DuendeIdentityServer.AuditEventSink.csproj index 8225ba2..d0d49b1 100644 --- a/src/Rsk.DuendeIdentityServer.AuditEventSink/Rsk.DuendeIdentityServer.AuditEventSink.csproj +++ b/src/Rsk.DuendeIdentityServer.AuditEventSink/Rsk.DuendeIdentityServer.AuditEventSink.csproj @@ -5,13 +5,13 @@ Rock Solid Knowledge Ltd Duende IdentityServer event sink to add audit records into AdminUI auditing https://github.com/RockSolidKnowledge/RSK.IdentityServer4.AuditEventSink - Upgrade to .NET 10 - Copyright 2022 (c) Rock Solid Knowledge Ltd. All rights reserved + Add event extensibility + Copyright 2026 (c) Rock Solid Knowledge Ltd. All rights reserved Audit AdminUI IdentityServer Events true icon.png Apache-2.0 - 4.0.0 + 4.1.0 diff --git a/tests/Rsk.DuendeIdentityServer.AuditEventSink.Tests/AuditSinkTests.cs b/tests/Rsk.DuendeIdentityServer.AuditEventSink.Tests/AuditSinkTests.cs index de8418e..42abfb9 100644 --- a/tests/Rsk.DuendeIdentityServer.AuditEventSink.Tests/AuditSinkTests.cs +++ b/tests/Rsk.DuendeIdentityServer.AuditEventSink.Tests/AuditSinkTests.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; using Duende.IdentityServer.Events; using Moq; using RSK.Audit; @@ -88,11 +90,39 @@ public async Task PersistAsync_WhenFailureEvent_WillCallFailureAuditRecord() recorder.Verify(x => x.RecordFailure(It.IsAny()), Times.Once); } + [Fact] + public async Task PersistAsync_WhenCustomMappingIsProvided_WillUseCustomAdapter() + { + // Arrange + var recorder = new Mock(); + var customAuditEventArguments = new Mock().Object; + var customMappings = new Dictionary> + { + [typeof(CustomStubEvent)] = _ => customAuditEventArguments + }; + + var sut = new AuditSink(recorder.Object, customMappings); + var evt = new CustomStubEvent(string.Empty, string.Empty, EventTypes.Success, -1); + + // Act + await sut.PersistAsync(evt); + + // Assert + recorder.Verify(x => x.RecordSuccess(customAuditEventArguments), Times.Once); + } + private class StubEvent : Event { public StubEvent(string category, string name, EventTypes type, int id, string message = null) : base(category, name, type, id, message) { } } + + private class CustomStubEvent : Event + { + public CustomStubEvent(string category, string name, EventTypes type, int id, string message = null) : base(category, name, type, id, message) + { + } + } } }