From dad0271b45a6495ef01ba6d59690d211d2374631 Mon Sep 17 00:00:00 2001 From: Raphael Guntersweiler Date: Fri, 31 May 2024 22:52:02 +0200 Subject: [PATCH 01/14] implemented auditing base --- .../AuditorExtenstions.cs | 115 ++++++++++++++++++ .../Entities/AuditRecord.cs | 12 ++ ...FloppyBot.Base.Auditing.Abstraction.csproj | 17 +++ .../IAuditor.cs | 23 ++++ .../Impl/NoopAuditor.cs | 18 +++ .../Registration.cs | 12 ++ .../AuditorExtensions.cs | 47 +++++++ .../FloppyBot.Base.Auditing.Storage.csproj | 15 +++ .../InternalAuditRecord.cs | 49 ++++++++ .../StorageAuditor.cs | 43 +++++++ src/FloppyBot.sln | 17 +++ 11 files changed, 368 insertions(+) create mode 100644 src/FloppyBot.Base.Auditing.Abstraction/AuditorExtenstions.cs create mode 100644 src/FloppyBot.Base.Auditing.Abstraction/Entities/AuditRecord.cs create mode 100644 src/FloppyBot.Base.Auditing.Abstraction/FloppyBot.Base.Auditing.Abstraction.csproj create mode 100644 src/FloppyBot.Base.Auditing.Abstraction/IAuditor.cs create mode 100644 src/FloppyBot.Base.Auditing.Abstraction/Impl/NoopAuditor.cs create mode 100644 src/FloppyBot.Base.Auditing.Abstraction/Registration.cs create mode 100644 src/FloppyBot.Base.Auditing.Storage/AuditorExtensions.cs create mode 100644 src/FloppyBot.Base.Auditing.Storage/FloppyBot.Base.Auditing.Storage.csproj create mode 100644 src/FloppyBot.Base.Auditing.Storage/InternalAuditRecord.cs create mode 100644 src/FloppyBot.Base.Auditing.Storage/StorageAuditor.cs diff --git a/src/FloppyBot.Base.Auditing.Abstraction/AuditorExtenstions.cs b/src/FloppyBot.Base.Auditing.Abstraction/AuditorExtenstions.cs new file mode 100644 index 0000000..0902fd2 --- /dev/null +++ b/src/FloppyBot.Base.Auditing.Abstraction/AuditorExtenstions.cs @@ -0,0 +1,115 @@ +using System.Diagnostics; +using FloppyBot.Base.Auditing.Abstraction.Entities; +using FloppyBot.Chat.Entities.Identifiers; + +namespace FloppyBot.Base.Auditing.Abstraction; + +public static class AuditorExtensions +{ + /// + /// Records a new event + /// + /// + /// The auditor to use + /// The identifier of the user taking the action + /// The channel that this action was taken on + /// The type of the object subjected to change + /// An identifier for the object + /// The action performed on the object + /// Additional context data + /// Optional timestamp + [StackTraceHidden] + public static void Record( + this IAuditor auditor, + string userIdentifier, + string channelIdentifier, + string objectType, + string objectIdentifier, + string action, + string? additionalData = null, + DateTimeOffset? timestamp = null + ) + { + auditor.Record( + new AuditRecord( + // The ID is set to null here, but it will be set by the storage layer + Id: null!, + Timestamp: timestamp ?? DateTimeOffset.MinValue, + UserIdentifier: userIdentifier, + ChannelIdentifier: channelIdentifier, + ObjectType: objectType, + ObjectIdentifier: objectIdentifier, + Action: action, + AdditionalData: additionalData + ) + ); + } + + /// + /// Records a new event + /// + /// The auditor to use + /// The identifier of the user taking the action + /// The channel that this action was taken on + /// The type of the object subjected to change + /// An identifier for the object + /// The action performed on the object + /// Additional context data + /// Optional timestamp + [StackTraceHidden] + public static void Record( + this IAuditor auditor, + ChannelIdentifier user, + ChannelIdentifier channel, + string objectType, + string objectIdentifier, + string action, + string? additionalData = null, + DateTimeOffset? timestamp = null + ) + { + auditor.Record( + user.ToString(), + channel.ToString(), + objectType, + objectIdentifier, + action, + additionalData, + timestamp + ); + } + + /// + /// Records a new event + /// + /// The auditor to use + /// The identifier of the user taking the action + /// The channel that this action was taken on + /// The object affected + /// A function that returns the identifier of the object + /// The action performed on the object + /// Additional context data + /// Optional timestamp + [StackTraceHidden] + public static void Record( + this IAuditor auditor, + ChannelIdentifier user, + ChannelIdentifier channel, + T @object, + Func objectIdentifier, + string action, + string? additionalData = null, + DateTimeOffset? timestamp = null + ) + { + auditor.Record( + user.ToString(), + channel.ToString(), + typeof(T).Name, + objectIdentifier(@object), + action, + additionalData, + timestamp + ); + } +} diff --git a/src/FloppyBot.Base.Auditing.Abstraction/Entities/AuditRecord.cs b/src/FloppyBot.Base.Auditing.Abstraction/Entities/AuditRecord.cs new file mode 100644 index 0000000..e21d25b --- /dev/null +++ b/src/FloppyBot.Base.Auditing.Abstraction/Entities/AuditRecord.cs @@ -0,0 +1,12 @@ +namespace FloppyBot.Base.Auditing.Abstraction.Entities; + +public record AuditRecord( + string Id, + DateTimeOffset Timestamp, + string UserIdentifier, + string ChannelIdentifier, + string ObjectType, + string ObjectIdentifier, + string Action, + string? AdditionalData +); diff --git a/src/FloppyBot.Base.Auditing.Abstraction/FloppyBot.Base.Auditing.Abstraction.csproj b/src/FloppyBot.Base.Auditing.Abstraction/FloppyBot.Base.Auditing.Abstraction.csproj new file mode 100644 index 0000000..640aa74 --- /dev/null +++ b/src/FloppyBot.Base.Auditing.Abstraction/FloppyBot.Base.Auditing.Abstraction.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/src/FloppyBot.Base.Auditing.Abstraction/IAuditor.cs b/src/FloppyBot.Base.Auditing.Abstraction/IAuditor.cs new file mode 100644 index 0000000..e204b66 --- /dev/null +++ b/src/FloppyBot.Base.Auditing.Abstraction/IAuditor.cs @@ -0,0 +1,23 @@ +using FloppyBot.Base.Auditing.Abstraction.Entities; + +namespace FloppyBot.Base.Auditing.Abstraction; + +/// +/// A service that records events for auditing purposes +/// +public interface IAuditor +{ + /// + /// Records a new event + /// + /// + void Record(AuditRecord auditRecord); + + /// + /// Get a list of audit records for the specified channels + /// + /// Channel to query from + /// Additional channels to query from + /// + IEnumerable GetAuditRecords(string channel, params string[] channels); +} diff --git a/src/FloppyBot.Base.Auditing.Abstraction/Impl/NoopAuditor.cs b/src/FloppyBot.Base.Auditing.Abstraction/Impl/NoopAuditor.cs new file mode 100644 index 0000000..0137d0b --- /dev/null +++ b/src/FloppyBot.Base.Auditing.Abstraction/Impl/NoopAuditor.cs @@ -0,0 +1,18 @@ +using FloppyBot.Base.Auditing.Abstraction.Entities; + +namespace FloppyBot.Base.Auditing.Abstraction.Impl; + +/// +/// A no-op auditor that does nothing, useful for testing +/// +public class NoopAuditor : IAuditor +{ + /// + public void Record(AuditRecord auditRecord) { } + + /// + public IEnumerable GetAuditRecords(string channel, params string[] channels) + { + return []; + } +} diff --git a/src/FloppyBot.Base.Auditing.Abstraction/Registration.cs b/src/FloppyBot.Base.Auditing.Abstraction/Registration.cs new file mode 100644 index 0000000..3a62ea3 --- /dev/null +++ b/src/FloppyBot.Base.Auditing.Abstraction/Registration.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace FloppyBot.Base.Auditing.Abstraction; + +public static class Registration +{ + public static IServiceCollection AddAuditor(this IServiceCollection services) + where T : class, IAuditor + { + return services.AddSingleton(); + } +} diff --git a/src/FloppyBot.Base.Auditing.Storage/AuditorExtensions.cs b/src/FloppyBot.Base.Auditing.Storage/AuditorExtensions.cs new file mode 100644 index 0000000..7ee7e0f --- /dev/null +++ b/src/FloppyBot.Base.Auditing.Storage/AuditorExtensions.cs @@ -0,0 +1,47 @@ +using System.Diagnostics; +using FloppyBot.Base.Auditing.Abstraction; +using FloppyBot.Base.Storage; +using FloppyBot.Chat.Entities.Identifiers; + +namespace FloppyBot.Base.Auditing.Storage; + +public static class AuditorExtensions +{ + /// + /// Records a new event + /// + /// The auditor to use + /// The identifier of the user taking the action + /// The channel that this action was taken on + /// The object affected + /// The action performed on the object + /// Additional context data + /// Optional timestamp + [StackTraceHidden] + public static void Record( + this IAuditor auditor, + ChannelIdentifier user, + ChannelIdentifier channel, + T @object, + string action, + string? additionalData = null, + DateTimeOffset? timestamp = null + ) + where T : IEntity + { + auditor.Record( + user, + channel, + @object, + GetObjectIdentifier, + action, + additionalData ?? @object.ToString() + ); + } + + private static string GetObjectIdentifier(T @object) + where T : IEntity + { + return @object.Id; + } +} diff --git a/src/FloppyBot.Base.Auditing.Storage/FloppyBot.Base.Auditing.Storage.csproj b/src/FloppyBot.Base.Auditing.Storage/FloppyBot.Base.Auditing.Storage.csproj new file mode 100644 index 0000000..ce0982a --- /dev/null +++ b/src/FloppyBot.Base.Auditing.Storage/FloppyBot.Base.Auditing.Storage.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + + diff --git a/src/FloppyBot.Base.Auditing.Storage/InternalAuditRecord.cs b/src/FloppyBot.Base.Auditing.Storage/InternalAuditRecord.cs new file mode 100644 index 0000000..5eb2c15 --- /dev/null +++ b/src/FloppyBot.Base.Auditing.Storage/InternalAuditRecord.cs @@ -0,0 +1,49 @@ +using FloppyBot.Base.Auditing.Abstraction.Entities; +using FloppyBot.Base.Storage; + +namespace FloppyBot.Base.Auditing.Storage; + +internal record InternalAuditRecord( + string Id, + DateTimeOffset Timestamp, + string UserIdentifier, + string ChannelIdentifier, + string ObjectType, + string ObjectIdentifier, + string Action, + string? AdditionalData +) : IEntity +{ + public InternalAuditRecord WithId(string newId) + { + return this with { Id = newId }; + } + + public AuditRecord ToAuditRecord() + { + return new AuditRecord( + Id, + Timestamp, + UserIdentifier, + ChannelIdentifier, + ObjectType, + ObjectIdentifier, + Action, + AdditionalData + ); + } + + public static InternalAuditRecord FromAuditRecord(AuditRecord auditRecord) + { + return new InternalAuditRecord( + auditRecord.Id, + auditRecord.Timestamp, + auditRecord.UserIdentifier, + auditRecord.ChannelIdentifier, + auditRecord.ObjectType, + auditRecord.ObjectIdentifier, + auditRecord.Action, + auditRecord.AdditionalData + ); + } +} diff --git a/src/FloppyBot.Base.Auditing.Storage/StorageAuditor.cs b/src/FloppyBot.Base.Auditing.Storage/StorageAuditor.cs new file mode 100644 index 0000000..26095a8 --- /dev/null +++ b/src/FloppyBot.Base.Auditing.Storage/StorageAuditor.cs @@ -0,0 +1,43 @@ +using FloppyBot.Base.Auditing.Abstraction; +using FloppyBot.Base.Auditing.Abstraction.Entities; +using FloppyBot.Base.Clock; +using FloppyBot.Base.Storage; + +namespace FloppyBot.Base.Auditing.Storage; + +/// +/// Implementation of that stores audit records in a repository. +/// +public class StorageAuditor : IAuditor +{ + private readonly IRepository _repository; + private readonly ITimeProvider _timeProvider; + + public StorageAuditor(IRepositoryFactory repositoryFactory, ITimeProvider timeProvider) + { + _timeProvider = timeProvider; + _repository = repositoryFactory.GetRepository("AuditRecord"); + } + + /// + public void Record(AuditRecord auditRecord) + { + _repository.Insert( + InternalAuditRecord.FromAuditRecord(auditRecord) with + { + Timestamp = _timeProvider.GetCurrentUtcTime(), + } + ); + } + + /// + public IEnumerable GetAuditRecords(string channel, params string[] channels) + { + var channelList = channels.Prepend(channel).ToList(); + return _repository + .GetAll() + .Where(c => channelList.Contains(c.ChannelIdentifier)) + .ToList() + .Select(i => i.ToAuditRecord()); + } +} diff --git a/src/FloppyBot.sln b/src/FloppyBot.sln index f2eb11e..464e332 100644 --- a/src/FloppyBot.sln +++ b/src/FloppyBot.sln @@ -200,6 +200,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Logging", "Logging", "{2CC2 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FloppyBot.Base.Logging.MongoDb", "FloppyBot.Base.Logging.MongoDb\FloppyBot.Base.Logging.MongoDb.csproj", "{93149607-078B-4964-A9EE-6504B43BC055}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Auditing", "Auditing", "{B9371406-4567-483D-B66B-3324D1BBE279}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FloppyBot.Base.Auditing.Abstraction", "FloppyBot.Base.Auditing.Abstraction\FloppyBot.Base.Auditing.Abstraction.csproj", "{3F3A2311-C9FE-478E-980A-8EC697A5E0C0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FloppyBot.Base.Auditing.Storage", "FloppyBot.Base.Auditing.Storage\FloppyBot.Base.Auditing.Storage.csproj", "{D0C19855-9BB0-4F33-831A-8F3897B17522}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -296,6 +302,9 @@ Global {2CC2386C-DA9F-4144-A8EC-31A884E2B65B} = {7F239034-8411-4F30-9E57-B0F5B4F9E1EA} {159AAC30-157C-4F14-A17C-D5635D5A1DD4} = {2CC2386C-DA9F-4144-A8EC-31A884E2B65B} {93149607-078B-4964-A9EE-6504B43BC055} = {2CC2386C-DA9F-4144-A8EC-31A884E2B65B} + {B9371406-4567-483D-B66B-3324D1BBE279} = {7F239034-8411-4F30-9E57-B0F5B4F9E1EA} + {3F3A2311-C9FE-478E-980A-8EC697A5E0C0} = {B9371406-4567-483D-B66B-3324D1BBE279} + {D0C19855-9BB0-4F33-831A-8F3897B17522} = {B9371406-4567-483D-B66B-3324D1BBE279} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {67258FB8-8B58-4F1B-B099-3F177074B50F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -574,5 +583,13 @@ Global {93149607-078B-4964-A9EE-6504B43BC055}.Debug|Any CPU.Build.0 = Debug|Any CPU {93149607-078B-4964-A9EE-6504B43BC055}.Release|Any CPU.ActiveCfg = Release|Any CPU {93149607-078B-4964-A9EE-6504B43BC055}.Release|Any CPU.Build.0 = Release|Any CPU + {3F3A2311-C9FE-478E-980A-8EC697A5E0C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F3A2311-C9FE-478E-980A-8EC697A5E0C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F3A2311-C9FE-478E-980A-8EC697A5E0C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F3A2311-C9FE-478E-980A-8EC697A5E0C0}.Release|Any CPU.Build.0 = Release|Any CPU + {D0C19855-9BB0-4F33-831A-8F3897B17522}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D0C19855-9BB0-4F33-831A-8F3897B17522}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0C19855-9BB0-4F33-831A-8F3897B17522}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D0C19855-9BB0-4F33-831A-8F3897B17522}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal From 62ac7ac790de81bef99a495a7768d0d3d312cda7 Mon Sep 17 00:00:00 2001 From: Raphael Guntersweiler Date: Fri, 31 May 2024 23:35:57 +0200 Subject: [PATCH 02/14] quotes are now audited --- .../AuditorExtenstions.cs | 4 +- .../CommonActions.cs | 8 + .../Registration.cs | 2 +- .../QuoteCommandTests.cs | 142 +++++++++++++++++- .../QuoteCommands.cs | 44 ++++-- .../Storage/QuoteAuditing.cs | 69 +++++++++ .../FloppyBot.Commands.Core.csproj | 2 + 7 files changed, 253 insertions(+), 18 deletions(-) create mode 100644 src/FloppyBot.Base.Auditing.Abstraction/CommonActions.cs create mode 100644 src/FloppyBot.Commands.Aux.Quotes/Storage/QuoteAuditing.cs diff --git a/src/FloppyBot.Base.Auditing.Abstraction/AuditorExtenstions.cs b/src/FloppyBot.Base.Auditing.Abstraction/AuditorExtenstions.cs index 0902fd2..390f753 100644 --- a/src/FloppyBot.Base.Auditing.Abstraction/AuditorExtenstions.cs +++ b/src/FloppyBot.Base.Auditing.Abstraction/AuditorExtenstions.cs @@ -4,6 +4,7 @@ namespace FloppyBot.Base.Auditing.Abstraction; +[StackTraceHidden] public static class AuditorExtensions { /// @@ -18,7 +19,6 @@ public static class AuditorExtensions /// The action performed on the object /// Additional context data /// Optional timestamp - [StackTraceHidden] public static void Record( this IAuditor auditor, string userIdentifier, @@ -56,7 +56,6 @@ public static void Record( /// The action performed on the object /// Additional context data /// Optional timestamp - [StackTraceHidden] public static void Record( this IAuditor auditor, ChannelIdentifier user, @@ -90,7 +89,6 @@ public static void Record( /// The action performed on the object /// Additional context data /// Optional timestamp - [StackTraceHidden] public static void Record( this IAuditor auditor, ChannelIdentifier user, diff --git a/src/FloppyBot.Base.Auditing.Abstraction/CommonActions.cs b/src/FloppyBot.Base.Auditing.Abstraction/CommonActions.cs new file mode 100644 index 0000000..be1e03a --- /dev/null +++ b/src/FloppyBot.Base.Auditing.Abstraction/CommonActions.cs @@ -0,0 +1,8 @@ +namespace FloppyBot.Base.Auditing.Abstraction; + +public static class CommonActions +{ + public const string Created = "Created"; + public const string Updated = "Updated"; + public const string Deleted = "Deleted"; +} diff --git a/src/FloppyBot.Base.Auditing.Abstraction/Registration.cs b/src/FloppyBot.Base.Auditing.Abstraction/Registration.cs index 3a62ea3..a4068e6 100644 --- a/src/FloppyBot.Base.Auditing.Abstraction/Registration.cs +++ b/src/FloppyBot.Base.Auditing.Abstraction/Registration.cs @@ -7,6 +7,6 @@ public static class Registration public static IServiceCollection AddAuditor(this IServiceCollection services) where T : class, IAuditor { - return services.AddSingleton(); + return services.AddScoped(); } } diff --git a/src/FloppyBot.Commands.Aux.Quotes.Tests/QuoteCommandTests.cs b/src/FloppyBot.Commands.Aux.Quotes.Tests/QuoteCommandTests.cs index f47f996..134d34f 100644 --- a/src/FloppyBot.Commands.Aux.Quotes.Tests/QuoteCommandTests.cs +++ b/src/FloppyBot.Commands.Aux.Quotes.Tests/QuoteCommandTests.cs @@ -1,4 +1,6 @@ using FakeItEasy; +using FloppyBot.Base.Auditing.Abstraction; +using FloppyBot.Base.Auditing.Abstraction.Entities; using FloppyBot.Base.Testing; using FloppyBot.Chat.Entities; using FloppyBot.Commands.Aux.Quotes.Storage; @@ -11,11 +13,17 @@ public class QuoteCommandTests { private readonly QuoteCommands _quoteCommands; private readonly IQuoteService _quoteService; + private readonly IAuditor _auditor; public QuoteCommandTests() { _quoteService = A.Fake(); - _quoteCommands = new QuoteCommands(LoggingUtils.GetLogger(), _quoteService); + _auditor = A.Fake(); + _quoteCommands = new QuoteCommands( + LoggingUtils.GetLogger(), + _quoteService, + _auditor + ); } [TestMethod] @@ -56,6 +64,22 @@ public void AddQuote() ) ) .MustHaveHappenedOnceExactly(); + A.CallTo( + () => + _auditor.Record( + new AuditRecord( + null!, + DateTimeOffset.MinValue, + "Mock/UserName", + "Mock/Channel", + nameof(Quote), + "1337", + CommonActions.Created, + "Quote #1337: This is my quote [Cool Game @ 2022-10-12]" + ) + ) + ) + .MustHaveHappenedOnceExactly(); } [TestMethod] @@ -75,11 +99,32 @@ public void EditQuote() ) ); - var reply = _quoteCommands.EditQuote("Mock/Channel", 1337, "This is my new text"); + var reply = _quoteCommands.EditQuote( + "Mock/Channel", + 1337, + "This is my new text", + new ChatUser("Mock/UserName", "User Name", PrivilegeLevel.Viewer) + ); Assert.AreEqual("Updated Quote #1337: This is my new text [Cool Game @ 2022-10-12]", reply); A.CallTo(() => _quoteService.EditQuote("Mock/Channel", 1337, "This is my new text")) .MustHaveHappenedOnceExactly(); + A.CallTo( + () => + _auditor.Record( + new AuditRecord( + null!, + DateTimeOffset.MinValue, + "Mock/UserName", + "Mock/Channel", + nameof(Quote), + "1337", + CommonActions.Updated, + "Quote #1337: This is my new text [Cool Game @ 2022-10-12]" + ) + ) + ) + .MustHaveHappenedOnceExactly(); } [TestMethod] @@ -99,12 +144,33 @@ public void EditQuoteContext() ) ); - var reply = _quoteCommands.EditQuoteContext("Mock/Channel", 1337, "Uncool Game"); + var reply = _quoteCommands.EditQuoteContext( + "Mock/Channel", + 1337, + "Uncool Game", + new ChatUser("Mock/UserName", "User Name", PrivilegeLevel.Viewer) + ); Assert.AreEqual("Updated Quote #1337: This is my quote [Uncool Game @ 2022-10-12]", reply); A.CallTo(() => _quoteService.EditQuoteContext("Mock/Channel", 1337, "Uncool Game")) .MustHaveHappenedOnceExactly(); + A.CallTo( + () => + _auditor.Record( + new AuditRecord( + null!, + DateTimeOffset.MinValue, + "Mock/UserName", + "Mock/Channel", + nameof(Quote), + "1337", + CommonActions.Updated, + "Quote #1337: This is my quote [Uncool Game @ 2022-10-12]" + ) + ) + ) + .MustHaveHappenedOnceExactly(); } [TestMethod] @@ -112,11 +178,31 @@ public void DeleteQuote() { A.CallTo(() => _quoteService.DeleteQuote(A._, An._)).Returns(true); - var reply = _quoteCommands.DeleteQuote("Mock/Channel", 1337); + var reply = _quoteCommands.DeleteQuote( + "Mock/Channel", + 1337, + new ChatUser("Mock/UserName", "User Name", PrivilegeLevel.Viewer) + ); Assert.AreEqual("Deleted Quote #1337", reply); A.CallTo(() => _quoteService.DeleteQuote("Mock/Channel", 1337)) .MustHaveHappenedOnceExactly(); + A.CallTo( + () => + _auditor.Record( + new AuditRecord( + null!, + DateTimeOffset.MinValue, + "Mock/UserName", + "Mock/Channel", + nameof(Quote), + "1337", + CommonActions.Deleted, + null + ) + ) + ) + .MustHaveHappenedOnceExactly(); } [DataTestMethod] @@ -153,6 +239,22 @@ bool expectExecution A.CallTo(() => _quoteService.DeleteQuote("Mock/Channel", 1)) .MustHaveHappened(expectExecution ? 1 : 0, Times.Exactly); + A.CallTo( + () => + _auditor.Record( + new AuditRecord( + null!, + DateTimeOffset.MinValue, + "Mock/User", + "Mock/Channel", + nameof(Quote), + "1", + CommonActions.Deleted, + null + ) + ) + ) + .MustHaveHappened(expectExecution ? 1 : 0, Times.Exactly); } [DataTestMethod] @@ -201,6 +303,22 @@ bool expectExecution A.CallTo(() => _quoteService.EditQuote("Mock/Channel", 1, "My new text")) .MustHaveHappened(expectExecution ? 1 : 0, Times.Exactly); + A.CallTo( + () => + _auditor.Record( + new AuditRecord( + null!, + DateTimeOffset.MinValue, + "Mock/User", + "Mock/Channel", + nameof(Quote), + "1", + CommonActions.Updated, + "Quote #1: My new text [Some Game @ 2022-10-12]" + ) + ) + ) + .MustHaveHappened(expectExecution ? 1 : 0, Times.Exactly); } [DataTestMethod] @@ -249,5 +367,21 @@ bool expectExecution A.CallTo(() => _quoteService.EditQuoteContext("Mock/Channel", 1, "My new text")) .MustHaveHappened(expectExecution ? 1 : 0, Times.Exactly); + A.CallTo( + () => + _auditor.Record( + new AuditRecord( + null!, + DateTimeOffset.MinValue, + "Mock/User", + "Mock/Channel", + nameof(Quote), + "1", + CommonActions.Updated, + "Quote #1: My text [My new text @ 2022-10-12]" + ) + ) + ) + .MustHaveHappened(expectExecution ? 1 : 0, Times.Exactly); } } diff --git a/src/FloppyBot.Commands.Aux.Quotes/QuoteCommands.cs b/src/FloppyBot.Commands.Aux.Quotes/QuoteCommands.cs index 8d9a199..845bd22 100644 --- a/src/FloppyBot.Commands.Aux.Quotes/QuoteCommands.cs +++ b/src/FloppyBot.Commands.Aux.Quotes/QuoteCommands.cs @@ -1,4 +1,6 @@ using System.Collections.Immutable; +using FloppyBot.Base.Auditing.Abstraction; +using FloppyBot.Base.Auditing.Storage; using FloppyBot.Base.Clock; using FloppyBot.Base.Rng; using FloppyBot.Base.TextFormatting; @@ -54,11 +56,17 @@ public class QuoteCommands private readonly ILogger _logger; private readonly IQuoteService _quoteService; + private readonly IAuditor _auditor; - public QuoteCommands(ILogger logger, IQuoteService quoteService) + public QuoteCommands( + ILogger logger, + IQuoteService quoteService, + IAuditor auditor + ) { _logger = logger; _quoteService = quoteService; + _auditor = auditor; } [DependencyRegistration] @@ -68,6 +76,7 @@ public static void RegisterDependencies(IServiceCollection services) services .AddSingleton() .AddSingleton() + .AddAuditor() .AddScoped() .AddScoped(); } @@ -115,6 +124,7 @@ [AllArguments] string quoteText sourceContext ?? sourceChannel.Interface, author.DisplayName ); + _auditor.QuoteCreated(author, sourceChannel, quote); return REPLY_CREATED.Format(new { Quote = quote }); } @@ -128,7 +138,8 @@ [AllArguments] string quoteText public string EditQuote( [SourceChannel] string sourceChannel, [ArgumentIndex(0)] int quoteId, - [ArgumentRange(1)] string newQuoteText + [ArgumentRange(1)] string newQuoteText, + [Author] ChatUser author ) { var editedQuote = _quoteService.EditQuote(sourceChannel, quoteId, newQuoteText); @@ -137,6 +148,7 @@ public string EditQuote( return REPLY_QUOTE_NOT_FOUND.Format(new { QuoteId = quoteId }); } + _auditor.QuoteUpdated(author, sourceChannel, editedQuote); return REPLY_EDITED.Format(new { Quote = editedQuote }); } @@ -150,7 +162,8 @@ public string EditQuote( public string EditQuoteContext( [SourceChannel] string sourceChannel, [ArgumentIndex(0)] int quoteId, - [ArgumentRange(1)] string newQuoteContext + [ArgumentRange(1)] string newQuoteContext, + [Author] ChatUser author ) { var editedQuote = _quoteService.EditQuoteContext(sourceChannel, quoteId, newQuoteContext); @@ -159,6 +172,7 @@ public string EditQuoteContext( return REPLY_QUOTE_NOT_FOUND.Format(new { QuoteId = quoteId }); } + _auditor.QuoteUpdated(author, sourceChannel, editedQuote); return REPLY_EDITED.Format(new { Quote = editedQuote }); } @@ -168,11 +182,21 @@ public string EditQuoteContext( [CommandSyntax("", "123")] [PrivilegeGuard(PrivilegeLevel.Moderator)] [CommandParameterHint(1, "id", CommandParameterType.Number)] - public string DeleteQuote([SourceChannel] string sourceChannel, [ArgumentIndex(0)] int quoteId) + public string DeleteQuote( + [SourceChannel] string sourceChannel, + [ArgumentIndex(0)] int quoteId, + [Author] ChatUser author + ) { - return _quoteService.DeleteQuote(sourceChannel, quoteId) - ? REPLY_DELETED.Format(new { QuoteId = quoteId }) - : REPLY_QUOTE_NOT_FOUND.Format(new { QuoteId = quoteId }); + var deleted = _quoteService.DeleteQuote(sourceChannel, quoteId); + + if (!deleted) + { + return REPLY_QUOTE_NOT_FOUND.Format(new { QuoteId = quoteId }); + } + + _auditor.QuoteDeleted(author, sourceChannel, quoteId); + return REPLY_DELETED.Format(new { QuoteId = quoteId }); } private string? DoQuote( @@ -221,7 +245,7 @@ public string DeleteQuote([SourceChannel] string sourceChannel, [ArgumentIndex(0 return REPLY_TEXT_MISSING; } - return EditQuote(sourceChannel, editQuoteId, subOpText); + return EditQuote(sourceChannel, editQuoteId, subOpText, author); } if (OpEditContext.Contains(op)) @@ -238,7 +262,7 @@ public string DeleteQuote([SourceChannel] string sourceChannel, [ArgumentIndex(0 return REPLY_CONTEXT_MISSING; } - return EditQuoteContext(sourceChannel, editQuoteId, subOpText); + return EditQuoteContext(sourceChannel, editQuoteId, subOpText, author); } if (OpDelete.Contains(op)) @@ -250,7 +274,7 @@ public string DeleteQuote([SourceChannel] string sourceChannel, [ArgumentIndex(0 return REPLY_QUOTE_ID_INVALID; } - return DeleteQuote(sourceChannel, deleteQuoteId); + return DeleteQuote(sourceChannel, deleteQuoteId, author); } _logger.LogInformation( diff --git a/src/FloppyBot.Commands.Aux.Quotes/Storage/QuoteAuditing.cs b/src/FloppyBot.Commands.Aux.Quotes/Storage/QuoteAuditing.cs new file mode 100644 index 0000000..6c36e7f --- /dev/null +++ b/src/FloppyBot.Commands.Aux.Quotes/Storage/QuoteAuditing.cs @@ -0,0 +1,69 @@ +using FloppyBot.Base.Auditing.Abstraction; +using FloppyBot.Chat.Entities; +using FloppyBot.Chat.Entities.Identifiers; +using FloppyBot.Commands.Aux.Quotes.Storage.Entities; + +namespace FloppyBot.Commands.Aux.Quotes.Storage; + +public static class QuoteAuditing +{ + public static void QuoteCreated( + this IAuditor auditor, + ChatUser user, + ChannelIdentifier channel, + Quote? quote + ) + { + if (quote is null) + { + return; + } + + auditor.Record( + user.Identifier, + channel, + quote, + q => q.QuoteId.ToString(), + CommonActions.Created, + quote.ToString() + ); + } + + public static void QuoteUpdated( + this IAuditor auditor, + ChatUser user, + ChannelIdentifier channel, + Quote? quote + ) + { + if (quote is null) + { + return; + } + + auditor.Record( + user.Identifier, + channel, + quote, + q => q.QuoteId.ToString(), + CommonActions.Updated, + quote.ToString() + ); + } + + public static void QuoteDeleted( + this IAuditor auditor, + ChatUser user, + ChannelIdentifier channel, + int quoteId + ) + { + auditor.Record( + user.Identifier, + channel, + nameof(Quote), + quoteId.ToString(), + CommonActions.Deleted + ); + } +} diff --git a/src/FloppyBot.Commands.Core/FloppyBot.Commands.Core.csproj b/src/FloppyBot.Commands.Core/FloppyBot.Commands.Core.csproj index 1f70d76..a162706 100644 --- a/src/FloppyBot.Commands.Core/FloppyBot.Commands.Core.csproj +++ b/src/FloppyBot.Commands.Core/FloppyBot.Commands.Core.csproj @@ -5,6 +5,8 @@ enable + + From aa68f686275571e5aefe26786a27357070783304 Mon Sep 17 00:00:00 2001 From: Raphael Guntersweiler Date: Sat, 1 Jun 2024 00:45:49 +0200 Subject: [PATCH 03/14] started adding quote auditing --- .../FloppyBot.WebApi.Agent.csproj | 5 ++++ src/FloppyBot.WebApi.Auth/AuthExtensions.cs | 11 ++++++++ .../Controllers/QuotesController.cs | 10 ++++++-- src/FloppyBot.WebApi.V2/Registration.cs | 25 +++++++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 src/FloppyBot.WebApi.V2/Registration.cs diff --git a/src/FloppyBot.WebApi.Agent/FloppyBot.WebApi.Agent.csproj b/src/FloppyBot.WebApi.Agent/FloppyBot.WebApi.Agent.csproj index d70f0a7..215af6b 100644 --- a/src/FloppyBot.WebApi.Agent/FloppyBot.WebApi.Agent.csproj +++ b/src/FloppyBot.WebApi.Agent/FloppyBot.WebApi.Agent.csproj @@ -25,4 +25,9 @@ + + + + EXCLUDE_WEB_API_V1 + \ No newline at end of file diff --git a/src/FloppyBot.WebApi.Auth/AuthExtensions.cs b/src/FloppyBot.WebApi.Auth/AuthExtensions.cs index 3333951..e69bd3c 100644 --- a/src/FloppyBot.WebApi.Auth/AuthExtensions.cs +++ b/src/FloppyBot.WebApi.Auth/AuthExtensions.cs @@ -1,4 +1,6 @@ using System.Security.Claims; +using FloppyBot.Chat.Entities; +using FloppyBot.Chat.Entities.Identifiers; namespace FloppyBot.WebApi.Auth; @@ -36,4 +38,13 @@ public static IEnumerable GetUserPermissions(this ClaimsPrincipal user) .Select(c => c.Value) .FirstOrDefault(); } + + public static ChatUser AsChatUser(this ClaimsPrincipal user) + { + return new ChatUser( + new ChannelIdentifier("WebApi", user.GetUserId()), + string.Empty, + PrivilegeLevel.Unknown + ); + } } diff --git a/src/FloppyBot.WebApi.V2/Controllers/QuotesController.cs b/src/FloppyBot.WebApi.V2/Controllers/QuotesController.cs index 66f9216..41ffb2e 100644 --- a/src/FloppyBot.WebApi.V2/Controllers/QuotesController.cs +++ b/src/FloppyBot.WebApi.V2/Controllers/QuotesController.cs @@ -1,3 +1,4 @@ +using FloppyBot.Base.Auditing.Abstraction; using FloppyBot.Commands.Aux.Quotes.Storage; using FloppyBot.WebApi.Auth; using FloppyBot.WebApi.Auth.Controllers; @@ -14,11 +15,13 @@ namespace FloppyBot.WebApi.V2.Controllers; public class QuotesController : ChannelScopedController { private readonly IQuoteService _quoteService; + private readonly IAuditor _auditor; - public QuotesController(IUserService userService, IQuoteService quoteService) + public QuotesController(IUserService userService, IQuoteService quoteService, IAuditor auditor) : base(userService) { _quoteService = quoteService; + _auditor = auditor; } [HttpGet] @@ -40,11 +43,13 @@ [FromBody] QuoteDto quote ) { var channelId = EnsureChannelAccess(messageInterface, channel); - if (!_quoteService.UpdateQuote(channelId, quoteId, quote.ToEntity())) + var quoteEntity = quote.ToEntity(); + if (!_quoteService.UpdateQuote(channelId, quoteId, quoteEntity)) { return NotFound(); } + _auditor.QuoteUpdated(User.AsChatUser(), channelId, quoteEntity); return NoContent(); } @@ -61,6 +66,7 @@ [FromRoute] int quoteId return NotFound(); } + _auditor.QuoteDeleted(User.AsChatUser(), channelId, quoteId); return NoContent(); } } diff --git a/src/FloppyBot.WebApi.V2/Registration.cs b/src/FloppyBot.WebApi.V2/Registration.cs new file mode 100644 index 0000000..4a780bf --- /dev/null +++ b/src/FloppyBot.WebApi.V2/Registration.cs @@ -0,0 +1,25 @@ +using FloppyBot.Base.Auditing.Abstraction; +using FloppyBot.Base.Auditing.Storage; +using FloppyBot.Commands.Aux.Quotes; +using FloppyBot.Commands.Aux.Twitch; +using FloppyBot.Commands.Custom.Execution; +using FloppyBot.Commands.Registry; +using FloppyBot.FileStorage; +using Microsoft.Extensions.DependencyInjection; + +namespace FloppyBot.WebApi.V2; + +public static class Registration +{ + public static IServiceCollection AddV2WebApi(this IServiceCollection services) + { + ShoutoutCommand.RegisterDependencies(services); + ShoutoutCommand.SetupTimerMessageDbDependencies(services); + QuoteCommands.RegisterDependencies(services); + CustomCommandHost.WebDiSetup(services); + return services + .AddFileStorage() + .AddDistributedCommandRegistry() + .AddAuditor(); + } +} From 646dee9e2b88e1a9827379f298bf53c2c32b6df0 Mon Sep 17 00:00:00 2001 From: Raphael Guntersweiler Date: Sat, 1 Jun 2024 01:22:16 +0200 Subject: [PATCH 04/14] auditing for shoutout commands --- .../ShoutoutCommandTests.cs | 74 +++++++++++++++++- .../ShoutoutCommand.cs | 23 +++++- .../TwitchAuditing.cs | 77 +++++++++++++++++++ .../ShoutoutCommandConfigController.cs | 11 ++- 4 files changed, 175 insertions(+), 10 deletions(-) create mode 100644 src/FloppyBot.Commands.Aux.Twitch/TwitchAuditing.cs diff --git a/src/FloppyBot.Commands.Aux.Twitch.Tests/ShoutoutCommandTests.cs b/src/FloppyBot.Commands.Aux.Twitch.Tests/ShoutoutCommandTests.cs index 19a7f41..669f67d 100644 --- a/src/FloppyBot.Commands.Aux.Twitch.Tests/ShoutoutCommandTests.cs +++ b/src/FloppyBot.Commands.Aux.Twitch.Tests/ShoutoutCommandTests.cs @@ -1,4 +1,7 @@ using FakeItEasy; +using FloppyBot.Base.Auditing.Abstraction; +using FloppyBot.Base.Auditing.Abstraction.Entities; +using FloppyBot.Chat.Entities; using FloppyBot.Commands.Aux.Twitch.Api; using FloppyBot.Commands.Aux.Twitch.Storage; using FloppyBot.Commands.Aux.Twitch.Storage.Entities; @@ -11,15 +14,19 @@ public class ShoutoutCommandTests { private readonly ShoutoutCommand _shoutoutCommand; private readonly IShoutoutMessageSettingService _shoutoutMessageSettingService; + private readonly IAuditor _auditor; public ShoutoutCommandTests() { + _auditor = A.Fake(); + var twitchApiService = A.Fake(); _shoutoutMessageSettingService = A.Fake(); _shoutoutCommand = new ShoutoutCommand( twitchApiService, _shoutoutMessageSettingService, - A.Fake>() + A.Fake>(), + _auditor ); A.CallTo(() => twitchApiService.LookupUser(A.Ignored)) @@ -110,7 +117,11 @@ public async Task DoNotReplyIfNoUserCanBeFound() [TestMethod] public void ConfigureCreatesDatabaseRecord() { - var reply = _shoutoutCommand.SetShoutout("Twitch/someuser", "My new template"); + var reply = _shoutoutCommand.SetShoutout( + new ChatUser("Twitch/someuser", "Some User", PrivilegeLevel.Moderator), + "Twitch/someuser", + "My new template" + ); Assert.AreEqual(ShoutoutCommand.REPLY_SAVE, reply); A.CallTo( @@ -121,12 +132,32 @@ public void ConfigureCreatesDatabaseRecord() ) ) .MustHaveHappenedOnceExactly(); + A.CallTo( + () => + _auditor.Record( + new AuditRecord( + null!, + DateTimeOffset.MinValue, + "Twitch/someuser", + "Twitch/someuser", + TwitchAuditing.ShoutoutMessageType, + "Message", + CommonActions.Updated, + "My new template" + ) + ) + ) + .MustHaveHappenedOnceExactly(); } [TestMethod] public void ConfigureTeamShoutoutSavesToDatabase() { - var reply = _shoutoutCommand.SetTeamShoutout("Twitch/someuser", "My new team template"); + var reply = _shoutoutCommand.SetTeamShoutout( + new ChatUser("Twitch/someuser", "Some User", PrivilegeLevel.Moderator), + "Twitch/someuser", + "My new team template" + ); Assert.AreEqual(ShoutoutCommand.REPLY_SAVE, reply); A.CallTo( @@ -139,15 +170,50 @@ public void ConfigureTeamShoutoutSavesToDatabase() ) ) .MustHaveHappenedOnceExactly(); + A.CallTo( + () => + _auditor.Record( + new AuditRecord( + null!, + DateTimeOffset.MinValue, + "Twitch/someuser", + "Twitch/someuser", + TwitchAuditing.ShoutoutMessageType, + "TeamMessage", + CommonActions.Updated, + "My new team template" + ) + ) + ) + .MustHaveHappenedOnceExactly(); } [TestMethod] public void ClearIssuesDeleteCommand() { - var reply = _shoutoutCommand.ClearShoutout("Twitch/someuser"); + var reply = _shoutoutCommand.ClearShoutout( + new ChatUser("Twitch/someuser", "Some User", PrivilegeLevel.Moderator), + "Twitch/someuser" + ); Assert.AreEqual(ShoutoutCommand.REPLY_CLEAR, reply); A.CallTo(() => _shoutoutMessageSettingService.ClearSettings("Twitch/someuser")) .MustHaveHappenedOnceExactly(); + A.CallTo( + () => + _auditor.Record( + new AuditRecord( + null!, + DateTimeOffset.MinValue, + "Twitch/someuser", + "Twitch/someuser", + TwitchAuditing.ShoutoutMessageType, + string.Empty, + CommonActions.Deleted, + null + ) + ) + ) + .MustHaveHappenedOnceExactly(); } } diff --git a/src/FloppyBot.Commands.Aux.Twitch/ShoutoutCommand.cs b/src/FloppyBot.Commands.Aux.Twitch/ShoutoutCommand.cs index cec1f1c..1b5223e 100644 --- a/src/FloppyBot.Commands.Aux.Twitch/ShoutoutCommand.cs +++ b/src/FloppyBot.Commands.Aux.Twitch/ShoutoutCommand.cs @@ -1,4 +1,5 @@ using FloppyBot.Aux.MessageCounter.Core; +using FloppyBot.Base.Auditing.Abstraction; using FloppyBot.Base.Cron; using FloppyBot.Base.TextFormatting; using FloppyBot.Chat.Entities; @@ -39,16 +40,19 @@ public class ShoutoutCommand private readonly IShoutoutMessageSettingService _shoutoutMessageSettingService; private readonly ITwitchApiService _twitchApiService; + private readonly IAuditor _auditor; public ShoutoutCommand( ITwitchApiService twitchApiService, IShoutoutMessageSettingService shoutoutMessageSettingService, - ILogger logger + ILogger logger, + IAuditor auditor ) { _twitchApiService = twitchApiService; _shoutoutMessageSettingService = shoutoutMessageSettingService; _logger = logger; + _auditor = auditor; } [DependencyRegistration] @@ -170,9 +174,14 @@ userTeamLookup is not null "Shoutout to {DisplayName} at {Link}. They last played {LastGame}!" )] // ReSharper disable once UnusedMember.Global - public string SetShoutout([SourceChannel] string sourceChannel, [AllArguments] string template) + public string SetShoutout( + [Author] ChatUser author, + [SourceChannel] ChannelIdentifier sourceChannel, + [AllArguments] string template + ) { _shoutoutMessageSettingService.SetShoutoutMessage(sourceChannel, template); + _auditor.ShoutoutMessageSet(author, sourceChannel, template); return REPLY_SAVE; } @@ -190,7 +199,8 @@ public string SetShoutout([SourceChannel] string sourceChannel, [AllArguments] s + $"{nameof(MessageTemplateParams.TeamLink)}" )] public string? SetTeamShoutout( - [SourceChannel] string sourceChannel, + [Author] ChatUser author, + [SourceChannel] ChannelIdentifier sourceChannel, [AllArguments] string template ) { @@ -204,6 +214,7 @@ settings with TeamMessage = template, } ); + _auditor.TeamShoutoutMessageSet(author, sourceChannel, template); return REPLY_SAVE; } @@ -212,9 +223,13 @@ settings with "Clears the channels shoutout message, effectively disabling the shoutout command" )] // ReSharper disable once UnusedMember.Global - public string ClearShoutout([SourceChannel] string sourceChannel) + public string ClearShoutout( + [Author] ChatUser author, + [SourceChannel] ChannelIdentifier sourceChannel + ) { _shoutoutMessageSettingService.ClearSettings(sourceChannel); + _auditor.ShoutoutMessageCleared(author, sourceChannel); return REPLY_CLEAR; } } diff --git a/src/FloppyBot.Commands.Aux.Twitch/TwitchAuditing.cs b/src/FloppyBot.Commands.Aux.Twitch/TwitchAuditing.cs new file mode 100644 index 0000000..71064ef --- /dev/null +++ b/src/FloppyBot.Commands.Aux.Twitch/TwitchAuditing.cs @@ -0,0 +1,77 @@ +using FloppyBot.Base.Auditing.Abstraction; +using FloppyBot.Chat.Entities; +using FloppyBot.Chat.Entities.Identifiers; +using FloppyBot.Commands.Aux.Twitch.Storage.Entities; + +namespace FloppyBot.Commands.Aux.Twitch; + +public static class TwitchAuditing +{ + public const string ShoutoutMessageType = "ShoutoutMessage"; + + public static void ShoutoutMessageSet( + this IAuditor auditor, + ChatUser user, + ChannelIdentifier channel, + ShoutoutMessageSetting message + ) + { + auditor.Record( + user.Identifier, + channel, + ShoutoutMessageType, + string.Empty, + CommonActions.Updated, + message.ToString() + ); + } + + public static void ShoutoutMessageSet( + this IAuditor auditor, + ChatUser user, + ChannelIdentifier channel, + string message + ) + { + auditor.Record( + user.Identifier, + channel, + ShoutoutMessageType, + nameof(ShoutoutMessageSetting.Message), + CommonActions.Updated, + message + ); + } + + public static void TeamShoutoutMessageSet( + this IAuditor auditor, + ChatUser user, + ChannelIdentifier channel, + string message + ) + { + auditor.Record( + user.Identifier, + channel, + ShoutoutMessageType, + nameof(ShoutoutMessageSetting.TeamMessage), + CommonActions.Updated, + message + ); + } + + public static void ShoutoutMessageCleared( + this IAuditor auditor, + ChatUser user, + ChannelIdentifier channel + ) + { + auditor.Record( + user.Identifier, + channel, + ShoutoutMessageType, + string.Empty, + CommonActions.Deleted + ); + } +} diff --git a/src/FloppyBot.WebApi.V2/Controllers/ShoutoutCommandConfigController.cs b/src/FloppyBot.WebApi.V2/Controllers/ShoutoutCommandConfigController.cs index 00d5421..a60afea 100644 --- a/src/FloppyBot.WebApi.V2/Controllers/ShoutoutCommandConfigController.cs +++ b/src/FloppyBot.WebApi.V2/Controllers/ShoutoutCommandConfigController.cs @@ -1,3 +1,5 @@ +using FloppyBot.Base.Auditing.Abstraction; +using FloppyBot.Commands.Aux.Twitch; using FloppyBot.Commands.Aux.Twitch.Storage; using FloppyBot.WebApi.Auth; using FloppyBot.WebApi.Auth.Controllers; @@ -14,14 +16,17 @@ namespace FloppyBot.WebApi.V2.Controllers; public class ShoutoutCommandConfigController : ChannelScopedController { private readonly IShoutoutMessageSettingService _shoutoutMessageSettingService; + private readonly IAuditor _auditor; public ShoutoutCommandConfigController( IUserService userService, - IShoutoutMessageSettingService shoutoutMessageSettingService + IShoutoutMessageSettingService shoutoutMessageSettingService, + IAuditor auditor ) : base(userService) { _shoutoutMessageSettingService = shoutoutMessageSettingService; + _auditor = auditor; } [HttpGet] @@ -49,7 +54,9 @@ [FromBody] ShoutoutCommandConfig config ) { var channelId = EnsureChannelAccess(messageInterface, channel); - _shoutoutMessageSettingService.SetShoutoutMessage(config.ToEntity(channelId)); + var entity = config.ToEntity(channelId); + _shoutoutMessageSettingService.SetShoutoutMessage(entity); + _auditor.ShoutoutMessageSet(User.AsChatUser(), channelId, entity); return NoContent(); } } From e971f90a8a52efef79305ff118372bd1fd7d2808 Mon Sep 17 00:00:00 2001 From: Raphael Guntersweiler Date: Sat, 1 Jun 2024 01:33:07 +0200 Subject: [PATCH 05/14] added auditing for sub alert messages --- .../CommonActions.cs | 1 + .../SubAlertCommands.cs | 16 ++++-- .../TwitchAuditing.cs | 49 +++++++++++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/FloppyBot.Base.Auditing.Abstraction/CommonActions.cs b/src/FloppyBot.Base.Auditing.Abstraction/CommonActions.cs index be1e03a..244bd6d 100644 --- a/src/FloppyBot.Base.Auditing.Abstraction/CommonActions.cs +++ b/src/FloppyBot.Base.Auditing.Abstraction/CommonActions.cs @@ -5,4 +5,5 @@ public static class CommonActions public const string Created = "Created"; public const string Updated = "Updated"; public const string Deleted = "Deleted"; + public const string Disabled = "Disabled"; } diff --git a/src/FloppyBot.Commands.Aux.Twitch/SubAlertCommands.cs b/src/FloppyBot.Commands.Aux.Twitch/SubAlertCommands.cs index e53937f..1064960 100644 --- a/src/FloppyBot.Commands.Aux.Twitch/SubAlertCommands.cs +++ b/src/FloppyBot.Commands.Aux.Twitch/SubAlertCommands.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using FloppyBot.Aux.TwitchAlerts.Core; using FloppyBot.Aux.TwitchAlerts.Core.Entities; +using FloppyBot.Base.Auditing.Abstraction; using FloppyBot.Chat.Entities; using FloppyBot.Commands.Core.Attributes; using FloppyBot.Commands.Core.Attributes.Args; @@ -24,10 +25,12 @@ public class SubAlertCommands "{User} just subscribed with {SubscriptionTier}! Thank you so much for the support! 🎉"; private readonly ITwitchAlertService _alertService; + private readonly IAuditor _auditor; - public SubAlertCommands(ITwitchAlertService alertService) + public SubAlertCommands(ITwitchAlertService alertService, IAuditor auditor) { _alertService = alertService; + _auditor = auditor; } [DependencyRegistration] @@ -42,6 +45,7 @@ public static void RegisterDependencies(IServiceCollection services) [CommandSyntax("")] // ReSharper disable once UnusedMember.Global public string SetAlertMessage( + [Author] ChatUser author, [SourceChannel] string sourceChannel, [AllArguments] string message ) @@ -59,6 +63,7 @@ [AllArguments] string message }; _alertService.StoreAlertSettings(settings); + _auditor.SubAlertMessageSet(author, sourceChannel, message); return REPLY_ALERT_SET; } @@ -66,13 +71,14 @@ [AllArguments] string message [CommandDescription("Clears the message to be sent when someone subscribes to the channel.")] [CommandSyntax("")] // ReSharper disable once UnusedMember.Global - public string ClearAlertMessage([SourceChannel] string sourceChannel) + public string ClearAlertMessage([Author] ChatUser author, [SourceChannel] string sourceChannel) { var settings = _alertService.GetAlertSettings(sourceChannel); if (settings is not null) { settings = settings with { SubAlertsEnabled = false, }; _alertService.StoreAlertSettings(settings); + _auditor.SubAlertMessageDisabled(author, sourceChannel); } return REPLY_ALERT_CLEAR; @@ -82,7 +88,10 @@ public string ClearAlertMessage([SourceChannel] string sourceChannel) [CommandDescription("Resets the sub alert message to the default message")] [CommandSyntax("")] // ReSharper disable once UnusedMember.Global - public string SetDefaultAlertMessage([SourceChannel] string sourceChannel) + public string SetDefaultAlertMessage( + [Author] ChatUser author, + [SourceChannel] string sourceChannel + ) { var settings = _alertService.GetAlertSettings(sourceChannel) @@ -97,6 +106,7 @@ public string SetDefaultAlertMessage([SourceChannel] string sourceChannel) }; _alertService.StoreAlertSettings(settings); + _auditor.SubAlertMessageCleared(author, sourceChannel); return REPLY_ALERT_SET; } } diff --git a/src/FloppyBot.Commands.Aux.Twitch/TwitchAuditing.cs b/src/FloppyBot.Commands.Aux.Twitch/TwitchAuditing.cs index 71064ef..c78ed3f 100644 --- a/src/FloppyBot.Commands.Aux.Twitch/TwitchAuditing.cs +++ b/src/FloppyBot.Commands.Aux.Twitch/TwitchAuditing.cs @@ -1,3 +1,4 @@ +using FloppyBot.Aux.TwitchAlerts.Core.Entities; using FloppyBot.Base.Auditing.Abstraction; using FloppyBot.Chat.Entities; using FloppyBot.Chat.Entities.Identifiers; @@ -8,6 +9,7 @@ namespace FloppyBot.Commands.Aux.Twitch; public static class TwitchAuditing { public const string ShoutoutMessageType = "ShoutoutMessage"; + public const string SubAlertMessageType = "SubAlertMessage"; public static void ShoutoutMessageSet( this IAuditor auditor, @@ -74,4 +76,51 @@ ChannelIdentifier channel CommonActions.Deleted ); } + + public static void SubAlertMessageSet( + this IAuditor auditor, + ChatUser user, + ChannelIdentifier channel, + string message + ) + { + auditor.Record( + user.Identifier, + channel, + nameof(TwitchAlertSettings), + nameof(TwitchAlertSettings.SubMessage), + CommonActions.Updated, + message + ); + } + + public static void SubAlertMessageDisabled( + this IAuditor auditor, + ChatUser user, + ChannelIdentifier channel + ) + { + auditor.Record( + user.Identifier, + channel, + nameof(TwitchAlertSettings), + nameof(TwitchAlertSettings.SubMessage), + CommonActions.Disabled + ); + } + + public static void SubAlertMessageCleared( + this IAuditor auditor, + ChatUser user, + ChannelIdentifier channel + ) + { + auditor.Record( + user.Identifier, + channel, + nameof(TwitchAlertSettings), + nameof(TwitchAlertSettings.SubMessage), + CommonActions.Deleted + ); + } } From bf39683df1aa71364abe568fc3b306a29c11a9b9 Mon Sep 17 00:00:00 2001 From: Raphael Guntersweiler Date: Sat, 1 Jun 2024 12:44:38 +0200 Subject: [PATCH 06/14] timer messages are now audited --- .../TimerCommandsTests.cs | 30 +++++++++++++++++-- .../TimerAuditing.cs | 29 ++++++++++++++++++ .../TimerCommands.cs | 13 ++++++-- 3 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 src/FloppyBot.Commands.Aux.Timer/TimerAuditing.cs diff --git a/src/FloppyBot.Commands.Aux.Timer.Tests/TimerCommandsTests.cs b/src/FloppyBot.Commands.Aux.Timer.Tests/TimerCommandsTests.cs index 6e9bc1e..aa6739c 100644 --- a/src/FloppyBot.Commands.Aux.Timer.Tests/TimerCommandsTests.cs +++ b/src/FloppyBot.Commands.Aux.Timer.Tests/TimerCommandsTests.cs @@ -1,4 +1,6 @@ using FakeItEasy; +using FloppyBot.Base.Auditing.Abstraction; +using FloppyBot.Base.Auditing.Abstraction.Entities; using FloppyBot.Base.Testing; using FloppyBot.Chat.Entities; using FloppyBot.Commands.Aux.Timer.Storage; @@ -11,11 +13,13 @@ public class TimerCommandsTests { private readonly TimerCommands _host; private readonly ITimerService _timerService; + private readonly IAuditor _auditor; public TimerCommandsTests() { _timerService = A.Fake(); - _host = new TimerCommands(LoggingUtils.GetLogger(), _timerService); + _auditor = A.Fake(); + _host = new TimerCommands(LoggingUtils.GetLogger(), _timerService, _auditor); } [TestMethod] @@ -25,7 +29,8 @@ public void CreateNewTimer() "12m", "Hello World", new ChatUser("Mock/User", "User", PrivilegeLevel.Moderator), - "Mock/Channel/Message" + "Mock/Channel/Message", + "Mock/Channel" ); Assert.AreEqual( @@ -44,6 +49,22 @@ public void CreateNewTimer() ) ) .MustHaveHappenedOnceExactly(); + A.CallTo( + () => + _auditor.Record( + new AuditRecord( + null!, + DateTimeOffset.MinValue, + "Mock/User", + "Mock/Channel", + TimerAuditing.TimerType, + "Mock/Channel/Message", + CommonActions.Created, + "[00:12:00]: Hello World" + ) + ) + ) + .MustHaveHappenedOnceExactly(); } [DataTestMethod] @@ -57,8 +78,11 @@ public void RepliesWithErrorWhenParsingFails(string input) input, "Some Text", new ChatUser("Mock/User", "User", PrivilegeLevel.Moderator), - "Mock/Channel/Message" + "Mock/Channel/Message", + "Mock/Channel" ) ); + + A.CallTo(() => _auditor.Record(A._)).MustNotHaveHappened(); } } diff --git a/src/FloppyBot.Commands.Aux.Timer/TimerAuditing.cs b/src/FloppyBot.Commands.Aux.Timer/TimerAuditing.cs new file mode 100644 index 0000000..4585489 --- /dev/null +++ b/src/FloppyBot.Commands.Aux.Timer/TimerAuditing.cs @@ -0,0 +1,29 @@ +using FloppyBot.Base.Auditing.Abstraction; +using FloppyBot.Chat.Entities; +using FloppyBot.Chat.Entities.Identifiers; + +namespace FloppyBot.Commands.Aux.Timer; + +public static class TimerAuditing +{ + public const string TimerType = "Timer"; + + public static void TimerCreated( + this IAuditor auditor, + ChatUser user, + ChannelIdentifier channel, + string sourceMessageId, + string timerMessage, + TimeSpan timeSpan + ) + { + auditor.Record( + user.Identifier, + channel, + TimerType, + sourceMessageId, + CommonActions.Created, + $"[{timeSpan}]: {timerMessage}" + ); + } +} diff --git a/src/FloppyBot.Commands.Aux.Timer/TimerCommands.cs b/src/FloppyBot.Commands.Aux.Timer/TimerCommands.cs index 1cc9426..6bfe0c3 100644 --- a/src/FloppyBot.Commands.Aux.Timer/TimerCommands.cs +++ b/src/FloppyBot.Commands.Aux.Timer/TimerCommands.cs @@ -1,3 +1,4 @@ +using FloppyBot.Base.Auditing.Abstraction; using FloppyBot.Base.Cron; using FloppyBot.Base.TextFormatting; using FloppyBot.Chat.Entities; @@ -25,11 +26,17 @@ public class TimerCommands private readonly ILogger _logger; private readonly ITimerService _timerService; + private readonly IAuditor _auditor; - public TimerCommands(ILogger logger, ITimerService timerService) + public TimerCommands( + ILogger logger, + ITimerService timerService, + IAuditor auditor + ) { _logger = logger; _timerService = timerService; + _auditor = auditor; } [DependencyRegistration] @@ -48,7 +55,8 @@ public CommandResult CreateTimer( [ArgumentIndex(0)] string timeExpression, [ArgumentRange(1)] string timerMessage, [Author] ChatUser author, - [SourceMessageIdentifier] ChatMessageIdentifier sourceMessageId + [SourceMessageIdentifier] ChatMessageIdentifier sourceMessageId, + [SourceChannel] ChannelIdentifier sourceChannel ) { TimeSpan? timespan = TimeExpressionParser.ParseTimeExpression(timeExpression); @@ -58,6 +66,7 @@ [SourceMessageIdentifier] ChatMessageIdentifier sourceMessageId } _timerService.CreateTimer(sourceMessageId, author.Identifier, timespan.Value, timerMessage); + _auditor.TimerCreated(author, sourceChannel, sourceMessageId, timerMessage, timespan.Value); return CommandResult.SuccessWith(REPLY_CREATED.Format(new { Time = timespan })); } } From 84c54912c7d0d7a5527d8fd895ad00597fc0d1f5 Mon Sep 17 00:00:00 2001 From: Raphael Guntersweiler Date: Sat, 1 Jun 2024 18:18:22 +0200 Subject: [PATCH 07/14] custom commands are now audited --- .../CustomCommandAdministrationCommands.cs | 46 +++++- .../Auditing/CustomCommandAuditing.cs | 74 +++++++++ .../FloppyBot.Commands.Custom.Storage.csproj | 1 + ...ustomCommandAdministrationCommandsTests.cs | 145 +++++++++++++++++- 4 files changed, 251 insertions(+), 15 deletions(-) create mode 100644 src/FloppyBot.Commands.Custom.Storage/Auditing/CustomCommandAuditing.cs diff --git a/src/FloppyBot.Commands.Custom.Execution/Administration/CustomCommandAdministrationCommands.cs b/src/FloppyBot.Commands.Custom.Execution/Administration/CustomCommandAdministrationCommands.cs index 804d4be..e9b7520 100644 --- a/src/FloppyBot.Commands.Custom.Execution/Administration/CustomCommandAdministrationCommands.cs +++ b/src/FloppyBot.Commands.Custom.Execution/Administration/CustomCommandAdministrationCommands.cs @@ -1,3 +1,4 @@ +using FloppyBot.Base.Auditing.Abstraction; using FloppyBot.Base.Extensions; using FloppyBot.Base.TextFormatting; using FloppyBot.Chat.Entities; @@ -7,6 +8,7 @@ using FloppyBot.Commands.Core.Attributes.Metadata; using FloppyBot.Commands.Core.Entities; using FloppyBot.Commands.Custom.Storage; +using FloppyBot.Commands.Custom.Storage.Auditing; namespace FloppyBot.Commands.Custom.Execution.Administration; @@ -35,20 +37,24 @@ public class CustomCommandAdministrationCommands private readonly ICustomCommandService _commandService; private readonly ICounterStorageService _counterStorageService; + private readonly IAuditor _auditor; public CustomCommandAdministrationCommands( ICustomCommandService commandService, - ICounterStorageService counterStorageService + ICounterStorageService counterStorageService, + IAuditor auditor ) { _commandService = commandService; _counterStorageService = counterStorageService; + _auditor = auditor; } [Command("newcmd")] [CommandDescription("Creates a new custom text command")] [CommandSyntax(" ")] public CommandResult CreateCommand( + [Author] ChatUser author, [SourceChannel] string sourceChannel, [ArgumentIndex(0)] string commandName, [ArgumentRange(1)] string commandResponse @@ -60,24 +66,40 @@ public CommandResult CreateCommand( commandResponse ); var formatParams = new CustomCommandFormattingParams(commandName); - return created - ? new CommandResult(CommandOutcome.Success, REPLY_CREATE_SUCCESS.Format(formatParams)) - : new CommandResult(CommandOutcome.Failed, REPLY_CREATE_FAILED.Format(formatParams)); + if (!created) + { + return new CommandResult( + CommandOutcome.Failed, + REPLY_CREATE_FAILED.Format(formatParams) + ); + } + + _auditor.CommandCreated(author, sourceChannel, commandName, commandResponse); + return new CommandResult(CommandOutcome.Success, REPLY_CREATE_SUCCESS.Format(formatParams)); } [Command("deletecmd")] [CommandDescription("Deletes a custom text command")] [CommandSyntax("")] public CommandResult DeleteCommand( + [Author] ChatUser author, [SourceChannel] string sourceChannel, [ArgumentIndex(0)] string commandName ) { bool deleted = _commandService.DeleteCommand(sourceChannel, commandName); var formatParams = new CustomCommandFormattingParams(commandName); - return deleted - ? new CommandResult(CommandOutcome.Success, REPLY_DELETE_SUCCESS.Format(formatParams)) - : new CommandResult(CommandOutcome.Failed, REPLY_DELETE_FAILED.Format(formatParams)); + + if (!deleted) + { + return new CommandResult( + CommandOutcome.Failed, + REPLY_DELETE_FAILED.Format(formatParams) + ); + } + + _auditor.CommandDeleted(author, sourceChannel, commandName); + return new CommandResult(CommandOutcome.Success, REPLY_DELETE_SUCCESS.Format(formatParams)); } [Command("counter", "count")] @@ -85,6 +107,7 @@ public CommandResult DeleteCommand( [CommandDescription("Sets the counter for a custom command")] [CommandSyntax(" |")] public CommandResult SetCounter( + [Author] ChatUser author, [SourceChannel] string sourceChannel, [ArgumentIndex(0, stopIfMissing: true)] string commandName, [ArgumentIndex(1, stopIfMissing: true)] string operationOrValue @@ -108,6 +131,7 @@ public CommandResult SetCounter( { case CMD_COUNTER_CLEAR: _counterStorageService.Set(commandId, 0); + _auditor.CounterUpdated(author, sourceChannel, commandName, 0); return CreateCounterResult(formatParams with { Counter = 0 }); } @@ -128,6 +152,13 @@ public CommandResult SetCounter( { // Relative int incrementedValue = _counterStorageService.Increase(commandId, incrementValue); + _auditor.CounterUpdated( + author, + sourceChannel, + commandName, + incrementedValue, + incrementValue + ); return CreateCounterResult(formatParams with { Counter = incrementedValue }); } @@ -135,6 +166,7 @@ public CommandResult SetCounter( { // Absolute _counterStorageService.Set(commandId, newValue); + _auditor.CounterUpdated(author, sourceChannel, commandName, newValue); return CreateCounterResult(formatParams with { Counter = newValue }); } diff --git a/src/FloppyBot.Commands.Custom.Storage/Auditing/CustomCommandAuditing.cs b/src/FloppyBot.Commands.Custom.Storage/Auditing/CustomCommandAuditing.cs new file mode 100644 index 0000000..df2c052 --- /dev/null +++ b/src/FloppyBot.Commands.Custom.Storage/Auditing/CustomCommandAuditing.cs @@ -0,0 +1,74 @@ +using System.Diagnostics; +using FloppyBot.Base.Auditing.Abstraction; +using FloppyBot.Chat.Entities; +using FloppyBot.Chat.Entities.Identifiers; + +namespace FloppyBot.Commands.Custom.Storage.Auditing; + +public static class CustomCommandActions +{ + public const string CounterUpdated = "CounterUpdated"; +} + +[StackTraceHidden] +public static class CustomCommandAuditing +{ + public const string CustomCommandType = "CustomCommand"; + + public static void CommandCreated( + this IAuditor auditor, + ChatUser author, + ChannelIdentifier channel, + string commandName, + string response + ) + { + auditor.Record( + author.Identifier, + channel, + CustomCommandType, + commandName, + CommonActions.Created, + response + ); + } + + public static void CommandDeleted( + this IAuditor auditor, + ChatUser author, + ChannelIdentifier channel, + string commandName + ) + { + auditor.Record( + author.Identifier, + channel, + CustomCommandType, + commandName, + CommonActions.Deleted + ); + } + + public static void CounterUpdated( + this IAuditor auditor, + ChatUser author, + ChannelIdentifier channel, + string commandName, + int value, + int? increment = null + ) + { + var incrementStr = increment.HasValue + ? $"({(increment > 0 ? "+" : string.Empty)}{increment})" + : string.Empty; + var detail = $"{value} {incrementStr}".Trim(); + auditor.Record( + author.Identifier, + channel, + CustomCommandType, + commandName, + CustomCommandActions.CounterUpdated, + detail + ); + } +} diff --git a/src/FloppyBot.Commands.Custom.Storage/FloppyBot.Commands.Custom.Storage.csproj b/src/FloppyBot.Commands.Custom.Storage/FloppyBot.Commands.Custom.Storage.csproj index cf1a1b7..310788a 100644 --- a/src/FloppyBot.Commands.Custom.Storage/FloppyBot.Commands.Custom.Storage.csproj +++ b/src/FloppyBot.Commands.Custom.Storage/FloppyBot.Commands.Custom.Storage.csproj @@ -5,6 +5,7 @@ enable + diff --git a/src/FloppyBot.Commands.Custom.Tests/Execution/CustomCommandAdministrationCommandsTests.cs b/src/FloppyBot.Commands.Custom.Tests/Execution/CustomCommandAdministrationCommandsTests.cs index 17586e2..92d6a46 100644 --- a/src/FloppyBot.Commands.Custom.Tests/Execution/CustomCommandAdministrationCommandsTests.cs +++ b/src/FloppyBot.Commands.Custom.Tests/Execution/CustomCommandAdministrationCommandsTests.cs @@ -1,8 +1,12 @@ using FakeItEasy; +using FloppyBot.Base.Auditing.Abstraction; +using FloppyBot.Base.Auditing.Abstraction.Entities; using FloppyBot.Base.TextFormatting; +using FloppyBot.Chat.Entities; using FloppyBot.Commands.Core.Entities; using FloppyBot.Commands.Custom.Execution.Administration; using FloppyBot.Commands.Custom.Storage; +using FloppyBot.Commands.Custom.Storage.Auditing; using FloppyBot.Commands.Custom.Storage.Entities; namespace FloppyBot.Commands.Custom.Tests.Execution; @@ -13,14 +17,17 @@ public class CustomCommandAdministrationCommandsTests private readonly ICounterStorageService _counterStorageService; private readonly ICustomCommandService _customCommandService; private readonly CustomCommandAdministrationCommands _host; + private readonly IAuditor _auditor; public CustomCommandAdministrationCommandsTests() { _counterStorageService = A.Fake(); _customCommandService = A.Fake(); + _auditor = A.Fake(); _host = new CustomCommandAdministrationCommands( _customCommandService, - _counterStorageService + _counterStorageService, + _auditor ); } @@ -38,6 +45,7 @@ public void CreateCommand() .ReturnsLazily(() => true); CommandResult result = _host.CreateCommand( + new ChatUser("Mock/User", "MockUser", PrivilegeLevel.Unknown), "Mock/UnitTest", "mycommand", "This is my cool command" @@ -61,6 +69,22 @@ public void CreateCommand() ) ) .MustHaveHappenedOnceExactly(); + A.CallTo( + () => + _auditor.Record( + new AuditRecord( + null!, + DateTimeOffset.MinValue, + "Mock/User", + "Mock/UnitTest", + CustomCommandAuditing.CustomCommandType, + "mycommand", + CommonActions.Created, + "This is my cool command" + ) + ) + ) + .MustHaveHappenedOnceExactly(); } [TestMethod] @@ -77,6 +101,7 @@ public void CreateExisting() .ReturnsLazily(() => false); CommandResult result = _host.CreateCommand( + new ChatUser("Mock/User", "MockUser", PrivilegeLevel.Unknown), "Mock/UnitTest", "mycommand", "This is my cool command" @@ -100,6 +125,7 @@ public void CreateExisting() ) ) .MustHaveHappenedOnceExactly(); + A.CallTo(() => _auditor.Record(An._)).MustNotHaveHappened(); } [TestMethod] @@ -108,7 +134,11 @@ public void DeleteCommand() A.CallTo(() => _customCommandService.DeleteCommand(A.Ignored, A.Ignored)) .ReturnsLazily(() => true); - CommandResult result = _host.DeleteCommand("Mock/UnitTest", "mycommand"); + CommandResult result = _host.DeleteCommand( + new ChatUser("Mock/User", "MockUser", PrivilegeLevel.Unknown), + "Mock/UnitTest", + "mycommand" + ); Assert.AreEqual( new CommandResult( CommandOutcome.Success, @@ -118,6 +148,23 @@ public void DeleteCommand() ), result ); + + A.CallTo( + () => + _auditor.Record( + new AuditRecord( + null!, + DateTimeOffset.MinValue, + "Mock/User", + "Mock/UnitTest", + CustomCommandAuditing.CustomCommandType, + "mycommand", + CommonActions.Deleted, + null + ) + ) + ) + .MustHaveHappenedOnceExactly(); } [TestMethod] @@ -126,7 +173,11 @@ public void DeleteFailed() A.CallTo(() => _customCommandService.DeleteCommand(A.Ignored, A.Ignored)) .ReturnsLazily(() => false); - CommandResult result = _host.DeleteCommand("Mock/UnitTest", "mycommand"); + CommandResult result = _host.DeleteCommand( + new ChatUser("Mock/User", "MockUser", PrivilegeLevel.Unknown), + "Mock/UnitTest", + "mycommand" + ); Assert.AreEqual( new CommandResult( CommandOutcome.Failed, @@ -136,6 +187,7 @@ public void DeleteFailed() ), result ); + A.CallTo(() => _auditor.Record(An._)).MustNotHaveHappened(); } [TestMethod] @@ -145,7 +197,12 @@ public void SetCounterAbsolute() .ReturnsLazily(() => new CustomCommandDescription { Id = "abc123" }); A.CallTo(() => _counterStorageService.Peek("abc123")).ReturnsLazily(() => 5); - CommandResult result = _host.SetCounter("Mock/UnitTest", "mycommand", "5"); + CommandResult result = _host.SetCounter( + new ChatUser("Mock/User", "MockUser", PrivilegeLevel.Unknown), + "Mock/UnitTest", + "mycommand", + "5" + ); Assert.AreEqual( new CommandResult( CommandOutcome.Success, @@ -156,6 +213,22 @@ public void SetCounterAbsolute() result ); A.CallTo(() => _counterStorageService.Set("abc123", 5)).MustHaveHappenedOnceExactly(); + A.CallTo( + () => + _auditor.Record( + new AuditRecord( + null!, + DateTimeOffset.MinValue, + "Mock/User", + "Mock/UnitTest", + CustomCommandAuditing.CustomCommandType, + "mycommand", + CustomCommandActions.CounterUpdated, + "5" + ) + ) + ) + .MustHaveHappenedOnceExactly(); } [DataTestMethod] @@ -168,7 +241,12 @@ public void SetCounterRelative(string input, int expectedIncrement) A.CallTo(() => _counterStorageService.Increase(A.Ignored, An.Ignored)) .ReturnsLazily((string _, int increment) => 10 + increment); - CommandResult result = _host.SetCounter("Mock/UnitTest", "mycommand", input); + CommandResult result = _host.SetCounter( + new ChatUser("Mock/User", "MockUser", PrivilegeLevel.Unknown), + "Mock/UnitTest", + "mycommand", + input + ); Assert.AreEqual( new CommandResult( CommandOutcome.Success, @@ -182,6 +260,24 @@ public void SetCounterRelative(string input, int expectedIncrement) A.CallTo(() => _counterStorageService.Increase("abc123", expectedIncrement)) .MustHaveHappenedOnceExactly(); A.CallTo(() => _counterStorageService.Set("abc123", An.Ignored)).MustNotHaveHappened(); + A.CallTo( + () => + _auditor.Record( + new AuditRecord( + null!, + DateTimeOffset.MinValue, + "Mock/User", + "Mock/UnitTest", + CustomCommandAuditing.CustomCommandType, + "mycommand", + CustomCommandActions.CounterUpdated, + expectedIncrement > 0 + ? $"{10 + expectedIncrement} (+{expectedIncrement})" + : $"{10 + expectedIncrement} ({expectedIncrement})" + ) + ) + ) + .MustHaveHappenedOnceExactly(); } [TestMethod] @@ -190,7 +286,12 @@ public void SetCounterClear() A.CallTo(() => _customCommandService.GetCommand("Mock/UnitTest", "mycommand")) .ReturnsLazily(() => new CustomCommandDescription { Id = "abc123" }); - CommandResult result = _host.SetCounter("Mock/UnitTest", "mycommand", "clear"); + CommandResult result = _host.SetCounter( + new ChatUser("Mock/User", "MockUser", PrivilegeLevel.Unknown), + "Mock/UnitTest", + "mycommand", + "clear" + ); Assert.AreEqual( new CommandResult( CommandOutcome.Success, @@ -201,6 +302,22 @@ public void SetCounterClear() result ); A.CallTo(() => _counterStorageService.Set("abc123", 0)).MustHaveHappenedOnceExactly(); + A.CallTo( + () => + _auditor.Record( + new AuditRecord( + null!, + DateTimeOffset.MinValue, + "Mock/User", + "Mock/UnitTest", + CustomCommandAuditing.CustomCommandType, + "mycommand", + CustomCommandActions.CounterUpdated, + "0" + ) + ) + ) + .MustHaveHappenedOnceExactly(); } [TestMethod] @@ -209,7 +326,12 @@ public void SetUnknownCommand() A.CallTo(() => _customCommandService.GetCommand("Mock/UnitTest", "mycommand")) .ReturnsLazily(() => null); - CommandResult result = _host.SetCounter("Mock/UnitTest", "mycommand", "clear"); + CommandResult result = _host.SetCounter( + new ChatUser("Mock/User", "MockUser", PrivilegeLevel.Unknown), + "Mock/UnitTest", + "mycommand", + "clear" + ); Assert.AreEqual( new CommandResult( CommandOutcome.Failed, @@ -221,6 +343,7 @@ public void SetUnknownCommand() ); A.CallTo(() => _counterStorageService.Set(A.Ignored, An.Ignored)) .MustNotHaveHappened(); + A.CallTo(() => _auditor.Record(An._)).MustNotHaveHappened(); } [TestMethod] @@ -229,7 +352,12 @@ public void SetCommandFailed() A.CallTo(() => _customCommandService.GetCommand("Mock/UnitTest", "mycommand")) .ReturnsLazily(() => new CustomCommandDescription { Id = "abc123" }); - CommandResult result = _host.SetCounter("Mock/UnitTest", "mycommand", "notAThing"); + CommandResult result = _host.SetCounter( + new ChatUser("Mock/User", "MockUser", PrivilegeLevel.Unknown), + "Mock/UnitTest", + "mycommand", + "notAThing" + ); Assert.AreEqual( new CommandResult( CommandOutcome.Failed, @@ -239,5 +367,6 @@ public void SetCommandFailed() ); A.CallTo(() => _counterStorageService.Set(A.Ignored, A.Ignored)) .MustNotHaveHappened(); + A.CallTo(() => _auditor.Record(An._)).MustNotHaveHappened(); } } From d86764c2851a9f3e26d77f79e056079e65acdf03 Mon Sep 17 00:00:00 2001 From: Raphael Guntersweiler Date: Sat, 1 Jun 2024 19:04:43 +0200 Subject: [PATCH 08/14] API auditing for controllers --- .../Auditing/CustomCommandAuditing.cs | 38 +++++++++++++++++++ .../Controllers/CustomCommandController.cs | 22 ++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/FloppyBot.Commands.Custom.Storage/Auditing/CustomCommandAuditing.cs b/src/FloppyBot.Commands.Custom.Storage/Auditing/CustomCommandAuditing.cs index df2c052..d72699e 100644 --- a/src/FloppyBot.Commands.Custom.Storage/Auditing/CustomCommandAuditing.cs +++ b/src/FloppyBot.Commands.Custom.Storage/Auditing/CustomCommandAuditing.cs @@ -1,7 +1,9 @@ using System.Diagnostics; +using System.Text.Json; using FloppyBot.Base.Auditing.Abstraction; using FloppyBot.Chat.Entities; using FloppyBot.Chat.Entities.Identifiers; +using FloppyBot.Commands.Custom.Storage.Entities; namespace FloppyBot.Commands.Custom.Storage.Auditing; @@ -33,6 +35,24 @@ string response ); } + public static void CommandCreated( + this IAuditor auditor, + ChatUser author, + ChannelIdentifier channel, + CustomCommandDescription commandDescription + ) + { + var commandJson = JsonSerializer.Serialize(commandDescription); + auditor.Record( + author.Identifier, + channel, + CustomCommandType, + commandDescription.Name, + CommonActions.Created, + commandJson + ); + } + public static void CommandDeleted( this IAuditor auditor, ChatUser author, @@ -71,4 +91,22 @@ public static void CounterUpdated( detail ); } + + public static void CommandUpdated( + this IAuditor auditor, + ChatUser author, + ChannelIdentifier channel, + CustomCommandDescription commandDescription + ) + { + var commandJson = JsonSerializer.Serialize(commandDescription); + auditor.Record( + author.Identifier, + channel, + CustomCommandType, + commandDescription.Name, + CommonActions.Updated, + commandJson + ); + } } diff --git a/src/FloppyBot.WebApi.V2/Controllers/CustomCommandController.cs b/src/FloppyBot.WebApi.V2/Controllers/CustomCommandController.cs index 7fc9d77..7a01d5f 100644 --- a/src/FloppyBot.WebApi.V2/Controllers/CustomCommandController.cs +++ b/src/FloppyBot.WebApi.V2/Controllers/CustomCommandController.cs @@ -1,4 +1,6 @@ +using FloppyBot.Base.Auditing.Abstraction; using FloppyBot.Commands.Custom.Storage; +using FloppyBot.Commands.Custom.Storage.Auditing; using FloppyBot.WebApi.Auth; using FloppyBot.WebApi.Auth.Controllers; using FloppyBot.WebApi.Auth.UserProfiles; @@ -16,16 +18,19 @@ public class CustomCommandController : ChannelScopedController { private readonly ICustomCommandService _customCommandService; private readonly ICounterStorageService _counterStorageService; + private readonly IAuditor _auditor; public CustomCommandController( IUserService userService, ICustomCommandService customCommandService, - ICounterStorageService counterStorageService + ICounterStorageService counterStorageService, + IAuditor auditor ) : base(userService) { _customCommandService = customCommandService; _counterStorageService = counterStorageService; + _auditor = auditor; } [HttpGet] @@ -75,12 +80,19 @@ [FromBody] CustomCommandDto createDto return BadRequest(); } + _auditor.CommandCreated(User.AsChatUser(), channelId, command); if (createDto.Counter is not null) { var createdCommand = _customCommandService.GetCommand(channelId, createDto.Name) ?? throw new InvalidOperationException("Command not found after creation."); _counterStorageService.Set(createdCommand.Id, createDto.Counter.Value); + _auditor.CounterUpdated( + User.AsChatUser(), + channelId, + command.Name, + createDto.Counter.Value + ); } return NoContent(); @@ -105,9 +117,16 @@ [FromBody] CustomCommandDto updateDto command = updateDto.ToEntity().WithId(command.Id) with { Owners = command.Owners, }; _customCommandService.UpdateCommand(command); + _auditor.CommandUpdated(User.AsChatUser(), channelId, command); if (updateDto.Counter is not null) { _counterStorageService.Set(command.Id, updateDto.Counter.Value); + _auditor.CounterUpdated( + User.AsChatUser(), + channelId, + command.Name, + updateDto.Counter.Value + ); } return NoContent(); @@ -127,6 +146,7 @@ [FromRoute] string commandName return NotFound(); } + _auditor.CommandDeleted(User.AsChatUser(), channelId, commandName); return NoContent(); } } From f3b3626f388771550670024de31a8d89fcadb7aa Mon Sep 17 00:00:00 2001 From: Raphael Guntersweiler Date: Sat, 1 Jun 2024 19:16:07 +0200 Subject: [PATCH 09/14] added controller to fetch audit logs from --- .../IAuditor.cs | 5 ++- .../Impl/NoopAuditor.cs | 2 +- .../StorageAuditor.cs | 8 ++--- src/FloppyBot.WebApi.Auth/Permissions.cs | 1 + .../Controllers/Admin/AuditController.cs | 31 +++++++++++++++++++ 5 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 src/FloppyBot.WebApi.V2/Controllers/Admin/AuditController.cs diff --git a/src/FloppyBot.Base.Auditing.Abstraction/IAuditor.cs b/src/FloppyBot.Base.Auditing.Abstraction/IAuditor.cs index e204b66..629af29 100644 --- a/src/FloppyBot.Base.Auditing.Abstraction/IAuditor.cs +++ b/src/FloppyBot.Base.Auditing.Abstraction/IAuditor.cs @@ -16,8 +16,7 @@ public interface IAuditor /// /// Get a list of audit records for the specified channels /// - /// Channel to query from - /// Additional channels to query from + /// Channels to query from /// - IEnumerable GetAuditRecords(string channel, params string[] channels); + IEnumerable GetAuditRecords(params string[] channels); } diff --git a/src/FloppyBot.Base.Auditing.Abstraction/Impl/NoopAuditor.cs b/src/FloppyBot.Base.Auditing.Abstraction/Impl/NoopAuditor.cs index 0137d0b..90ed30c 100644 --- a/src/FloppyBot.Base.Auditing.Abstraction/Impl/NoopAuditor.cs +++ b/src/FloppyBot.Base.Auditing.Abstraction/Impl/NoopAuditor.cs @@ -11,7 +11,7 @@ public class NoopAuditor : IAuditor public void Record(AuditRecord auditRecord) { } /// - public IEnumerable GetAuditRecords(string channel, params string[] channels) + public IEnumerable GetAuditRecords(params string[] channels) { return []; } diff --git a/src/FloppyBot.Base.Auditing.Storage/StorageAuditor.cs b/src/FloppyBot.Base.Auditing.Storage/StorageAuditor.cs index 26095a8..744961d 100644 --- a/src/FloppyBot.Base.Auditing.Storage/StorageAuditor.cs +++ b/src/FloppyBot.Base.Auditing.Storage/StorageAuditor.cs @@ -31,13 +31,13 @@ public void Record(AuditRecord auditRecord) } /// - public IEnumerable GetAuditRecords(string channel, params string[] channels) + public IEnumerable GetAuditRecords(params string[] channels) { - var channelList = channels.Prepend(channel).ToList(); return _repository .GetAll() - .Where(c => channelList.Contains(c.ChannelIdentifier)) + .Where(c => channels.Contains(c.ChannelIdentifier)) .ToList() - .Select(i => i.ToAuditRecord()); + .Select(i => i.ToAuditRecord()) + .OrderByDescending(i => i.Timestamp); } } diff --git a/src/FloppyBot.WebApi.Auth/Permissions.cs b/src/FloppyBot.WebApi.Auth/Permissions.cs index c34e546..4defe8a 100644 --- a/src/FloppyBot.WebApi.Auth/Permissions.cs +++ b/src/FloppyBot.WebApi.Auth/Permissions.cs @@ -22,6 +22,7 @@ public static class Permissions public const string EDIT_FILES = "edit:files"; public const string READ_LOGS = "read:logs"; + public const string READ_AUDIT = "read:audit"; public static readonly string[] AllPermissions; diff --git a/src/FloppyBot.WebApi.V2/Controllers/Admin/AuditController.cs b/src/FloppyBot.WebApi.V2/Controllers/Admin/AuditController.cs new file mode 100644 index 0000000..8c3fba6 --- /dev/null +++ b/src/FloppyBot.WebApi.V2/Controllers/Admin/AuditController.cs @@ -0,0 +1,31 @@ +using FloppyBot.Base.Auditing.Abstraction; +using FloppyBot.Base.Auditing.Abstraction.Entities; +using FloppyBot.WebApi.Auth; +using FloppyBot.WebApi.Auth.UserProfiles; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace FloppyBot.WebApi.V2.Controllers.Admin; + +[ApiController] +[Route("api/v2/admin/audit")] +[Authorize(Permissions.READ_AUDIT)] +public class AuditController : ControllerBase +{ + private readonly IAuditor _auditor; + private readonly IUserService _userService; + + public AuditController(IUserService userService, IAuditor auditor) + { + _auditor = auditor; + _userService = userService; + } + + [HttpGet] + public ActionResult GetRecords() + { + // TODO: Introduce filtering parameters (like seen with logs) + var ownerOf = _userService.GetAccessibleChannelsForUser(User.GetUserId()); + return Ok(_auditor.GetAuditRecords(ownerOf.ToArray())); + } +} From 3adbc98c69fe3ecdad8e8f171eeb212b7f6f7352 Mon Sep 17 00:00:00 2001 From: Raphael Guntersweiler Date: Sat, 1 Jun 2024 19:54:05 +0200 Subject: [PATCH 10/14] files controller audited --- .../Auditing/FileStorageAuditing.cs | 40 +++++++++++++++++++ .../FloppyBot.FileStorage.csproj | 1 + .../Controllers/FilesController.cs | 8 +++- 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 src/FloppyBot.FileStorage/Auditing/FileStorageAuditing.cs diff --git a/src/FloppyBot.FileStorage/Auditing/FileStorageAuditing.cs b/src/FloppyBot.FileStorage/Auditing/FileStorageAuditing.cs new file mode 100644 index 0000000..826f1f7 --- /dev/null +++ b/src/FloppyBot.FileStorage/Auditing/FileStorageAuditing.cs @@ -0,0 +1,40 @@ +using System.Diagnostics; +using FloppyBot.Base.Auditing.Abstraction; +using FloppyBot.Chat.Entities; +using FloppyBot.Chat.Entities.Identifiers; + +namespace FloppyBot.FileStorage.Auditing; + +[StackTraceHidden] +public static class FileStorageAuditing +{ + public const string FileHeaderType = "File"; + + public static void FileCreated( + this IAuditor auditor, + ChatUser author, + ChannelIdentifier channel, + string filename, + long fileSize + ) + { + auditor.Record( + author.Identifier, + channel, + FileHeaderType, + filename, + CommonActions.Created, + $"{fileSize} bytes" + ); + } + + public static void FileDeleted( + this IAuditor auditor, + ChatUser author, + ChannelIdentifier channel, + string filename + ) + { + auditor.Record(author.Identifier, channel, FileHeaderType, filename, CommonActions.Deleted); + } +} diff --git a/src/FloppyBot.FileStorage/FloppyBot.FileStorage.csproj b/src/FloppyBot.FileStorage/FloppyBot.FileStorage.csproj index 6acbe70..2c6c6b5 100644 --- a/src/FloppyBot.FileStorage/FloppyBot.FileStorage.csproj +++ b/src/FloppyBot.FileStorage/FloppyBot.FileStorage.csproj @@ -5,6 +5,7 @@ enable + diff --git a/src/FloppyBot.WebApi.V2/Controllers/FilesController.cs b/src/FloppyBot.WebApi.V2/Controllers/FilesController.cs index 2409c08..dd05ed9 100644 --- a/src/FloppyBot.WebApi.V2/Controllers/FilesController.cs +++ b/src/FloppyBot.WebApi.V2/Controllers/FilesController.cs @@ -1,5 +1,7 @@ +using FloppyBot.Base.Auditing.Abstraction; using FloppyBot.Base.Extensions; using FloppyBot.FileStorage; +using FloppyBot.FileStorage.Auditing; using FloppyBot.WebApi.Auth; using FloppyBot.WebApi.Auth.Controllers; using FloppyBot.WebApi.Auth.UserProfiles; @@ -17,11 +19,13 @@ namespace FloppyBot.WebApi.V2.Controllers; public class FilesController : ChannelScopedController { private readonly IFileService _fileService; + private readonly IAuditor _auditor; - public FilesController(IUserService userService, IFileService fileService) + public FilesController(IUserService userService, IFileService fileService, IAuditor auditor) : base(userService) { _fileService = fileService; + _auditor = auditor; } [HttpGet] @@ -64,6 +68,7 @@ public IActionResult UploadFile( ); } + _auditor.FileCreated(User.AsChatUser(), channelId, file.FileName, file.Length); return NoContent(); } @@ -77,6 +82,7 @@ [FromRoute] string fileName { var channelId = EnsureChannelAccess(messageInterface, channel); _fileService.DeleteFile(channelId, fileName); + _auditor.FileDeleted(User.AsChatUser(), channelId, fileName); return NoContent(); } From 2c4f256e615424617aba114884d4b46d261e2489 Mon Sep 17 00:00:00 2001 From: Raphael Guntersweiler Date: Sat, 1 Jun 2024 21:18:32 +0200 Subject: [PATCH 11/14] command and timer config audited --- .../CommonActions.cs | 1 + .../TwitchAuditing.cs | 26 +++++++- .../Auditing/CommandConfigurationAuditing.cs | 61 +++++++++++++++++++ .../Controllers/CommandConfigController.cs | 22 +++++-- .../Controllers/TimerMessageController.cs | 14 +++-- 5 files changed, 110 insertions(+), 14 deletions(-) create mode 100644 src/FloppyBot.Commands.Core/Auditing/CommandConfigurationAuditing.cs diff --git a/src/FloppyBot.Base.Auditing.Abstraction/CommonActions.cs b/src/FloppyBot.Base.Auditing.Abstraction/CommonActions.cs index 244bd6d..2258ff8 100644 --- a/src/FloppyBot.Base.Auditing.Abstraction/CommonActions.cs +++ b/src/FloppyBot.Base.Auditing.Abstraction/CommonActions.cs @@ -6,4 +6,5 @@ public static class CommonActions public const string Updated = "Updated"; public const string Deleted = "Deleted"; public const string Disabled = "Disabled"; + public const string Enabled = "Enabled"; } diff --git a/src/FloppyBot.Commands.Aux.Twitch/TwitchAuditing.cs b/src/FloppyBot.Commands.Aux.Twitch/TwitchAuditing.cs index c78ed3f..28f6997 100644 --- a/src/FloppyBot.Commands.Aux.Twitch/TwitchAuditing.cs +++ b/src/FloppyBot.Commands.Aux.Twitch/TwitchAuditing.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using FloppyBot.Aux.TwitchAlerts.Core.Entities; using FloppyBot.Base.Auditing.Abstraction; using FloppyBot.Chat.Entities; @@ -10,6 +11,7 @@ public static class TwitchAuditing { public const string ShoutoutMessageType = "ShoutoutMessage"; public const string SubAlertMessageType = "SubAlertMessage"; + public const string TimerMessageType = "TimerMessage"; public static void ShoutoutMessageSet( this IAuditor auditor, @@ -87,7 +89,7 @@ string message auditor.Record( user.Identifier, channel, - nameof(TwitchAlertSettings), + SubAlertMessageType, nameof(TwitchAlertSettings.SubMessage), CommonActions.Updated, message @@ -103,7 +105,7 @@ ChannelIdentifier channel auditor.Record( user.Identifier, channel, - nameof(TwitchAlertSettings), + SubAlertMessageType, nameof(TwitchAlertSettings.SubMessage), CommonActions.Disabled ); @@ -118,9 +120,27 @@ ChannelIdentifier channel auditor.Record( user.Identifier, channel, - nameof(TwitchAlertSettings), + SubAlertMessageType, nameof(TwitchAlertSettings.SubMessage), CommonActions.Deleted ); } + + public static void TimerMessagesChanged( + this IAuditor auditor, + ChatUser user, + ChannelIdentifier channel, + TimerMessageConfiguration config + ) + { + var configJson = JsonSerializer.Serialize(config); + auditor.Record( + user.Identifier, + channel, + TimerMessageType, + string.Empty, + CommonActions.Updated, + configJson + ); + } } diff --git a/src/FloppyBot.Commands.Core/Auditing/CommandConfigurationAuditing.cs b/src/FloppyBot.Commands.Core/Auditing/CommandConfigurationAuditing.cs new file mode 100644 index 0000000..b92214b --- /dev/null +++ b/src/FloppyBot.Commands.Core/Auditing/CommandConfigurationAuditing.cs @@ -0,0 +1,61 @@ +using System.Diagnostics; +using FloppyBot.Base.Auditing.Abstraction; +using FloppyBot.Chat.Entities; +using FloppyBot.Chat.Entities.Identifiers; +using FloppyBot.Commands.Core.Config; + +namespace FloppyBot.Commands.Core.Auditing; + +[StackTraceHidden] +public static class CommandConfigurationAuditing +{ + public static void CommandConfigurationUpdated( + this IAuditor auditor, + ChatUser user, + ChannelIdentifier channel, + CommandConfiguration configuration + ) + { + auditor.Record( + user.Identifier, + channel, + nameof(CommandConfiguration), + configuration.CommandName, + CommonActions.Updated, + configuration.ToString() + ); + } + + public static void CommandConfigurationDeleted( + this IAuditor auditor, + ChatUser user, + ChannelIdentifier channel, + string commandName + ) + { + auditor.Record( + user.Identifier, + channel, + nameof(CommandConfiguration), + commandName, + CommonActions.Updated + ); + } + + public static void CommandConfigurationDisabledSet( + this IAuditor auditor, + ChatUser user, + ChannelIdentifier channel, + string commandName, + bool disabled + ) + { + auditor.Record( + user.Identifier, + channel, + nameof(CommandConfiguration), + commandName, + disabled ? CommonActions.Disabled : CommonActions.Enabled + ); + } +} diff --git a/src/FloppyBot.WebApi.V2/Controllers/CommandConfigController.cs b/src/FloppyBot.WebApi.V2/Controllers/CommandConfigController.cs index 6815f77..15aa766 100644 --- a/src/FloppyBot.WebApi.V2/Controllers/CommandConfigController.cs +++ b/src/FloppyBot.WebApi.V2/Controllers/CommandConfigController.cs @@ -1,4 +1,6 @@ +using FloppyBot.Base.Auditing.Abstraction; using FloppyBot.Base.Extensions; +using FloppyBot.Commands.Core.Auditing; using FloppyBot.Commands.Core.Config; using FloppyBot.Commands.Registry; using FloppyBot.WebApi.Auth; @@ -18,16 +20,19 @@ public class CommandConfigController : ChannelScopedController { private readonly ICommandConfigurationService _commandConfigurationService; private readonly IDistributedCommandRegistry _distributedCommandRegistry; + private readonly IAuditor _auditor; public CommandConfigController( IUserService userService, ICommandConfigurationService commandConfigurationService, - IDistributedCommandRegistry distributedCommandRegistry + IDistributedCommandRegistry distributedCommandRegistry, + IAuditor auditor ) : base(userService) { _commandConfigurationService = commandConfigurationService; _distributedCommandRegistry = distributedCommandRegistry; + _auditor = auditor; } [HttpGet] @@ -103,10 +108,9 @@ [FromBody] CommandConfigurationDto commandConfigurationDto Disabled = false, }; - _commandConfigurationService.SetCommandConfiguration( - (commandConfigurationDto with { Id = commandConfiguration.Id, }).ToEntity() - ); - + var entity = (commandConfigurationDto with { Id = commandConfiguration.Id, }).ToEntity(); + _commandConfigurationService.SetCommandConfiguration(entity); + _auditor.CommandConfigurationUpdated(User.AsChatUser(), channelId, entity); return NoContent(); } @@ -120,7 +124,7 @@ [FromRoute] string commandName { var channelId = EnsureChannelAccess(messageInterface, channel); _commandConfigurationService.DeleteCommandConfiguration(channelId, commandName); - + _auditor.CommandConfigurationDeleted(User.AsChatUser(), channelId, commandName); return NoContent(); } @@ -154,6 +158,12 @@ commandConfiguration with Disabled = isDisabled, } ); + _auditor.CommandConfigurationDisabledSet( + User.AsChatUser(), + channelId, + commandName, + isDisabled + ); return NoContent(); } diff --git a/src/FloppyBot.WebApi.V2/Controllers/TimerMessageController.cs b/src/FloppyBot.WebApi.V2/Controllers/TimerMessageController.cs index 4a03293..5f445fa 100644 --- a/src/FloppyBot.WebApi.V2/Controllers/TimerMessageController.cs +++ b/src/FloppyBot.WebApi.V2/Controllers/TimerMessageController.cs @@ -1,3 +1,5 @@ +using FloppyBot.Base.Auditing.Abstraction; +using FloppyBot.Commands.Aux.Twitch; using FloppyBot.Commands.Aux.Twitch.Storage; using FloppyBot.WebApi.Auth; using FloppyBot.WebApi.Auth.Controllers; @@ -14,14 +16,17 @@ namespace FloppyBot.WebApi.V2.Controllers; public class TimerMessageController : ChannelScopedController { private readonly ITimerMessageConfigurationService _timerMessageConfigurationService; + private readonly IAuditor _auditor; public TimerMessageController( IUserService userService, - ITimerMessageConfigurationService timerMessageConfigurationService + ITimerMessageConfigurationService timerMessageConfigurationService, + IAuditor auditor ) : base(userService) { _timerMessageConfigurationService = timerMessageConfigurationService; + _auditor = auditor; } [HttpGet] @@ -46,10 +51,9 @@ [FromBody] TimerMessageConfigurationDto config ) { var channelId = EnsureChannelAccess(messageInterface, channel); - _timerMessageConfigurationService.UpdateConfigurationForChannel( - channelId, - config.ToEntity() - ); + var entity = config.ToEntity(); + _timerMessageConfigurationService.UpdateConfigurationForChannel(channelId, entity); + _auditor.TimerMessagesChanged(User.AsChatUser(), channelId, entity); return NoContent(); } } From 5132856f517555e906b442a1642164463fd05dd8 Mon Sep 17 00:00:00 2001 From: Raphael Guntersweiler Date: Thu, 20 Jun 2024 21:25:41 +0200 Subject: [PATCH 12/14] added audit logging to new features --- .../CustomCommandExecutor.cs | 30 ++++++++++++++++--- .../Execution/CustomCommandExecutorTests.cs | 16 ++++++---- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/FloppyBot.Commands.Custom.Execution/CustomCommandExecutor.cs b/src/FloppyBot.Commands.Custom.Execution/CustomCommandExecutor.cs index 828fc80..c5497b5 100644 --- a/src/FloppyBot.Commands.Custom.Execution/CustomCommandExecutor.cs +++ b/src/FloppyBot.Commands.Custom.Execution/CustomCommandExecutor.cs @@ -1,4 +1,5 @@ -using FloppyBot.Base.Clock; +using FloppyBot.Base.Auditing.Abstraction; +using FloppyBot.Base.Clock; using FloppyBot.Base.Rng; using FloppyBot.Base.TextFormatting; using FloppyBot.Chat.Entities; @@ -9,6 +10,7 @@ using FloppyBot.Commands.Custom.Communication.Entities; using FloppyBot.Commands.Custom.Execution.InternalEntities; using FloppyBot.Commands.Custom.Storage; +using FloppyBot.Commands.Custom.Storage.Auditing; using FloppyBot.Commands.Custom.Storage.Entities; using FloppyBot.Commands.Parser.Entities; using Microsoft.Extensions.Logging; @@ -31,6 +33,7 @@ public class CustomCommandExecutor : ICustomCommandExecutor private readonly ILogger _logger; private readonly IRandomNumberGenerator _randomNumberGenerator; private readonly ITimeProvider _timeProvider; + private readonly IAuditor _auditor; public CustomCommandExecutor( ILogger logger, @@ -38,7 +41,8 @@ public CustomCommandExecutor( IRandomNumberGenerator randomNumberGenerator, ICooldownService cooldownService, ICounterStorageService counterStorageService, - ISoundCommandInvocationSender invocationSender + ISoundCommandInvocationSender invocationSender, + IAuditor auditor ) { _logger = logger; @@ -47,6 +51,7 @@ ISoundCommandInvocationSender invocationSender _cooldownService = cooldownService; _counterStorageService = counterStorageService; _invocationSender = invocationSender; + _auditor = auditor; } public IEnumerable Execute( @@ -244,14 +249,31 @@ CustomCommandDescription description return null; } + int? result = null; switch (prefix) { case "+": _logger.LogDebug("Found +, increasing counter by {IncrementValue}", increment); - return _counterStorageService.Increase(description.Id, increment); + result = _counterStorageService.Increase(description.Id, increment); + _auditor.CounterUpdated( + instruction.Context!.SourceMessage.Author, + instruction.Context!.SourceMessage.Identifier.GetChannel(), + description.Name, + result.Value, + increment + ); + return result; case "-": _logger.LogDebug("Found -, decreasing counter by {IncrementValue}", increment); - return _counterStorageService.Increase(description.Id, -increment); + result = _counterStorageService.Increase(description.Id, -increment); + _auditor.CounterUpdated( + instruction.Context!.SourceMessage.Author, + instruction.Context!.SourceMessage.Identifier.GetChannel(), + description.Name, + result.Value, + -increment + ); + return result.Value; } _logger.LogTrace("No valid input found, ignoring (Input was: {InputArgument})", firstArg); diff --git a/src/FloppyBot.Commands.Custom.Tests/Execution/CustomCommandExecutorTests.cs b/src/FloppyBot.Commands.Custom.Tests/Execution/CustomCommandExecutorTests.cs index ff31141..7ed0b96 100644 --- a/src/FloppyBot.Commands.Custom.Tests/Execution/CustomCommandExecutorTests.cs +++ b/src/FloppyBot.Commands.Custom.Tests/Execution/CustomCommandExecutorTests.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using FakeItEasy; +using FloppyBot.Base.Auditing.Abstraction.Impl; using FloppyBot.Base.Clock; using FloppyBot.Base.EquatableCollections; using FloppyBot.Base.Rng; @@ -91,7 +92,8 @@ public void HandlesCooldownCorrectly(int advanceBySecond, bool expectResult) new RandomNumberGenerator(), cooldownService, A.Fake(), - A.Fake() + A.Fake(), + new NoopAuditor() ); string?[] reply = executor.Execute(CommandInstruction, CommandDescription).ToArray(); @@ -126,7 +128,8 @@ bool expectResult new RandomNumberGenerator(), cooldownService, _counterStorageService, - A.Fake() + A.Fake(), + new NoopAuditor() ); string?[] reply = executor @@ -169,7 +172,8 @@ public void HandlesCounterCorrectly() new RandomNumberGenerator(), cooldownService, _counterStorageService, - A.Fake() + A.Fake(), + new NoopAuditor() ); string?[] reply = executor @@ -220,7 +224,8 @@ public void HandlesUserLimitationCorrectly(string inputUser, bool expectResult) new RandomNumberGenerator(), cooldownService, _counterStorageService, - A.Fake() + A.Fake(), + new NoopAuditor() ); string?[] reply = executor @@ -317,7 +322,8 @@ public void AssumingOperationsAllowed_CounterWillIncrementWithCorrectCommand() new RandomNumberGenerator(), cooldownService, counterService, - A.Fake() + A.Fake(), + new NoopAuditor() ); var customizedCommandDescription = CommandDescription with From 7cd22e5084b2c8cce7eb68bc795f0acba4272e47 Mon Sep 17 00:00:00 2001 From: Raphael Guntersweiler Date: Thu, 20 Jun 2024 21:43:13 +0200 Subject: [PATCH 13/14] audit log filtering --- .../Controllers/Admin/AuditController.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/FloppyBot.WebApi.V2/Controllers/Admin/AuditController.cs b/src/FloppyBot.WebApi.V2/Controllers/Admin/AuditController.cs index 8c3fba6..697598b 100644 --- a/src/FloppyBot.WebApi.V2/Controllers/Admin/AuditController.cs +++ b/src/FloppyBot.WebApi.V2/Controllers/Admin/AuditController.cs @@ -1,6 +1,7 @@ using FloppyBot.Base.Auditing.Abstraction; using FloppyBot.Base.Auditing.Abstraction.Entities; using FloppyBot.WebApi.Auth; +using FloppyBot.WebApi.Auth.Controllers; using FloppyBot.WebApi.Auth.UserProfiles; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -8,24 +9,25 @@ namespace FloppyBot.WebApi.V2.Controllers.Admin; [ApiController] -[Route("api/v2/admin/audit")] +[Route("api/v2/admin/audit/{messageInterface}/{channel}")] [Authorize(Permissions.READ_AUDIT)] -public class AuditController : ControllerBase +public class AuditController : ChannelScopedController { private readonly IAuditor _auditor; - private readonly IUserService _userService; public AuditController(IUserService userService, IAuditor auditor) + : base(userService) { _auditor = auditor; - _userService = userService; } [HttpGet] - public ActionResult GetRecords() + public ActionResult GetRecords( + [FromRoute] string messageInterface, + [FromRoute] string channel + ) { - // TODO: Introduce filtering parameters (like seen with logs) - var ownerOf = _userService.GetAccessibleChannelsForUser(User.GetUserId()); - return Ok(_auditor.GetAuditRecords(ownerOf.ToArray())); + var channelId = EnsureChannelAccess(messageInterface, channel); + return Ok(_auditor.GetAuditRecords(channelId)); } } From b88839e9251d144af51850fc8beddf681138fd54 Mon Sep 17 00:00:00 2001 From: Raphael Guntersweiler Date: Thu, 20 Jun 2024 22:00:26 +0200 Subject: [PATCH 14/14] ensured that rate-limiting messages can be recognized on UI --- src/FloppyBot.WebApi.Agent/Utils/Limiters.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/FloppyBot.WebApi.Agent/Utils/Limiters.cs b/src/FloppyBot.WebApi.Agent/Utils/Limiters.cs index 89fedd5..379167f 100644 --- a/src/FloppyBot.WebApi.Agent/Utils/Limiters.cs +++ b/src/FloppyBot.WebApi.Agent/Utils/Limiters.cs @@ -45,6 +45,11 @@ private static RateLimitPartition RateLimiter_Build(HttpContext httpCont { var logger = httpContext.GetLogger(LOGGER_CATEGORY); + if (HttpMethods.IsOptions(httpContext.Request.Method)) + { + return RateLimitPartition.GetNoLimiter(KEY_GLOBAL); + } + string? accessToken = httpContext.Request.Headers.Authorization.ToString().HashString(); string? remoteIp = httpContext.GetRemoteHostIpFromHeaders()?.ToString(); @@ -67,6 +72,9 @@ CancellationToken cancellationToken ) { var response = context.HttpContext.Response; + // Ensure that the response can be read by frontends + response.Headers["Access-Control-Allow-Origin"] = "*"; + if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) { // Add a Retry-After header to the response