diff --git a/RSK.IdentityServer4.AuditEventSink.sln b/RSK.IdentityServer4.AuditEventSink.sln index 2a4c179..cfc15e4 100644 --- a/RSK.IdentityServer4.AuditEventSink.sln +++ b/RSK.IdentityServer4.AuditEventSink.sln @@ -21,6 +21,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.Audit.Tests.Common", "t EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.Audit.Tests.Integration", "tests\Rsk.Audit.Tests.Integration\Rsk.Audit.Tests.Integration.csproj", "{374C47E2-B685-47BD-AD1A-6E435623E055}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.Open.IdentityServer.AuditEventSink", "src\Rsk.Open.IdentityServer.AuditEventSink\Rsk.Open.IdentityServer.AuditEventSink.csproj", "{535B7E62-8578-4B75-AB82-81E854A3F8DD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.Open.IdentityServer.AuditEventSink.Tests", "tests\Rsk.Open.IdentityServer.AuditEventSink.Tests\Rsk.Open.IdentityServer.AuditEventSink.Tests.csproj", "{CE817EDF-273F-4605-926D-A876DD1AA5BC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -55,6 +59,14 @@ Global {374C47E2-B685-47BD-AD1A-6E435623E055}.Debug|Any CPU.Build.0 = Debug|Any CPU {374C47E2-B685-47BD-AD1A-6E435623E055}.Release|Any CPU.ActiveCfg = Release|Any CPU {374C47E2-B685-47BD-AD1A-6E435623E055}.Release|Any CPU.Build.0 = Release|Any CPU + {535B7E62-8578-4B75-AB82-81E854A3F8DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {535B7E62-8578-4B75-AB82-81E854A3F8DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {535B7E62-8578-4B75-AB82-81E854A3F8DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {535B7E62-8578-4B75-AB82-81E854A3F8DD}.Release|Any CPU.Build.0 = Release|Any CPU + {CE817EDF-273F-4605-926D-A876DD1AA5BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE817EDF-273F-4605-926D-A876DD1AA5BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE817EDF-273F-4605-926D-A876DD1AA5BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE817EDF-273F-4605-926D-A876DD1AA5BC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -67,6 +79,8 @@ Global {3EA6C204-5D23-4844-B7D7-4C342CA401CD} = {4AD87BDC-20A4-4426-80E4-0706AA9D8294} {6FAFBAE2-745C-4243-BDA9-F01A6973F8D6} = {4AD87BDC-20A4-4426-80E4-0706AA9D8294} {374C47E2-B685-47BD-AD1A-6E435623E055} = {4AD87BDC-20A4-4426-80E4-0706AA9D8294} + {535B7E62-8578-4B75-AB82-81E854A3F8DD} = {0F18CE73-394B-4E63-8791-E6FDD93581AC} + {CE817EDF-273F-4605-926D-A876DD1AA5BC} = {4AD87BDC-20A4-4426-80E4-0706AA9D8294} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {741C58BE-23A1-4690-81D8-57D7789ACE7F} diff --git a/azure-pipelines.open-identity-server.yml b/azure-pipelines.open-identity-server.yml new file mode 100644 index 0000000..0331f37 --- /dev/null +++ b/azure-pipelines.open-identity-server.yml @@ -0,0 +1,91 @@ +trigger: none +pr: none + +variables: +- group: "NugetPackageRelease" +- name: buildConfiguration + value: 'Release' +- name: identityPackageVersion + value: '1.0.0.0' + +stages: +- stage: Build + jobs: + - job: BuildOpenIdentityServer + strategy: + matrix: + linux: + imageName: 'ubuntu-latest' + shouldPack: true + mac: + imageName: 'macOS-latest' + shouldPack: true + windows: + imageName: 'windows-latest' + shouldPack: true + pool: + vmImage: $(imageName) + steps: + - task: UseDotNet@2 + displayName: Install .NET Core sdk version 10.x + inputs: + packageType: sdk + version: 10.x + installationPath: $(Agent.ToolsDirectory)/dotnet + - task: NuGetToolInstaller@0 + inputs: + versionSpec: 5.4.0 + - task: DotNetCoreCLI@2 + displayName: 'Dotnet restore' + inputs: + command: 'restore' + projects: 'src/Rsk.Open.IdentityServer.AuditEventSink/Rsk.Open.IdentityServer.AuditEventSink.csproj' + feedsToUse: 'config' + nugetConfigPath: $(System.DefaultWorkingDirectory)/NuGet.AzDO.config + - task: DotNetCoreCLI@2 + displayName: dotnet build src/Rsk.Open.IdentityServer.AuditEventSink/Rsk.Open.IdentityServer.AuditEventSink.csproj + inputs: + command: 'build' + projects: 'src/Rsk.Open.IdentityServer.AuditEventSink/Rsk.Open.IdentityServer.AuditEventSink.csproj' + arguments: -c $(buildConfiguration) --no-restore /p:Version="$(identityPackageVersion)" + - task: DotNetCoreCLI@2 + displayName: dotnet test - Run Open IdentityServer Tests + inputs: + command: 'test' + projects: 'tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/Rsk.Open.IdentityServer.AuditEventSink.Tests.csproj' + arguments: -c $(buildConfiguration) --no-restore + - task: DotNetCoreCLI@2 + displayName: Package Rsk.Open.IdentityServer.AuditEventSink.csproj for Nuget + inputs: + command: 'pack' + packagesToPack: 'src/Rsk.Open.IdentityServer.AuditEventSink/Rsk.Open.IdentityServer.AuditEventSink.csproj' + nobuild: true + includesymbols: true + versionEnvVar: identityPackageVersion + versioningScheme: 'byEnvVar' + verbosityPack: 'Normal' + outputDir: '$(Build.ArtifactStagingDirectory)' + - task: PublishBuildArtifacts@1 + condition: and(succeeded(), eq(variables['shouldPack'], true)) + displayName: Publish Open IdentityServer artifacts + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)' + ArtifactName: 'Rsk.Open.IdentityServer.AuditEventSink nupkg' + publishLocation: 'Container' + +- stage: Publish + dependsOn: Build + condition: succeeded() + jobs: + - deployment: PublishNuGet + environment: Release + strategy: + runOnce: + deploy: + steps: + - template: templates/publish-nuget.yml + parameters: + toLive: true + artifact: 'Rsk.Open.IdentityServer.AuditEventSink nupkg' + packageToPublish: '$(Pipeline.Workspace)/Rsk.Open.IdentityServer.AuditEventSink nupkg/*.nupkg' + diff --git a/src/Rsk.Audit.EF/Rsk.Audit.EF.csproj b/src/Rsk.Audit.EF/Rsk.Audit.EF.csproj index 06d75e2..cf16ece 100644 --- a/src/Rsk.Audit.EF/Rsk.Audit.EF.csproj +++ b/src/Rsk.Audit.EF/Rsk.Audit.EF.csproj @@ -26,7 +26,7 @@ - + diff --git a/src/Rsk.DuendeIdentityServer.AuditEventSink/Rsk.DuendeIdentityServer.AuditEventSink.csproj b/src/Rsk.DuendeIdentityServer.AuditEventSink/Rsk.DuendeIdentityServer.AuditEventSink.csproj index c11d6d8..afbfe8b 100644 --- a/src/Rsk.DuendeIdentityServer.AuditEventSink/Rsk.DuendeIdentityServer.AuditEventSink.csproj +++ b/src/Rsk.DuendeIdentityServer.AuditEventSink/Rsk.DuendeIdentityServer.AuditEventSink.csproj @@ -1,4 +1,4 @@ - + net10.0 diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/AdapterFactory.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/AdapterFactory.cs new file mode 100644 index 0000000..d08c3ee --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/AdapterFactory.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using Open.IdentityServer.Events; +using RSK.Audit; +using Rsk.Open.IdentityServer.AuditEventSink.Adapters; + +namespace Rsk.Open.IdentityServer.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) + { + 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) + }; + } + } +} diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ApiAuthenticationFailureEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ApiAuthenticationFailureEventAdapter.cs new file mode 100644 index 0000000..bc3b528 --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ApiAuthenticationFailureEventAdapter.cs @@ -0,0 +1,21 @@ +using System; +using Open.IdentityServer.Events; +using RSK.Audit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters +{ + public class ApiAuthenticationFailureEventAdapter : IAuditEventArguments + { + private readonly ApiAuthenticationFailureEvent evt; + + public ApiAuthenticationFailureEventAdapter(ApiAuthenticationFailureEvent evt) + { + this.evt = evt ?? throw new ArgumentNullException(nameof(evt)); + } + + public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, null, null); + public string Action => evt.Name; + public AuditableResource Resource => new AuditableResource("IdentityServer", evt.ApiName); + public FormattedString Description => evt.ToString().SafeForFormatted(); + } +} diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ApiAuthenticationSuccessEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ApiAuthenticationSuccessEventAdapter.cs new file mode 100644 index 0000000..39bcacc --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ApiAuthenticationSuccessEventAdapter.cs @@ -0,0 +1,21 @@ +using System; +using Open.IdentityServer.Events; +using RSK.Audit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters +{ + public class ApiAuthenticationSuccessEventAdapter : IAuditEventArguments + { + private readonly ApiAuthenticationSuccessEvent evt; + + public ApiAuthenticationSuccessEventAdapter(ApiAuthenticationSuccessEvent evt) + { + this.evt = evt ?? throw new ArgumentNullException(nameof(evt)); + } + + public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, null, null); + public string Action => evt.Name; + public AuditableResource Resource => new AuditableResource("IdentityServer", evt.ApiName); + public FormattedString Description => evt.ToString().SafeForFormatted(); + } +} diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ClientAuthenticationFailureEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ClientAuthenticationFailureEventAdapter.cs new file mode 100644 index 0000000..b7c13fb --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ClientAuthenticationFailureEventAdapter.cs @@ -0,0 +1,21 @@ +using System; +using Open.IdentityServer.Events; +using RSK.Audit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters +{ + public class ClientAuthenticationFailureEventAdapter : IAuditEventArguments + { + private readonly ClientAuthenticationFailureEvent evt; + + public ClientAuthenticationFailureEventAdapter(ClientAuthenticationFailureEvent evt) + { + this.evt = evt ?? throw new ArgumentNullException(nameof(evt)); + } + + public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, evt.ClientId, evt.ClientId); + public string Action => evt.Name; + public AuditableResource Resource => new AuditableResource("IdentityServer"); + public FormattedString Description => evt.ToString().SafeForFormatted(); + } +} diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ClientAuthenticationSuccessEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ClientAuthenticationSuccessEventAdapter.cs new file mode 100644 index 0000000..dbf92f4 --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ClientAuthenticationSuccessEventAdapter.cs @@ -0,0 +1,21 @@ +using System; +using Open.IdentityServer.Events; +using RSK.Audit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters +{ + public class ClientAuthenticationSuccessEventAdapter : IAuditEventArguments + { + private readonly ClientAuthenticationSuccessEvent evt; + + public ClientAuthenticationSuccessEventAdapter(ClientAuthenticationSuccessEvent evt) + { + this.evt = evt ?? throw new ArgumentNullException(nameof(evt)); + } + + public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, evt.ClientId, evt.ClientId); + public string Action => evt.Name; + public AuditableResource Resource => new AuditableResource("IdentityServer"); + public FormattedString Description => evt.ToString().SafeForFormatted(); + } +} diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ConsentDeniedEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ConsentDeniedEventAdapter.cs new file mode 100644 index 0000000..54f9a1f --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ConsentDeniedEventAdapter.cs @@ -0,0 +1,21 @@ +using System; +using Open.IdentityServer.Events; +using RSK.Audit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters +{ + public class ConsentDeniedEventAdapter : IAuditEventArguments + { + private readonly ConsentDeniedEvent evt; + + public ConsentDeniedEventAdapter(ConsentDeniedEvent evt) + { + this.evt = evt ?? throw new ArgumentNullException(nameof(evt)); + } + + public ResourceActor Actor => new ResourceActor(ResourceActor.UserSubjectType, evt.SubjectId, evt.SubjectId); + public string Action => evt.Name; + public AuditableResource Resource => new AuditableResource("Client", evt.ClientId); + public FormattedString Description => evt.ToString().SafeForFormatted(); + } +} diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ConsentGrantedEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ConsentGrantedEventAdapter.cs new file mode 100644 index 0000000..6f495a2 --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ConsentGrantedEventAdapter.cs @@ -0,0 +1,20 @@ +using Open.IdentityServer.Events; +using RSK.Audit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters +{ + public class ConsentGrantedEventAdapter : IAuditEventArguments + { + private readonly ConsentGrantedEvent evt; + + public ConsentGrantedEventAdapter(ConsentGrantedEvent evt) + { + this.evt = evt; + } + + public ResourceActor Actor => new ResourceActor(ResourceActor.UserSubjectType, evt.SubjectId, evt.SubjectId); + public string Action => evt.Name; + public AuditableResource Resource => new AuditableResource("Client", evt.ClientId); + public FormattedString Description => evt.ToString().SafeForFormatted(); + } +} diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/DeviceAuthorizationFailureEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/DeviceAuthorizationFailureEventAdapter.cs new file mode 100644 index 0000000..465fe6b --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/DeviceAuthorizationFailureEventAdapter.cs @@ -0,0 +1,21 @@ +using System; +using Open.IdentityServer.Events; +using RSK.Audit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters +{ + public class DeviceAuthorizationFailureEventAdapter : IAuditEventArguments + { + private readonly DeviceAuthorizationFailureEvent evt; + + public DeviceAuthorizationFailureEventAdapter(DeviceAuthorizationFailureEvent evt) + { + this.evt = evt ?? throw new ArgumentNullException(nameof(evt)); + } + + public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, evt.ClientId, evt.ClientName); + public string Action => evt.Name; + public AuditableResource Resource => new AuditableResource("IdentityServer", evt.Endpoint); + public FormattedString Description => evt.ToString().SafeForFormatted(); + } +} diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/DeviceAuthorizationSuccessEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/DeviceAuthorizationSuccessEventAdapter.cs new file mode 100644 index 0000000..51b82b6 --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/DeviceAuthorizationSuccessEventAdapter.cs @@ -0,0 +1,21 @@ +using System; +using Open.IdentityServer.Events; +using RSK.Audit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters +{ + public class DeviceAuthorizationSuccessEventAdapter : IAuditEventArguments + { + private readonly DeviceAuthorizationSuccessEvent evt; + + public DeviceAuthorizationSuccessEventAdapter(DeviceAuthorizationSuccessEvent evt) + { + this.evt = evt ?? throw new ArgumentNullException(nameof(evt)); + } + + public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, evt.ClientId, evt.ClientName); + public string Action => evt.Name; + public AuditableResource Resource => new AuditableResource("IdentityServer", evt.Endpoint); + public FormattedString Description => evt.ToString().SafeForFormatted(); + } +} diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/GrantsRevokedEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/GrantsRevokedEventAdapter.cs new file mode 100644 index 0000000..8c659d5 --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/GrantsRevokedEventAdapter.cs @@ -0,0 +1,20 @@ +using System; +using Open.IdentityServer.Events; +using RSK.Audit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters +{ + public class GrantsRevokedEventAdapter : IAuditEventArguments + { + private readonly GrantsRevokedEvent evt; + + public GrantsRevokedEventAdapter(GrantsRevokedEvent evt) + { + this.evt = evt ?? throw new ArgumentNullException(nameof(evt)); + } + public ResourceActor Actor => new ResourceActor(ResourceActor.UserSubjectType, evt.SubjectId, evt.SubjectId); + public string Action => evt.Name; + public AuditableResource Resource => new AuditableResource("Client", evt.ClientId); + public FormattedString Description => evt.ToString().SafeForFormatted(); + } +} diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/InvalidClientConfigurationEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/InvalidClientConfigurationEventAdapter.cs new file mode 100644 index 0000000..f6a4893 --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/InvalidClientConfigurationEventAdapter.cs @@ -0,0 +1,21 @@ +using System; +using Open.IdentityServer.Events; +using RSK.Audit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters +{ + public class InvalidClientConfigurationEventAdapter : IAuditEventArguments + { + private readonly InvalidClientConfigurationEvent evt; + + public InvalidClientConfigurationEventAdapter(InvalidClientConfigurationEvent evt) + { + this.evt = evt ?? throw new ArgumentNullException(nameof(evt)); + } + + public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, null, null); + public string Action => evt.Name; + public AuditableResource Resource => new AuditableResource("IdentityServer", evt.ClientId); + public FormattedString Description => evt.ToString().SafeForFormatted(); + } +} \ No newline at end of file diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIntrospectionFailureEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIntrospectionFailureEventAdapter.cs new file mode 100644 index 0000000..2466deb --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIntrospectionFailureEventAdapter.cs @@ -0,0 +1,21 @@ +using System; +using Open.IdentityServer.Events; +using RSK.Audit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters +{ + public class TokenIntrospectionFailureEventAdapter : IAuditEventArguments + { + private readonly TokenIntrospectionFailureEvent evt; + + public TokenIntrospectionFailureEventAdapter(TokenIntrospectionFailureEvent evt) + { + this.evt = evt ?? throw new ArgumentNullException(nameof(evt)); + } + + public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, null, null); + public string Action => evt.Name; + public AuditableResource Resource => new AuditableResource("IdentityServer", evt.ApiName); + public FormattedString Description => evt.ToString().SafeForFormatted(); + } +} diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIntrospectionSuccessEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIntrospectionSuccessEventAdapter.cs new file mode 100644 index 0000000..8cfdfbf --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIntrospectionSuccessEventAdapter.cs @@ -0,0 +1,21 @@ +using System; +using Open.IdentityServer.Events; +using RSK.Audit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters +{ + public class TokenIntrospectionSuccessEventAdapter : IAuditEventArguments + { + private readonly TokenIntrospectionSuccessEvent evt; + + public TokenIntrospectionSuccessEventAdapter(TokenIntrospectionSuccessEvent evt) + { + this.evt = evt ?? throw new ArgumentNullException(nameof(evt)); + } + + public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, null, null); + public string Action => evt.Name; + public AuditableResource Resource => new AuditableResource("IdentityServer", evt.ApiName); + public FormattedString Description => evt.ToString().SafeForFormatted(); + } +} diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIssuedFailureEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIssuedFailureEventAdapter.cs new file mode 100644 index 0000000..157f601 --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIssuedFailureEventAdapter.cs @@ -0,0 +1,21 @@ +using System; +using Open.IdentityServer.Events; +using RSK.Audit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters +{ + public class TokenIssuedFailureEventAdapter : IAuditEventArguments + { + private readonly TokenIssuedFailureEvent evt; + + public TokenIssuedFailureEventAdapter(TokenIssuedFailureEvent evt) + { + this.evt = evt ?? throw new ArgumentNullException(nameof(evt)); + } + + public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, evt.ClientId, evt.ClientName); + public string Action => evt.Name; + public AuditableResource Resource => new AuditableResource("IdentityServer", evt.Endpoint); + public FormattedString Description => evt.ToString().SafeForFormatted(); + } +} diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIssuedSuccessEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIssuedSuccessEventAdapter.cs new file mode 100644 index 0000000..eeee54d --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIssuedSuccessEventAdapter.cs @@ -0,0 +1,21 @@ +using System; +using Open.IdentityServer.Events; +using RSK.Audit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters +{ + public class TokenIssuedSuccessEventAdapter : IAuditEventArguments + { + private readonly TokenIssuedSuccessEvent evt; + + public TokenIssuedSuccessEventAdapter(TokenIssuedSuccessEvent evt) + { + this.evt = evt ?? throw new ArgumentNullException(nameof(evt)); + } + + public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, evt.ClientId, evt.ClientName); + public string Action => evt.Name; + public AuditableResource Resource => new AuditableResource("IdentityServer", evt.Endpoint); + public FormattedString Description => evt.ToString().SafeForFormatted(); + } +} diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenRevokedSuccessEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenRevokedSuccessEventAdapter.cs new file mode 100644 index 0000000..43856f5 --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenRevokedSuccessEventAdapter.cs @@ -0,0 +1,21 @@ +using System; +using Open.IdentityServer.Events; +using RSK.Audit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters +{ + public class TokenRevokedSuccessEventAdapter : IAuditEventArguments + { + private readonly TokenRevokedSuccessEvent evt; + + public TokenRevokedSuccessEventAdapter(TokenRevokedSuccessEvent evt) + { + this.evt = evt ?? throw new ArgumentNullException(nameof(evt)); + } + + public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, evt.ClientId, evt.ClientName); + public string Action => evt.Name; + public AuditableResource Resource => new AuditableResource("IdentityServer"); + public FormattedString Description => evt.ToString().SafeForFormatted(); + } +} diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UnhandledExceptionEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UnhandledExceptionEventAdapter.cs new file mode 100644 index 0000000..a165171 --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UnhandledExceptionEventAdapter.cs @@ -0,0 +1,21 @@ +using System; +using Open.IdentityServer.Events; +using RSK.Audit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters +{ + public class UnhandledExceptionEventAdapter : IAuditEventArguments + { + private readonly UnhandledExceptionEvent evt; + + public UnhandledExceptionEventAdapter(UnhandledExceptionEvent evt) + { + this.evt = evt ?? throw new ArgumentNullException(nameof(evt)); + } + + public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, null, null); + public string Action => evt.Name; + public AuditableResource Resource => new AuditableResource("IdentityServer"); + public FormattedString Description => evt.ToString().SafeForFormatted(); + } +} diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UserLoginFailureEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UserLoginFailureEventAdapter.cs new file mode 100644 index 0000000..91b7ec2 --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UserLoginFailureEventAdapter.cs @@ -0,0 +1,21 @@ +using System; +using Open.IdentityServer.Events; +using RSK.Audit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters +{ + public class UserLoginFailureEventAdapter : IAuditEventArguments + { + private readonly UserLoginFailureEvent evt; + + public UserLoginFailureEventAdapter(UserLoginFailureEvent evt) + { + this.evt = evt ?? throw new ArgumentNullException(nameof(evt)); + } + + public ResourceActor Actor => new ResourceActor(ResourceActor.UserSubjectType, evt.Username, evt.Username); + public string Action => evt.Name; + public AuditableResource Resource => new AuditableResource("IdentityServer", evt.Endpoint); + public FormattedString Description => evt.ToString().SafeForFormatted(); + } +} diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UserLoginSuccessEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UserLoginSuccessEventAdapter.cs new file mode 100644 index 0000000..d1fc1e9 --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UserLoginSuccessEventAdapter.cs @@ -0,0 +1,24 @@ +using System; +using Open.IdentityServer.Events; +using RSK.Audit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters +{ + public class UserLoginSuccessEventAdapter : IAuditEventArguments + { + private readonly UserLoginSuccessEvent evt; + + public UserLoginSuccessEventAdapter(UserLoginSuccessEvent evt) + { + this.evt = evt ?? throw new ArgumentNullException(nameof(evt)); + } + + public ResourceActor Actor => new ResourceActor(ResourceActor.UserSubjectType, evt.SubjectId, evt.DisplayName); + + public string Action => evt.Name; + + public AuditableResource Resource => new AuditableResource("IdentityServer", evt.Endpoint); + + public FormattedString Description => evt.ToString().SafeForFormatted(); + } +} diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UserLogoutSuccessEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UserLogoutSuccessEventAdapter.cs new file mode 100644 index 0000000..856ec01 --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UserLogoutSuccessEventAdapter.cs @@ -0,0 +1,20 @@ +using Open.IdentityServer.Events; +using RSK.Audit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters +{ + public class UserLogoutSuccessEventAdapter : IAuditEventArguments + { + private readonly UserLogoutSuccessEvent evt; + + public UserLogoutSuccessEventAdapter(UserLogoutSuccessEvent evt) + { + this.evt = evt; + } + + public ResourceActor Actor => new ResourceActor(ResourceActor.UserSubjectType, evt.SubjectId, evt.DisplayName); + public string Action => evt.Name; + public AuditableResource Resource => new AuditableResource("IdentityServer", string.Empty); + public FormattedString Description => evt.ToString().SafeForFormatted(); + } +} diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/AuditSink.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/AuditSink.cs new file mode 100644 index 0000000..b737616 --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/AuditSink.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Open.IdentityServer.Events; +using Open.IdentityServer.Services; +using RSK.Audit; + +[assembly:InternalsVisibleTo("RSK.Open.IdentityServer.AuditEventSink.Tests")] + +namespace Rsk.Open.IdentityServer.AuditEventSink; + +public class AuditSink( + IRecordAuditableActions auditRecorder, + IDictionary> customEventAdapters = null) + : IEventSink +{ + private readonly IRecordAuditableActions auditRecorder = auditRecorder ?? throw new ArgumentNullException(); + + internal IAdapterFactory Factory { get; init; } = new AdapterFactory(customEventAdapters); + + public Task PersistAsync(Event evt) + { + var auditArgument = Factory.Create(evt); + + if (auditArgument != null) + { + if (evt.EventType == EventTypes.Success || evt.EventType == EventTypes.Information) + { + return auditRecorder.RecordSuccess(auditArgument); + } + + return auditRecorder.RecordFailure(auditArgument); + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/EventSinkAgregator.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/EventSinkAgregator.cs new file mode 100644 index 0000000..4125746 --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/EventSinkAgregator.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Open.IdentityServer.Events; +using Open.IdentityServer.Services; + +namespace Rsk.Open.IdentityServer.AuditEventSink +{ + public class EventSinkAggregator : IEventSink + { + private readonly ILogger logger; + public List EventSinks { get; set; } = new List(); + + public EventSinkAggregator(ILogger logger) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public Task PersistAsync(Event evt) + { + var eventSinkTasks = new List(); + + foreach (var eventSink in EventSinks) + { + eventSinkTasks.Add(ProtectedExecution(() => eventSink.PersistAsync(evt))); + } + + return Task.WhenAll(eventSinkTasks); + } + + private async Task ProtectedExecution(Func persistAsync) + { + try + { + await persistAsync(); + } + catch (Exception e) + { + logger.Log(LogLevel.Error, e.Message); + } + } + } +} diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/IAdapterFactory.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/IAdapterFactory.cs new file mode 100644 index 0000000..c0208a2 --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/IAdapterFactory.cs @@ -0,0 +1,10 @@ +using Open.IdentityServer.Events; +using RSK.Audit; + +namespace Rsk.Open.IdentityServer.AuditEventSink +{ + public interface IAdapterFactory + { + IAuditEventArguments Create(Event evt); + } +} \ No newline at end of file diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Rsk.Open.IdentityServer.AuditEventSink.csproj b/src/Rsk.Open.IdentityServer.AuditEventSink/Rsk.Open.IdentityServer.AuditEventSink.csproj new file mode 100644 index 0000000..14114c4 --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Rsk.Open.IdentityServer.AuditEventSink.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + Rock Solid Knowledge Ltd + Open.IdentityServer event sink to add audit records into AdminUI auditing + https://github.com/RockSolidKnowledge/Audit + Add event extensibility + Copyright 2026 (c) Rock Solid Knowledge Ltd. All rights reserved + Audit AdminUI Open.IdentityServer Events + true + icon.png + Apache-2.0 + 1.0.0 + Rsk.Open.IdentityServer.AuditEventSink + + + + + + + + + + + + diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/StringExtention.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/StringExtention.cs new file mode 100644 index 0000000..39c0a74 --- /dev/null +++ b/src/Rsk.Open.IdentityServer.AuditEventSink/StringExtention.cs @@ -0,0 +1,10 @@ +namespace Rsk.Open.IdentityServer.AuditEventSink +{ + public static class StringExtension + { + public static string SafeForFormatted(this string value) + { + return value.Replace("{", "{{").Replace("}", "}}"); + } + } +} diff --git a/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/AdapterFactoryTests.cs b/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/AdapterFactoryTests.cs new file mode 100644 index 0000000..9a0c4c9 --- /dev/null +++ b/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/AdapterFactoryTests.cs @@ -0,0 +1,324 @@ +using System.Collections.Generic; +using System.Security.Claims; +using Open.IdentityServer; +using Open.IdentityServer.Events; +using Open.IdentityServer.Models; +using Open.IdentityServer.ResponseHandling; +using Open.IdentityServer.Validation; +using Rsk.Open.IdentityServer.AuditEventSink.Adapters; +using Xunit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Tests +{ + public class AdapterFactoryTests + { + [Fact] + public void Create_WhenTokenIssuedSuccessEvent_WillReturnTokenIssuedSuccessEventAdapter() + { + // Arrange + var authResponse = new AuthorizeResponse() + { + Request = new ValidatedAuthorizeRequest() + { + Client = new Client(), + Subject = new ClaimsPrincipal(new ClaimsIdentity(new List() + { + new Claim(JwtClaimTypes.Subject, string.Empty) + })) + } + }; + + var evt = new TokenIssuedSuccessEvent(authResponse); + + var sut = new AdapterFactory(); + + // Act + var adapter = sut.Create(evt); + + // Assert + Assert.IsType(adapter); + } + + [Fact] + public void Create_WhenConsentGrantedEvent_WillReturnConsentGrantedEventAdapter() + { + // Arrange + var evt = new ConsentGrantedEvent(string.Empty, string.Empty, new List(), new List(), + false); + + var sut = new AdapterFactory(); + + // Act + var adapter = sut.Create(evt); + + // Assert + Assert.IsType(adapter); + } + + [Fact] + public void Create_WhenUserLoginFailureEvent_WillReturnUserLoginFailureAdapter() + { + // Arrange + var evt = new UserLoginFailureEvent(string.Empty, string.Empty); + + var sut = new AdapterFactory(); + + // Act + var adapter = sut.Create(evt); + + // Assert + Assert.IsType(adapter); + } + + [Fact] + public void Create_WhenUserLoginSuccessEvent_WillReturnUserLoginSuccessAdapter() + { + // Arrange + var evt = new UserLoginSuccessEvent(string.Empty, string.Empty, string.Empty, string.Empty); + + var sut = new AdapterFactory(); + + // Act + var adapter = sut.Create(evt); + + // Assert + Assert.IsType(adapter); + } + + [Fact] + public void Create_WhenUserLogoutSuccessEvent_WillReturnUserLogoutSuccessAdapter() + { + // Arrange + var evt = new UserLogoutSuccessEvent(string.Empty, string.Empty); + + var sut = new AdapterFactory(); + + // Act + var adapter = sut.Create(evt); + + // Assert + Assert.IsType(adapter); + } + + [Fact] + public void Create_WhenConsentDeniedEvent_WillReturnConsentDeniedAdapter() + { + // Arrange + var evt = new ConsentDeniedEvent(string.Empty, string.Empty, new string[] { }); + + var sut = new AdapterFactory(); + + // Act + var adapter = sut.Create(evt); + + // Assert + Assert.IsType(adapter); + } + + [Fact] + public void Create_WhenTokenIssuedFailureEvent_WillReturnTokenIssuedFailureAdapter() + { + // Arrange + var request = new ValidatedAuthorizeRequest() + { + Client = new Client(), + Subject = new ClaimsPrincipal(new ClaimsIdentity(new List() + { + new Claim(JwtClaimTypes.Subject, string.Empty) + })) + }; + + var evt = new TokenIssuedFailureEvent(request, string.Empty, string.Empty); + + var sut = new AdapterFactory(); + + // Act + var adapter = sut.Create(evt); + + // Assert + Assert.IsType(adapter); + } + + [Fact] + public void Create_WhenGrantsRevokedEvent_WillReturnGrantsRevokedAdapter() + { + // Arrange + var evt = new GrantsRevokedEvent(string.Empty, string.Empty); + + var sut = new AdapterFactory(); + + // Act + var adapter = sut.Create(evt); + + // Assert + Assert.IsType(adapter); + } + + [Fact] + public void Create_WhenApiAuthenticationFailureEvent_WillReturnApiAuthenticationFailureEventAdapter() + { + // Arrange + var evt = new ApiAuthenticationFailureEvent(string.Empty, string.Empty); + + var sut = new AdapterFactory(); + + // Act + var adapter = sut.Create(evt); + + // Assert + Assert.IsType(adapter); + } + + [Fact] + public void Create_WhenApiAuthenticationSuccessEvent_WillReturnApiAuthenticationSuccessEventAdapter() + { + // Arrange + var evt = new ApiAuthenticationSuccessEvent(string.Empty, string.Empty); + + var sut = new AdapterFactory(); + + // Act + var adapter = sut.Create(evt); + + // Assert + Assert.IsType(adapter); + } + + [Fact] + public void Create_WhenClientAuthenticationFailureEvent_WillReturnClientAuthenticationFailureEventAdapter() + { + // Arrange + var evt = new ClientAuthenticationFailureEvent(string.Empty, string.Empty); + + var sut = new AdapterFactory(); + + // Act + var adapter = sut.Create(evt); + + // Assert + Assert.IsType(adapter); + } + + [Fact] + public void Create_WhenClientAuthenticationSuccessEvent_WillReturnClientAuthenticationSuccessEventAdapter() + { + // Arrange + var evt = new ClientAuthenticationSuccessEvent(string.Empty, string.Empty); + + var sut = new AdapterFactory(); + + // Act + var adapter = sut.Create(evt); + + // Assert + Assert.IsType(adapter); + } + + [Fact] + public void Create_WhenDeviceAuthorizationFailureEvent_WillReturnDeviceAuthorizationFailureEventAdapter() + { + // Arrange + var evt = new DeviceAuthorizationFailureEvent(new DeviceAuthorizationRequestValidationResult(null)); + + var sut = new AdapterFactory(); + + // Act + var adapter = sut.Create(evt); + + // Assert + Assert.IsType(adapter); + } + + [Fact] + public void Create_WhenDeviceAuthorizationSuccessEvent_WillReturnDeviceAuthorizationSuccessEventAdapter() + { + // Arrange + var evt = new DeviceAuthorizationSuccessEvent(new DeviceAuthorizationResponse(), + new DeviceAuthorizationRequestValidationResult(new ValidatedDeviceAuthorizationRequest())); + + var sut = new AdapterFactory(); + + // Act + var adapter = sut.Create(evt); + + // Assert + Assert.IsType(adapter); + } + + [Fact] + public void Create_WhenInvalidClientConfigurationEvent_WillReturnInvalidClientConfigurationEventAdapter() + { + // Arrange + var evt = new InvalidClientConfigurationEvent(new Client(), string.Empty); + + var sut = new AdapterFactory(); + + // Act + var adapter = sut.Create(evt); + + // Assert + Assert.IsType(adapter); + } + + [Fact] + public void Create_WhenTokenIntrospectionFailureEvent_WillReturnTokenIntrospectionFailureEventAdapter() + { + // Arrange + var evt = new TokenIntrospectionFailureEvent(string.Empty, string.Empty); + + var sut = new AdapterFactory(); + + // Act + var adapter = sut.Create(evt); + + // Assert + Assert.IsType(adapter); + } + + [Fact] + public void Create_WhenTokenIntrospectionSuccessEvent_WillReturnTokenIntrospectionSuccessEventAdapter() + { + // Arrange + var evt = new TokenIntrospectionSuccessEvent(new IntrospectionRequestValidationResult + { Api = new ApiResource() }); + + var sut = new AdapterFactory(); + + // Act + var adapter = sut.Create(evt); + + // Assert + Assert.IsType(adapter); + } + + [Fact] + public void Create_WhenTokenRevokedSuccessEvent_WillReturnTokenRevokedSuccessEventAdapter() + { + // Arrange + var evt = new TokenRevokedSuccessEvent(new TokenRevocationRequestValidationResult(), new Client()); + + var sut = new AdapterFactory(); + + // Act + var adapter = sut.Create(evt); + + // Assert + Assert.IsType(adapter); + } + + [Fact] + public void Create_WhenUnhandledExceptionEvent_WillReturnUnhandledExceptionEventAdapter() + { + // Arrange + var evt = new UnhandledExceptionEvent(new System.Exception()); + + var sut = new AdapterFactory(); + + // Act + var adapter = sut.Create(evt); + + // Assert + Assert.IsType(adapter); + } + } +} diff --git a/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/AuditSinkTests.cs b/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/AuditSinkTests.cs new file mode 100644 index 0000000..045ba6e --- /dev/null +++ b/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/AuditSinkTests.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Moq; +using Open.IdentityServer.Events; +using RSK.Audit; +using Xunit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Tests +{ + public class AuditSinkTests + { + [Fact] + public async Task PersistAsync_WhenSuccessEvent_WillCallSuccessAuditRecord() + { + // Arrange + var recorder = new Mock(); + var factory = new Mock(); + factory.Setup(x => x.Create(It.IsAny())).Returns(new Mock().Object); + + var sut = new AuditSink(recorder.Object) {Factory = factory.Object}; + + + var successfulEvent = new StubEvent(string.Empty, string.Empty, EventTypes.Success, -1); + + // Act + await sut.PersistAsync(successfulEvent); + + // Assert + recorder.Verify(x => x.RecordSuccess(It.IsAny()), Times.Once); + } + + [Fact] + public async Task PersistAsync_WhenInformationEvent_WillCallSuccessAuditRecord() + { + // Arrange + var recorder = new Mock(); + var factory = new Mock(); + factory.Setup(x => x.Create(It.IsAny())).Returns(new Mock().Object); + + var sut = new AuditSink(recorder.Object) { Factory = factory.Object }; + + + var successfulEvent = new StubEvent(string.Empty, string.Empty, EventTypes.Information, -1); + + // Act + await sut.PersistAsync(successfulEvent); + + // Assert + recorder.Verify(x => x.RecordSuccess(It.IsAny()), Times.Once); + } + + [Fact] + public async Task PersistAsync_WhenErrorEvent_WillCallFailureAuditRecord() + { + // Arrange + var recorder = new Mock(); + var factory = new Mock(); + factory.Setup(x => x.Create(It.IsAny())).Returns(new Mock().Object); + + var sut = new AuditSink(recorder.Object) { Factory = factory.Object }; + + + var successfulEvent = new StubEvent(string.Empty, string.Empty, EventTypes.Error, -1); + + // Act + await sut.PersistAsync(successfulEvent); + + // Assert + recorder.Verify(x => x.RecordFailure(It.IsAny()), Times.Once); + } + + [Fact] + public async Task PersistAsync_WhenFailureEvent_WillCallFailureAuditRecord() + { + // Arrange + var recorder = new Mock(); + var factory = new Mock(); + factory.Setup(x => x.Create(It.IsAny())).Returns(new Mock().Object); + + var sut = new AuditSink(recorder.Object) { Factory = factory.Object }; + + + var successfulEvent = new StubEvent(string.Empty, string.Empty, EventTypes.Failure, -1); + + // Act + await sut.PersistAsync(successfulEvent); + + // Assert + 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) + { + } + } + } +} diff --git a/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/EventSinkAggregatorTests.cs b/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/EventSinkAggregatorTests.cs new file mode 100644 index 0000000..743ef8b --- /dev/null +++ b/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/EventSinkAggregatorTests.cs @@ -0,0 +1,105 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using Open.IdentityServer.Events; +using Open.IdentityServer.Services; +using Xunit; + +namespace Rsk.Open.IdentityServer.AuditEventSink.Tests +{ + public class EventSinkAggregatorTests + { + [Fact] + public async Task PersistAsync_WhenCalledWithMultiEventSinks_WillRaiseWithAll() + { + // Arrange + var sink1 = new StubSink(); + var sink2 = new StubSink(); + + var sut = new EventSinkAggregator(new Mock().Object); + + sut.EventSinks.Add(sink1); + sut.EventSinks.Add(sink2); + + // Act + await sut.PersistAsync(new StubEvent()); + + // Assert + Assert.Equal(1, sink1.WasCalled); + Assert.Equal(1, sink2.WasCalled); + } + + [Fact] + public async Task PersistAsync_WhenCalledWithMultiEventSinksAndOneThrowsAnException_WillRaiseToAllEventSinks() + { + // Arrange + var sink1 = new StubSink(); + var sink2 = new StubSink(); + var sink3 = new StubSinkThrowsException(); + + var logger = new StubLogger(); + + var sut = new EventSinkAggregator(logger); + + sut.EventSinks.Add(sink1); + sut.EventSinks.Add(sink2); + sut.EventSinks.Add(sink3); + + // Act + await sut.PersistAsync(new StubEvent()); + + // Assert + Assert.Equal(1, sink1.WasCalled); + Assert.Equal(1, sink2.WasCalled); + Assert.Equal(1, sink3.WasCalled); + Assert.Equal(1, logger.TimesErrored); + } + + private class StubSink : IEventSink + { + public int WasCalled { get; private set; } + + public Task PersistAsync(Event evt) + { + WasCalled++; + return Task.CompletedTask; + } + } + + private class StubSinkThrowsException : IEventSink + { + public int WasCalled { get; private set; } + + public Task PersistAsync(Event evt) + { + WasCalled++; + throw new Exception("Blah"); + } + } + + private class StubEvent : Event + { + public StubEvent() : base(string.Empty, string.Empty, EventTypes.Failure, 0) + { + } + } + + private class StubLogger : ILogger + { + public int TimesErrored = 0; + + public IDisposable BeginScope(TState state) + { + throw new NotImplementedException(); + } + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (logLevel == LogLevel.Error) TimesErrored++; + } + } + } +} diff --git a/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/Rsk.Open.IdentityServer.AuditEventSink.Tests.csproj b/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/Rsk.Open.IdentityServer.AuditEventSink.Tests.csproj new file mode 100644 index 0000000..d6959a3 --- /dev/null +++ b/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/Rsk.Open.IdentityServer.AuditEventSink.Tests.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + false + Rsk.Open.IdentityServer.AuditEventSink.Tests + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + +