diff --git a/Application/ApplicationManager.cs b/Application/ApplicationManager.cs index db4cce295..9729dc426 100644 --- a/Application/ApplicationManager.cs +++ b/Application/ApplicationManager.cs @@ -431,7 +431,8 @@ namespace IW4MAdmin.Application { Name = cmd.Name, Alias = cmd.Alias, - MinimumPermission = cmd.Permission + MinimumPermission = cmd.Permission, + AllowImpersonation = cmd.AllowImpersonation }); } diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs index 3039a49c0..b54bf3dd8 100644 --- a/Application/IW4MServer.cs +++ b/Application/IW4MServer.cs @@ -144,6 +144,7 @@ namespace IW4MAdmin catch (CommandException e) { Logger.WriteInfo(e.Message); + E.FailReason = GameEvent.EventFailReason.Invalid; } if (C != null) @@ -342,7 +343,7 @@ namespace IW4MAdmin return false; } - if (E.Origin.Level > EFClient.Permission.Moderator) + if (E.Origin.Level > Permission.Moderator) { E.Origin.Tell(string.Format(loc["SERVER_REPORT_COUNT"], E.Owner.Reports.Count)); } @@ -371,7 +372,7 @@ namespace IW4MAdmin Expires = expires, Offender = E.Target, Offense = E.Data, - Punisher = E.Origin, + Punisher = E.ImpersonationOrigin ?? E.Origin, When = DateTime.UtcNow, Link = E.Target.AliasLink }; @@ -388,7 +389,7 @@ namespace IW4MAdmin Expires = DateTime.UtcNow, Offender = E.Target, Offense = E.Data, - Punisher = E.Origin, + Punisher = E.ImpersonationOrigin ?? E.Origin, When = DateTime.UtcNow, Link = E.Target.AliasLink }; @@ -413,7 +414,7 @@ namespace IW4MAdmin Expires = DateTime.UtcNow, Offender = E.Target, Offense = E.Message, - Punisher = E.Origin, + Punisher = E.ImpersonationOrigin ?? E.Origin, Active = true, When = DateTime.UtcNow, Link = E.Target.AliasLink @@ -432,28 +433,28 @@ namespace IW4MAdmin else if (E.Type == GameEvent.EventType.TempBan) { - await TempBan(E.Data, (TimeSpan)E.Extra, E.Target, E.Origin); ; + await TempBan(E.Data, (TimeSpan)E.Extra, E.Target, E.ImpersonationOrigin ?? E.Origin); ; } else if (E.Type == GameEvent.EventType.Ban) { bool isEvade = E.Extra != null ? (bool)E.Extra : false; - await Ban(E.Data, E.Target, E.Origin, isEvade); + await Ban(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin, isEvade); } else if (E.Type == GameEvent.EventType.Unban) { - await Unban(E.Data, E.Target, E.Origin); + await Unban(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin); } else if (E.Type == GameEvent.EventType.Kick) { - await Kick(E.Data, E.Target, E.Origin); + await Kick(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin); } else if (E.Type == GameEvent.EventType.Warn) { - await Warn(E.Data, E.Target, E.Origin); + await Warn(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin); } else if (E.Type == GameEvent.EventType.Disconnect) diff --git a/Application/Main.cs b/Application/Main.cs index b02f73058..4369d2c6d 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -9,6 +9,7 @@ using SharedLibraryCore.Configuration; using SharedLibraryCore.Exceptions; using SharedLibraryCore.Helpers; using SharedLibraryCore.Interfaces; +using SharedLibraryCore.Repositories; using System; using System.Linq; using System.Text; @@ -284,6 +285,7 @@ namespace IW4MAdmin.Application .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddTransient() .AddSingleton(_serviceProvider => { diff --git a/Plugins/ProfanityDeterment/Configuration.cs b/Plugins/ProfanityDeterment/Configuration.cs index e1e27b211..f8532d342 100644 --- a/Plugins/ProfanityDeterment/Configuration.cs +++ b/Plugins/ProfanityDeterment/Configuration.cs @@ -16,9 +16,23 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment { OffensiveWords = new List() { - @"\s*n+.*i+.*g+.*e+.*r+\s*", - @"\s*n+.*i+.*g+.*a+\s*", - @"\s*f+u+.*c+.*k+.*\s*" + @"(ph|f)[a@]g[s\$]?", + @"(ph|f)[a@]gg[i1]ng", + @"(ph|f)[a@]gg?[o0][t\+][s\$]?", + @"(ph|f)[a@]gg[s\$]", + @"(ph|f)[e3][l1][l1]?[a@][t\+][i1][o0]", + @"(ph|f)u(c|k|ck|q)", + @"(ph|f)u(c|k|ck|q)[s\$]?", + @"(c|k|ck|q)un[t\+][l1][i1](c|k|ck|q)", + @"(c|k|ck|q)un[t\+][l1][i1](c|k|ck|q)[e3]r", + @"(c|k|ck|q)un[t\+][l1][i1](c|k|ck|q)[i1]ng", + @"b[i1][t\+]ch[s\$]?", + @"b[i1][t\+]ch[e3]r[s\$]?", + @"b[i1][t\+]ch[e3][s\$]", + @"b[i1][t\+]ch[i1]ng?", + @"n[i1]gg?[e3]r[s\$]?", + @"[s\$]h[i1][t\+][s\$]?", + @"[s\$][l1]u[t\+][s\$]?" }; var loc = Utilities.CurrentLocalization.LocalizationIndex; diff --git a/Plugins/Stats/Commands/ResetStats.cs b/Plugins/Stats/Commands/ResetStats.cs index a07d2bc3c..20fd80318 100644 --- a/Plugins/Stats/Commands/ResetStats.cs +++ b/Plugins/Stats/Commands/ResetStats.cs @@ -19,6 +19,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands Alias = "rs"; Permission = EFClient.Permission.User; RequiresTarget = false; + //AllowImpersonation = true; } public override async Task ExecuteAsync(GameEvent E) diff --git a/SharedLibraryCore/Command.cs b/SharedLibraryCore/Command.cs index a9ea491f0..cbde52552 100644 --- a/SharedLibraryCore/Command.cs +++ b/SharedLibraryCore/Command.cs @@ -117,5 +117,10 @@ namespace SharedLibraryCore /// Argument list for the command /// public CommandArgument[] Arguments { get; protected set; } = new CommandArgument[0]; + + /// + /// indicates if this command allows impersonation (run as) + /// + public bool AllowImpersonation { get; set; } } } diff --git a/SharedLibraryCore/Commands/CommandExtensions.cs b/SharedLibraryCore/Commands/CommandExtensions.cs new file mode 100644 index 000000000..09892ed19 --- /dev/null +++ b/SharedLibraryCore/Commands/CommandExtensions.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SharedLibraryCore.Commands +{ + public static class CommandExtensions + { + public static bool IsTargetingSelf(this GameEvent gameEvent) => gameEvent.Origin?.Equals(gameEvent.Target) ?? false; + + public static bool CanPerformActionOnTarget(this GameEvent gameEvent) => gameEvent.Origin?.Level > gameEvent.Target?.Level; + } +} diff --git a/SharedLibraryCore/Commands/CommandProcessing.cs b/SharedLibraryCore/Commands/CommandProcessing.cs index 5a3c13a98..5f62331d1 100644 --- a/SharedLibraryCore/Commands/CommandProcessing.cs +++ b/SharedLibraryCore/Commands/CommandProcessing.cs @@ -21,7 +21,8 @@ namespace SharedLibraryCore.Commands Command C = null; foreach (Command cmd in Manager.GetCommands()) { - if (cmd.Name == CommandString.ToLower() || cmd.Alias == CommandString.ToLower()) + if (cmd.Name.Equals(CommandString, StringComparison.OrdinalIgnoreCase) || + (cmd.Alias ?? "").Equals(CommandString, StringComparison.OrdinalIgnoreCase)) { C = cmd; } @@ -33,6 +34,12 @@ namespace SharedLibraryCore.Commands throw new CommandException($"{E.Origin} entered unknown command \"{CommandString}\""); } + if (!C.AllowImpersonation && E.ImpersonationOrigin != null) + { + E.ImpersonationOrigin.Tell(loc["COMMANDS_RUN_AS_FAIL"]); + throw new CommandException($"Command {C.Name} cannot be run as another client"); + } + E.Data = E.Data.RemoveWords(1); String[] Args = E.Data.Trim().Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); // todo: the code below can be cleaned up diff --git a/SharedLibraryCore/Commands/NativeCommands.cs b/SharedLibraryCore/Commands/NativeCommands.cs index e1c068795..27412f032 100644 --- a/SharedLibraryCore/Commands/NativeCommands.cs +++ b/SharedLibraryCore/Commands/NativeCommands.cs @@ -1434,6 +1434,7 @@ namespace SharedLibraryCore.Commands Alias = "sp"; Permission = Permission.Moderator; RequiresTarget = false; + AllowImpersonation = true; Arguments = new[] { new CommandArgument() diff --git a/SharedLibraryCore/Commands/RunAsCommand.cs b/SharedLibraryCore/Commands/RunAsCommand.cs new file mode 100644 index 000000000..67e7ac477 --- /dev/null +++ b/SharedLibraryCore/Commands/RunAsCommand.cs @@ -0,0 +1,65 @@ +using SharedLibraryCore.Configuration; +using SharedLibraryCore.Database.Models; +using SharedLibraryCore.Interfaces; +using System.Linq; +using System.Threading.Tasks; + +namespace SharedLibraryCore.Commands +{ + public class RunAsCommand : Command + { + public RunAsCommand(CommandConfiguration config, ITranslationLookup lookup) : base(config, lookup) + { + Name = "runas"; + Description = lookup["COMMANDS_RUN_AS_DESC"]; + Alias = "ra"; + Permission = EFClient.Permission.Moderator; + RequiresTarget = true; + Arguments = new[] + { + new CommandArgument() + { + Name = lookup["COMMANDS_ARGS_COMMANDS"], + Required = true + } + }; + } + + public override async Task ExecuteAsync(GameEvent E) + { + if (E.IsTargetingSelf()) + { + E.Origin.Tell(_translationLookup["COMMANDS_RUN_AS_SELF"]); + return; + } + + if (!E.CanPerformActionOnTarget()) + { + E.Origin.Tell(_translationLookup["COMMANDS_RUN_AS_FAIL_PERM"]); + return; + } + + string cmd = $"{Utilities.CommandPrefix}{E.Data}"; + var impersonatedCommandEvent = new GameEvent() + { + Type = GameEvent.EventType.Command, + Origin = E.Target, + ImpersonationOrigin = E.Origin, + Message = cmd, + Data = cmd, + Owner = E.Owner + }; + E.Owner.Manager.GetEventHandler().AddEvent(impersonatedCommandEvent); + + var result = await impersonatedCommandEvent.WaitAsync(Utilities.DefaultCommandTimeout, E.Owner.Manager.CancellationToken); + var response = E.Owner.CommandResult.Where(c => c.ClientId == E.Target.ClientId).ToList(); + + // remove the added command response + for (int i = 0; i < response.Count; i++) + { + E.Origin.Tell(_translationLookup["COMMANDS_RUN_AS_SUCCESS"].FormatExt(response[i].Response)); + E.Owner.CommandResult.Remove(response[i]); + } + } + } +} diff --git a/SharedLibraryCore/Configuration/CommandProperties.cs b/SharedLibraryCore/Configuration/CommandProperties.cs index 8271a3683..9eab8a9b7 100644 --- a/SharedLibraryCore/Configuration/CommandProperties.cs +++ b/SharedLibraryCore/Configuration/CommandProperties.cs @@ -24,5 +24,10 @@ namespace SharedLibraryCore.Configuration /// [JsonConverter(typeof(StringEnumConverter))] public Permission MinimumPermission { get; set; } + + /// + /// Indicates if the command can be run by another user (impersonation) + /// + public bool AllowImpersonation { get; set; } } } diff --git a/SharedLibraryCore/Database/DatabaseContext.cs b/SharedLibraryCore/Database/DatabaseContext.cs index b3ed94e9d..0a5b505c5 100644 --- a/SharedLibraryCore/Database/DatabaseContext.cs +++ b/SharedLibraryCore/Database/DatabaseContext.cs @@ -71,8 +71,8 @@ namespace SharedLibraryCore.Database protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { - // optionsBuilder.UseLoggerFactory(_loggerFactory) - // .EnableSensitiveDataLogging(); + optionsBuilder.UseLoggerFactory(_loggerFactory) + .EnableSensitiveDataLogging(); if (string.IsNullOrEmpty(_ConnectionString)) { diff --git a/SharedLibraryCore/Database/Models/EFChangeHistory.cs b/SharedLibraryCore/Database/Models/EFChangeHistory.cs index c1b040be4..fae53ab0f 100644 --- a/SharedLibraryCore/Database/Models/EFChangeHistory.cs +++ b/SharedLibraryCore/Database/Models/EFChangeHistory.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Text; namespace SharedLibraryCore.Database.Models { @@ -22,6 +19,7 @@ namespace SharedLibraryCore.Database.Models public int ChangeHistoryId { get; set; } public int OriginEntityId { get; set; } public int TargetEntityId { get; set; } + public int? ImpersonationEntityId { get; set; } public ChangeType TypeOfChange { get; set; } public DateTime TimeChanged { get; set; } = DateTime.UtcNow; [MaxLength(128)] diff --git a/SharedLibraryCore/Dtos/AuditInfo.cs b/SharedLibraryCore/Dtos/AuditInfo.cs new file mode 100644 index 000000000..bf963f51b --- /dev/null +++ b/SharedLibraryCore/Dtos/AuditInfo.cs @@ -0,0 +1,57 @@ +using System; + +namespace SharedLibraryCore.Dtos +{ + /// + /// data transfer class for audit information + /// + public class AuditInfo + { + /// + /// name of the origin entity + /// + public string OriginName { get; set; } + + /// + /// id of the origin entity + /// + public int OriginId { get; set; } + + /// + /// name of the target entity + /// + public string TargetName { get; set; } + + /// + /// id of the target entity + /// + public int? TargetId { get; set; } + + /// + /// when the audit event occured + /// + public DateTime When { get; set; } + + /// + /// what audit action occured + /// + public string Action { get; set; } + + /// + /// additional comment data about the audit event + /// + public string Data { get; set; } + + private string oldValue; + /// + /// previous value + /// + public string OldValue { get => oldValue ?? "--"; set => oldValue = value; } + + private string newValue; + /// + /// new value + /// + public string NewValue { get => newValue ?? "--"; set => newValue = value; } + } +} diff --git a/SharedLibraryCore/Dtos/PaginationInfo.cs b/SharedLibraryCore/Dtos/PaginationInfo.cs new file mode 100644 index 000000000..bd7794337 --- /dev/null +++ b/SharedLibraryCore/Dtos/PaginationInfo.cs @@ -0,0 +1,34 @@ +namespace SharedLibraryCore.Dtos +{ + /// + /// pagination information holder class + /// + public class PaginationInfo + { + /// + /// how many items to skip + /// + public int Offset { get; set; } + + /// + /// how many itesm to take + /// + public int Count { get; set; } + + /// + /// filter query + /// + public string Filter { get; set; } + + /// + /// direction of ordering + /// + public SortDirection Direction { get; set; } = SortDirection.Descending; + } + + public enum SortDirection + { + Ascending, + Descending + } +} diff --git a/SharedLibraryCore/Events/GameEvent.cs b/SharedLibraryCore/Events/GameEvent.cs index 9d78585f9..490edd74d 100644 --- a/SharedLibraryCore/Events/GameEvent.cs +++ b/SharedLibraryCore/Events/GameEvent.cs @@ -227,6 +227,7 @@ namespace SharedLibraryCore public int? GameTime { get; set; } public EFClient Origin; public EFClient Target; + public EFClient ImpersonationOrigin { get; set; } public Server Owner; public bool IsRemote { get; set; } = false; public object Extra { get; set; } @@ -276,7 +277,7 @@ namespace SharedLibraryCore Owner?.Logger.WriteError("Waiting for event to complete timed out"); Owner?.Logger.WriteDebug($"{Id}, {Type}, {Data}, {Extra}, {FailReason.ToString()}, {Message}, {Origin}, {Target}"); #if DEBUG - throw new Exception(); + //throw new Exception(); #endif } diff --git a/SharedLibraryCore/Interfaces/IAuditInformationRepository.cs b/SharedLibraryCore/Interfaces/IAuditInformationRepository.cs new file mode 100644 index 000000000..a4bd53a9b --- /dev/null +++ b/SharedLibraryCore/Interfaces/IAuditInformationRepository.cs @@ -0,0 +1,19 @@ +using SharedLibraryCore.Dtos; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SharedLibraryCore.Interfaces +{ + /// + /// describes the capabilities of the audit info repository + /// + public interface IAuditInformationRepository + { + /// + /// retrieves a list of audit information for given pagination params + /// + /// pagination info + /// + Task> ListAuditInformation(PaginationInfo paginationInfo); + } +} diff --git a/SharedLibraryCore/Interfaces/IManagerCommand.cs b/SharedLibraryCore/Interfaces/IManagerCommand.cs index 7ab87395c..e0d35fbea 100644 --- a/SharedLibraryCore/Interfaces/IManagerCommand.cs +++ b/SharedLibraryCore/Interfaces/IManagerCommand.cs @@ -44,5 +44,10 @@ namespace SharedLibraryCore.Interfaces /// Indicates if target is required /// bool RequiresTarget { get; } + + /// + /// Indicates if the commands can be run as another client + /// + bool AllowImpersonation { get; } } } diff --git a/SharedLibraryCore/Migrations/20200423225137_AddImpersonationIdToEFChangeHistory.Designer.cs b/SharedLibraryCore/Migrations/20200423225137_AddImpersonationIdToEFChangeHistory.Designer.cs new file mode 100644 index 000000000..9720f13f5 --- /dev/null +++ b/SharedLibraryCore/Migrations/20200423225137_AddImpersonationIdToEFChangeHistory.Designer.cs @@ -0,0 +1,919 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SharedLibraryCore.Database; + +namespace SharedLibraryCore.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20200423225137_AddImpersonationIdToEFChangeHistory")] + partial class AddImpersonationIdToEFChangeHistory + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.3"); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CurrentSessionLength") + .HasColumnType("INTEGER"); + + b.Property("CurrentStrain") + .HasColumnType("REAL"); + + b.Property("CurrentViewAngleId") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("Distance") + .HasColumnType("REAL"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("HitDestinationId") + .HasColumnType("INTEGER"); + + b.Property("HitLocation") + .HasColumnType("INTEGER"); + + b.Property("HitOriginId") + .HasColumnType("INTEGER"); + + b.Property("HitType") + .HasColumnType("INTEGER"); + + b.Property("Hits") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("LastStrainAngleId") + .HasColumnType("INTEGER"); + + b.Property("RecoilOffset") + .HasColumnType("REAL"); + + b.Property("SessionAngleOffset") + .HasColumnType("REAL"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("REAL"); + + b.Property("SessionSPM") + .HasColumnType("REAL"); + + b.Property("SessionScore") + .HasColumnType("INTEGER"); + + b.Property("SessionSnapHits") + .HasColumnType("INTEGER"); + + b.Property("StrainAngleBetween") + .HasColumnType("REAL"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.ToTable("EFACSnapshot"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("SnapshotId") + .HasColumnType("INTEGER"); + + b.Property("Vector3Id") + .HasColumnType("INTEGER"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AttackerId") + .HasColumnType("INTEGER"); + + b.Property("Damage") + .HasColumnType("INTEGER"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("DeathType") + .HasColumnType("INTEGER"); + + b.Property("Fraction") + .HasColumnType("REAL"); + + b.Property("HitLoc") + .HasColumnType("INTEGER"); + + b.Property("IsKill") + .HasColumnType("INTEGER"); + + b.Property("KillOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("Map") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("VictimId") + .HasColumnType("INTEGER"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("INTEGER"); + + b.Property("VisibilityPercentage") + .HasColumnType("REAL"); + + b.Property("Weapon") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TimeSent") + .HasColumnType("TEXT"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AverageRecoilOffset") + .HasColumnType("REAL"); + + b.Property("AverageSnapValue") + .HasColumnType("REAL"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MaxStrain") + .HasColumnType("REAL"); + + b.Property("RollingWeightedKDR") + .HasColumnType("REAL"); + + b.Property("SPM") + .HasColumnType("REAL"); + + b.Property("Skill") + .HasColumnType("REAL"); + + b.Property("SnapHitCount") + .HasColumnType("INTEGER"); + + b.Property("TimePlayed") + .HasColumnType("INTEGER"); + + b.Property("VisionAverage") + .HasColumnType("REAL"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientStatistics"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EFClientStatisticsClientId") + .HasColumnName("EFClientStatisticsClientId") + .HasColumnType("INTEGER"); + + b.Property("EFClientStatisticsServerId") + .HasColumnName("EFClientStatisticsServerId") + .HasColumnType("INTEGER"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitOffsetAverage") + .HasColumnType("REAL"); + + b.Property("Location") + .HasColumnType("INTEGER"); + + b.Property("MaxAngleDistance") + .HasColumnType("REAL"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ActivityAmount") + .HasColumnType("INTEGER"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("Performance") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("RatingHistoryId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.ToTable("EFRating"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EndPoint") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.HasKey("ServerId"); + + b.ToTable("EFServers"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalKills") + .HasColumnType("INTEGER"); + + b.Property("TotalPlayTime") + .HasColumnType("INTEGER"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("IPAddress") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(24); + + b.Property("SearchableName") + .HasColumnType("TEXT") + .HasMaxLength(24); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress") + .IsUnique(); + + b.ToTable("EFAlias"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.Property("CurrentValue") + .HasColumnType("TEXT"); + + b.Property("ImpersonationEntityId") + .HasColumnType("INTEGER"); + + b.Property("OriginEntityId") + .HasColumnType("INTEGER"); + + b.Property("PreviousValue") + .HasColumnType("TEXT"); + + b.Property("TargetEntityId") + .HasColumnType("INTEGER"); + + b.Property("TimeChanged") + .HasColumnType("TEXT"); + + b.Property("TypeOfChange") + .HasColumnType("INTEGER"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AliasLinkId") + .HasColumnType("INTEGER"); + + b.Property("Connections") + .HasColumnType("INTEGER"); + + b.Property("CurrentAliasId") + .HasColumnType("INTEGER"); + + b.Property("FirstConnection") + .HasColumnType("TEXT"); + + b.Property("LastConnection") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Masked") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("PasswordSalt") + .HasColumnType("TEXT"); + + b.Property("TotalConnectionTime") + .HasColumnType("INTEGER"); + + b.HasKey("ClientId"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("NetworkId") + .IsUnique(); + + b.ToTable("EFClients"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Extra") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(32); + + b.Property("Updated") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AutomatedOffense") + .HasColumnType("TEXT"); + + b.Property("Expires") + .HasColumnType("TEXT"); + + b.Property("IsEvadedOffense") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("OffenderId") + .HasColumnType("INTEGER"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PunisherId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties"); + }); + + modelBuilder.Entity("SharedLibraryCore.Helpers.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshotVector3", b => + { + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b => + { + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b => + { + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Database.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFMeta", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SharedLibraryCore/Migrations/20200423225137_AddImpersonationIdToEFChangeHistory.cs b/SharedLibraryCore/Migrations/20200423225137_AddImpersonationIdToEFChangeHistory.cs new file mode 100644 index 000000000..9b11124d1 --- /dev/null +++ b/SharedLibraryCore/Migrations/20200423225137_AddImpersonationIdToEFChangeHistory.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace SharedLibraryCore.Migrations +{ + public partial class AddImpersonationIdToEFChangeHistory : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ImpersonationEntityId", + table: "EFChangeHistory", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ImpersonationEntityId", + table: "EFChangeHistory"); + } + } +} diff --git a/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs b/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs index c36dee7f6..2620ced31 100644 --- a/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs +++ b/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs @@ -14,7 +14,7 @@ namespace SharedLibraryCore.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "3.1.0"); + .HasAnnotation("ProductVersion", "3.1.3"); modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", b => { @@ -511,6 +511,9 @@ namespace SharedLibraryCore.Migrations b.Property("CurrentValue") .HasColumnType("TEXT"); + b.Property("ImpersonationEntityId") + .HasColumnType("INTEGER"); + b.Property("OriginEntityId") .HasColumnType("INTEGER"); diff --git a/SharedLibraryCore/Repositories/AuditInformationRepository.cs b/SharedLibraryCore/Repositories/AuditInformationRepository.cs new file mode 100644 index 000000000..90f327e43 --- /dev/null +++ b/SharedLibraryCore/Repositories/AuditInformationRepository.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore; +using SharedLibraryCore.Dtos; +using SharedLibraryCore.Interfaces; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace SharedLibraryCore.Repositories +{ + /// + /// implementation if IAuditInformationRepository + /// + public class AuditInformationRepository : IAuditInformationRepository + { + private readonly IDatabaseContextFactory _contextFactory; + + public AuditInformationRepository(IDatabaseContextFactory contextFactory) + { + _contextFactory = contextFactory; + } + + /// + public async Task> ListAuditInformation(PaginationInfo paginationInfo) + { + using (var ctx = _contextFactory.CreateContext(enableTracking: false)) + { + var iqItems = (from change in ctx.EFChangeHistory + where change.TypeOfChange != Database.Models.EFChangeHistory.ChangeType.Ban + orderby change.TimeChanged descending + join originClient in ctx.Clients + on (change.ImpersonationEntityId ?? change.OriginEntityId) equals originClient.ClientId + join targetClient in ctx.Clients + on change.TargetEntityId equals targetClient.ClientId + into targetChange + from targetClient in targetChange.DefaultIfEmpty() + select new AuditInfo() + { + Action = change.TypeOfChange.ToString(), + OriginName = originClient.CurrentAlias.Name, + OriginId = originClient.ClientId, + TargetName = targetClient == null ? "" : targetClient.CurrentAlias.Name, + TargetId = targetClient == null ? new int?() : targetClient.ClientId, + When = change.TimeChanged, + Data = change.Comment, + OldValue = change.PreviousValue, + NewValue = change.CurrentValue + }) + .Skip(paginationInfo.Offset) + .Take(paginationInfo.Count); + + return await iqItems.ToListAsync(); + } + } + } +} diff --git a/SharedLibraryCore/Services/ChangeHistoryService.cs b/SharedLibraryCore/Services/ChangeHistoryService.cs index b540c008a..7b3ceefb3 100644 --- a/SharedLibraryCore/Services/ChangeHistoryService.cs +++ b/SharedLibraryCore/Services/ChangeHistoryService.cs @@ -26,6 +26,7 @@ namespace SharedLibraryCore.Services { OriginEntityId = e.Origin.ClientId, TargetEntityId = e.Target.ClientId, + ImpersonationEntityId = e.ImpersonationOrigin?.ClientId, TypeOfChange = EFChangeHistory.ChangeType.Ban, Comment = e.Data }; @@ -43,6 +44,7 @@ namespace SharedLibraryCore.Services { OriginEntityId = e.Origin.ClientId, TargetEntityId = e.Target?.ClientId ?? 0, + ImpersonationEntityId = e.ImpersonationOrigin?.ClientId, Comment = "Executed command", CurrentValue = e.Message, TypeOfChange = EFChangeHistory.ChangeType.Command @@ -53,6 +55,7 @@ namespace SharedLibraryCore.Services { OriginEntityId = e.Origin.ClientId, TargetEntityId = e.Target.ClientId, + ImpersonationEntityId = e.ImpersonationOrigin?.ClientId, Comment = "Changed permission level", TypeOfChange = EFChangeHistory.ChangeType.Permission, CurrentValue = ((EFClient.Permission)e.Extra).ToString() diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs index ad6a342b4..80afde181 100644 --- a/SharedLibraryCore/Utilities.cs +++ b/SharedLibraryCore/Utilities.cs @@ -32,9 +32,9 @@ namespace SharedLibraryCore #endif public static Encoding EncodingType; public static Localization.Layout CurrentLocalization = new Localization.Layout(new Dictionary()); - public static TimeSpan DefaultCommandTimeout = new TimeSpan(0, 0, 25); + public static TimeSpan DefaultCommandTimeout { get; set; } = new TimeSpan(0, 0, 25); public static char[] DirectorySeparatorChars = new[] { '\\', '/' }; - + public static char CommandPrefix { get; set; } = '!'; public static EFClient IW4MAdminClient(Server server = null) { return new EFClient() diff --git a/Tests/ApplicationTests/CommandTests.cs b/Tests/ApplicationTests/CommandTests.cs new file mode 100644 index 000000000..f6bb9ec5b --- /dev/null +++ b/Tests/ApplicationTests/CommandTests.cs @@ -0,0 +1,178 @@ +using NUnit.Framework; +using System; +using SharedLibraryCore.Interfaces; +using IW4MAdmin; +using FakeItEasy; +using IW4MAdmin.Application.EventParsers; +using System.Linq; +using IW4MAdmin.Plugins.Stats.Models; +using IW4MAdmin.Application.Helpers; +using IW4MAdmin.Plugins.Stats.Config; +using System.Collections.Generic; +using SharedLibraryCore.Database.Models; +using Microsoft.Extensions.DependencyInjection; +using IW4MAdmin.Plugins.Stats.Helpers; +using ApplicationTests.Fixtures; +using System.Threading.Tasks; +using SharedLibraryCore.Commands; +using SharedLibraryCore.Configuration; +using SharedLibraryCore; +using ApplicationTests.Mocks; + +namespace ApplicationTests +{ + [TestFixture] + public class CommandTests + { + ILogger logger; + private IServiceProvider serviceProvider; + private ITranslationLookup transLookup; + private CommandConfiguration cmdConfig; + private MockEventHandler mockEventHandler; + + [SetUp] + public void Setup() + { + logger = A.Fake(); + cmdConfig = new CommandConfiguration(); + + serviceProvider = new ServiceCollection() + .BuildBase() + .BuildServiceProvider(); + + mockEventHandler = new MockEventHandler(true); + A.CallTo(() => serviceProvider.GetRequiredService().GetEventHandler()) + .Returns(mockEventHandler); + + var mgr = serviceProvider.GetRequiredService(); + transLookup = serviceProvider.GetRequiredService(); + + A.CallTo(() => mgr.GetCommands()) + .Returns(new Command[] + { + new ImpersonatableCommand(cmdConfig, transLookup), + new NonImpersonatableCommand(cmdConfig, transLookup) + }); + + //Utilities.DefaultCommandTimeout = new TimeSpan(0, 0, 2); + } + + #region RUNAS + [Test] + public async Task Test_RunAsFailsOnSelf() + { + var cmd = new RunAsCommand(cmdConfig, transLookup); + var server = serviceProvider.GetRequiredService(); + var target = ClientGenerators.CreateBasicClient(server); + + var gameEvent = new GameEvent() + { + Target = target, + Origin = target + }; + + await cmd.ExecuteAsync(gameEvent); + + Assert.IsNotEmpty(mockEventHandler.Events.Where(_event => _event.Type == GameEvent.EventType.Tell)); + Assert.IsEmpty(mockEventHandler.Events.Where(_event => _event.Type == GameEvent.EventType.Command)); + } + + [Test] + public async Task Test_RunAsFailsOnHigherPrivilege() + { + var cmd = new RunAsCommand(cmdConfig, transLookup); + var server = serviceProvider.GetRequiredService(); + var target = ClientGenerators.CreateBasicClient(server); + target.Level = EFClient.Permission.Administrator; + var origin = ClientGenerators.CreateBasicClient(server); + origin.NetworkId = 100; + origin.Level = EFClient.Permission.Moderator; + + var gameEvent = new GameEvent() + { + Target = target, + Origin = origin + }; + + await cmd.ExecuteAsync(gameEvent); + + Assert.IsNotEmpty(mockEventHandler.Events.Where(_event => _event.Type == GameEvent.EventType.Tell)); + Assert.IsEmpty(mockEventHandler.Events.Where(_event => _event.Type == GameEvent.EventType.Command)); + } + + [Test] + public async Task Test_RunAsFailsOnSamePrivilege() + { + var cmd = new RunAsCommand(cmdConfig, transLookup); + var server = serviceProvider.GetRequiredService(); + var target = ClientGenerators.CreateBasicClient(server); + target.Level = EFClient.Permission.Administrator; + var origin = ClientGenerators.CreateBasicClient(server); + origin.NetworkId = 100; + origin.Level = EFClient.Permission.Administrator; + + var gameEvent = new GameEvent() + { + Target = target, + Origin = origin + }; + + await cmd.ExecuteAsync(gameEvent); + + Assert.IsNotEmpty(mockEventHandler.Events.Where(_event => _event.Type == GameEvent.EventType.Tell)); + Assert.IsEmpty(mockEventHandler.Events.Where(_event => _event.Type == GameEvent.EventType.Command)); + } + + [Test] + public async Task Test_RunAsFailsOnDisallowedCommand() + { + var cmd = new RunAsCommand(cmdConfig, transLookup); + var server = serviceProvider.GetRequiredService(); + var target = ClientGenerators.CreateBasicClient(server); + target.Level = EFClient.Permission.Moderator; + var origin = ClientGenerators.CreateBasicClient(server); + origin.NetworkId = 100; + origin.Level = EFClient.Permission.Administrator; + + var gameEvent = new GameEvent() + { + Target = target, + Origin = origin, + Owner = server, + Data = nameof(NonImpersonatableCommand) + }; + + await cmd.ExecuteAsync(gameEvent); + + Assert.IsNotEmpty(mockEventHandler.Events.Where(_event => _event.Type == GameEvent.EventType.Tell)); + // failed when validating the command + Assert.IsNotEmpty(mockEventHandler.Events.Where(_event => _event.Type == GameEvent.EventType.Command && _event.FailReason == GameEvent.EventFailReason.Invalid)); + } + + [Test] + public async Task Test_RunAsQueuesEventAndResponse() + { + var cmd = new RunAsCommand(cmdConfig, transLookup); + var server = serviceProvider.GetRequiredService(); + var target = ClientGenerators.CreateBasicClient(server); + target.Level = EFClient.Permission.Moderator; + var origin = ClientGenerators.CreateBasicClient(server); + origin.NetworkId = 100; + origin.Level = EFClient.Permission.Administrator; + + var gameEvent = new GameEvent() + { + Target = target, + Origin = origin, + Data = nameof(ImpersonatableCommand), + Owner = server + }; + + await cmd.ExecuteAsync(gameEvent); + + Assert.IsNotEmpty(mockEventHandler.Events.Where(_event => _event.Type == GameEvent.EventType.Tell /*&& _event.Target == origin todo: fake the command result*/ )); + Assert.IsNotEmpty(mockEventHandler.Events.Where(_event => _event.Type == GameEvent.EventType.Command && !_event.Failed)); + } + #endregion + } +} diff --git a/Tests/ApplicationTests/Mocks/Commands.cs b/Tests/ApplicationTests/Mocks/Commands.cs new file mode 100644 index 000000000..be6d0e2ba --- /dev/null +++ b/Tests/ApplicationTests/Mocks/Commands.cs @@ -0,0 +1,36 @@ +using SharedLibraryCore; +using SharedLibraryCore.Configuration; +using SharedLibraryCore.Interfaces; +using System; +using System.Threading.Tasks; + +namespace ApplicationTests.Mocks +{ + class ImpersonatableCommand : Command + { + public ImpersonatableCommand(CommandConfiguration config, ITranslationLookup lookup) : base(config, lookup) + { + AllowImpersonation = true; + Name = nameof(ImpersonatableCommand); + } + + public override Task ExecuteAsync(GameEvent E) + { + E.Origin.Tell("test"); + return Task.CompletedTask; + } + } + + class NonImpersonatableCommand : Command + { + public NonImpersonatableCommand(CommandConfiguration config, ITranslationLookup lookup) : base(config, lookup) + { + Name = nameof(NonImpersonatableCommand); + } + + public override Task ExecuteAsync(GameEvent E) + { + return Task.CompletedTask; + } + } +} diff --git a/Tests/ApplicationTests/Mocks/EventHandler.cs b/Tests/ApplicationTests/Mocks/EventHandler.cs index fda78c697..ec1caea20 100644 --- a/Tests/ApplicationTests/Mocks/EventHandler.cs +++ b/Tests/ApplicationTests/Mocks/EventHandler.cs @@ -7,10 +7,22 @@ namespace ApplicationTests.Mocks class MockEventHandler : IEventHandler { public IList Events = new List(); + private readonly bool _autoExecute; + + public MockEventHandler(bool autoExecute = false) + { + _autoExecute = autoExecute; + } public void AddEvent(GameEvent gameEvent) { Events.Add(gameEvent); + + if (_autoExecute) + { + gameEvent.Owner?.ExecuteEvent(gameEvent); + gameEvent.Complete(); + } } } } diff --git a/WebfrontCore/Controllers/AdminController.cs b/WebfrontCore/Controllers/AdminController.cs new file mode 100644 index 000000000..e8215e2ca --- /dev/null +++ b/WebfrontCore/Controllers/AdminController.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using SharedLibraryCore; +using SharedLibraryCore.Dtos; +using SharedLibraryCore.Interfaces; +using System.Threading.Tasks; + +namespace WebfrontCore.Controllers +{ + public class AdminController : BaseController + { + private readonly IAuditInformationRepository _auditInformationRepository; + private readonly ITranslationLookup _translationLookup; + private static readonly int DEFAULT_COUNT = 25; + + public AdminController(IManager manager, IAuditInformationRepository auditInformationRepository, ITranslationLookup translationLookup) : base(manager) + { + _auditInformationRepository = auditInformationRepository; + _translationLookup = translationLookup; + } + + [Authorize] + public async Task AuditLog() + { + ViewBag.EnableColorCodes = Manager.GetApplicationSettings().Configuration().EnableColorCodes; + ViewBag.IsFluid = true; + ViewBag.Title = _translationLookup["WEBFRONT_NAV_AUDIT_LOG"]; + ViewBag.InitialOffset = DEFAULT_COUNT; + + var auditItems = await _auditInformationRepository.ListAuditInformation(new PaginationInfo() + { + Count = DEFAULT_COUNT + }); + + return View(auditItems); + } + + public async Task ListAuditLog([FromQuery] PaginationInfo paginationInfo) + { + ViewBag.EnableColorCodes = Manager.GetApplicationSettings().Configuration().EnableColorCodes; + var auditItems = await _auditInformationRepository.ListAuditInformation(paginationInfo); + return PartialView("_ListAuditLog", auditItems); + } + } +} diff --git a/WebfrontCore/Startup.cs b/WebfrontCore/Startup.cs index 66edbc0f8..2d6341c1c 100644 --- a/WebfrontCore/Startup.cs +++ b/WebfrontCore/Startup.cs @@ -101,7 +101,12 @@ namespace WebfrontCore #endif services.AddSingleton(Program.Manager); + + // todo: this needs to be handled more gracefully services.AddSingleton(Program.ApplicationServiceProvider.GetService()); + services.AddSingleton(Program.ApplicationServiceProvider.GetService()); + services.AddSingleton(Program.ApplicationServiceProvider.GetService()); + services.AddSingleton(Program.ApplicationServiceProvider.GetService()); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/WebfrontCore/Views/Admin/AuditLog.cshtml b/WebfrontCore/Views/Admin/AuditLog.cshtml new file mode 100644 index 000000000..87a0ef8b8 --- /dev/null +++ b/WebfrontCore/Views/Admin/AuditLog.cshtml @@ -0,0 +1,34 @@ +@{ + var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex; +} +

@ViewBag.Title

+ + + + + + + + + + + + + + + + + +
@loc["WEBFRONT_PENALTY_TEMPLATE_TYPE"]@loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]@loc["WEBFRONT_ADMIN_AUDIT_LOG_INFO"]@loc["WEBFRONT_ADMIN_AUDIT_LOG_CURRENT"]@loc["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]
+ + +@section scripts { + + + + +} diff --git a/WebfrontCore/Views/Admin/_ListAuditLog.cshtml b/WebfrontCore/Views/Admin/_ListAuditLog.cshtml new file mode 100644 index 000000000..8d8a68025 --- /dev/null +++ b/WebfrontCore/Views/Admin/_ListAuditLog.cshtml @@ -0,0 +1,99 @@ +@using SharedLibraryCore.Dtos +@model IEnumerable +@{ + var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex; +} + +@foreach (var info in Model) +{ + + + @loc["WEBFRONT_PENALTY_TEMPLATE_TYPE"] + + @info.Action + + + + @loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"] + + + + + + + + @loc["WEBFRONT_PENALTY_TEMPLATE_NAME"] + + @if (info.TargetId != null) + { + + + + } + else + { + -- + } + + + + @loc["WEBFRONT_ADMIN_AUDIT_LOG_INFO"] + + @info.Data + + + @* + @loc["WEBFRONT_ADMIN_AUDIT_LOG_PREVIOUS"] + + @info.OldValue + + *@ + + @loc["WEBFRONT_ADMIN_AUDIT_LOG_CURRENT"] + + @info.NewValue + + + + @loc["WEBFRONT_ADMIN_AUDIT_LOG_TIME"] + + @info.When.ToString() + + + + + + + @info.Action + + + + + + + + @if (info.TargetId != null) + { + + + + } + else + { + -- + } + + + @info.Data + + @* + @info.OldValue + *@ + + @info.NewValue + + + @info.When.ToString() + + +} \ No newline at end of file diff --git a/WebfrontCore/Views/Shared/_Layout.cshtml b/WebfrontCore/Views/Shared/_Layout.cshtml index e8f07b5af..abf01e8ff 100644 --- a/WebfrontCore/Views/Shared/_Layout.cshtml +++ b/WebfrontCore/Views/Shared/_Layout.cshtml @@ -39,38 +39,39 @@ @foreach (var _page in ViewBag.Pages) { - + } @if (!string.IsNullOrEmpty(ViewBag.SocialLink)) { - + } @if (ViewBag.Authorized) { - + @loc["WEBFRONT_NAV_AUDIT_LOG"] + @loc["WEBFRONT_ACTION_RECENT_CLIENTS"] + @loc["WEBFRONT_ACTION_TOKEN"] + @loc["WEBFRONT_NAV_LOGOUT"] + + } else { - + }