Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auditlog #73

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 7 additions & 26 deletions SS14.Admin/AdminLogs/AdminLogRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
using Content.Server.Database;
using Content.Shared.Database;
using Microsoft.EntityFrameworkCore;
using SS14.Admin.Pages.Logs;
using SS14.Admin.Helpers;
using SS14.Admin.Pages.AdminLogs;

namespace SS14.Admin.AdminLogs;

public static class AdminLogRepository
{
private static readonly Regex ParameterRegex = new(Regex.Escape("#"));

public static async Task<List<AdminLog>> FindAdminLogs(
ServerDbContext context,
DbSet<AdminLog> adminLogs,
Expand All @@ -21,7 +20,7 @@ public static async Task<List<AdminLog>> FindAdminLogs(
string? search,
int? roundId,
int? severity,
LogsIndexModel.OrderColumn sort,
AdminLogsIndexModel.OrderColumn sort,
int limit = 100, int offset = 0)
{
var fromDateValue = fromDate?.ToString("o", CultureInfo.InvariantCulture);
Expand All @@ -48,8 +47,8 @@ public static async Task<List<AdminLog>> FindAdminLogs(

var sortStatement = sort switch
{
LogsIndexModel.OrderColumn.Date => "a.date",
LogsIndexModel.OrderColumn.Impact => "a.impact",
AdminLogsIndexModel.OrderColumn.Date => "a.date",
AdminLogsIndexModel.OrderColumn.Impact => "a.impact",
_ => throw new ArgumentOutOfRangeException(nameof(sort), sort, "Unknown admin log sort column")
};

Expand All @@ -63,38 +62,20 @@ public static async Task<List<AdminLog>> FindAdminLogs(
{(playerUserId != null ? "p.player_user_id = # AND" : "")}
{(serverName != null ? "s.name = # AND" : "")}
{(typeInt != null ? "a.type = #::integer AND" : "")}
{(search != null ? $"{TextSearchForContext(context)} AND" : "")}
{(search != null ? $"{RawSqlHelper.TextSearchForContext(context)} AND" : "")}
{(roundId != null ? "r.round_id = #::integer AND" : "")}
{(severity != null ? "a.impact = #::integer AND" : "")}
TRUE
ORDER BY {sortStatement} DESC
LIMIT #::bigint OFFSET #::bigint
";

var result = adminLogs.FromSqlRaw(EnumerateParameters(query), values.ToArray());
var result = adminLogs.FromSqlRaw(RawSqlHelper.EnumerateParameters(query), values.ToArray());
return await result.ToListAsync();
}

public static async Task<Player?> FindPlayerByName(DbSet<Player> players, string name)
{
return await players.FromSqlRaw("SELECT * FROM player WHERE last_seen_user_name = {0}", name).SingleOrDefaultAsync();
}

private static string TextSearchForContext(ServerDbContext context)
{
return context is PostgresServerDbContext ? "to_tsvector('english'::regconfig, a.message) @@ websearch_to_tsquery('english'::regconfig, #)" : " a.message LIKE %#%";
}

private static string EnumerateParameters(string query)
{
var index = 0;

while (ParameterRegex.IsMatch(query))
{
query = ParameterRegex.Replace(query, $"{{{index}}}", 1);
index += 1;
}

return query;
}
}
5 changes: 5 additions & 0 deletions SS14.Admin/Admins/AdminFlags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ public enum AdminFlags : uint
/// </summary>
NameColor = 1 << 21,

/// <summary>
/// Lets you view the audit log
/// </summary>
Audit = 1 << 22,

/// <summary>
/// Dangerous host permissions like scsi.
/// </summary>
Expand Down
65 changes: 65 additions & 0 deletions SS14.Admin/AuditLogs/AuditLogRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System.Globalization;
using Content.Server.Database;
using Content.Shared.Database;
using Microsoft.EntityFrameworkCore;
using SS14.Admin.Helpers;

namespace SS14.Admin.AuditLogs;

public static class AuditLogRepository
{
public static async Task<List<AuditLog>> FindAuditLogs(
ServerDbContext context,
Guid? authorUserId,
Guid? effectedUserId,
DateTime? fromDate, DateTime? toDate,
AuditLogType? type,
string? textSearch,
int? severity,
int limit = 100, int offset = 0)
{
var fromDateValue = fromDate?.ToString("o", CultureInfo.InvariantCulture);
var toDateValue = toDate?.AddHours(23).ToString("o", CultureInfo.InvariantCulture);
uint? typeInt = type.HasValue ? Convert.ToUInt32(type) : null;

var values = new List<object>();
if (fromDateValue != null && toDateValue != null)
{
values.Add(fromDateValue);
values.Add(toDateValue);
}

if (authorUserId != null) values.Add(authorUserId);
if (effectedUserId != null) values.Add(effectedUserId);
if (typeInt != null) values.Add(typeInt);
if (textSearch != null) values.Add(textSearch);
if (severity != null) values.Add(severity);

values.Add(limit.ToString());
values.Add(offset.ToString());

var query = $@"
SELECT a.audit_log_id, a.type, a.impact, a.date, a.message, a.author_user_id FROM audit_log AS a
{(effectedUserId != null ? "INNER JOIN audit_log_effected_player AS p ON p.audit_log_id = a.audit_log_id" : "")}
WHERE
{(fromDateValue != null && toDateValue != null ? "a.date BETWEEN #::timestamp with time zone AND #::timestamp with time zone AND" : "")}
{(authorUserId != null ? "a.author_user_id = # AND" : "")}
{(effectedUserId != null ? "p.effected_user_id = # AND" : "")}
{(typeInt != null ? "a.type = #::integer AND" : "")}
{(textSearch != null ? $"{RawSqlHelper.TextSearchForContext(context)} AND" : "")}
{(severity != null ? "a.impact = #::integer AND" : "")}
TRUE
ORDER BY a.date DESC
LIMIT #::bigint OFFSET #::bigint
";

var result = context.AuditLog.FromSqlRaw(RawSqlHelper.EnumerateParameters(query), values.ToArray());
var logs = await result.ToListAsync();
foreach (var log in logs)
{
//log.Effected = await context.AuditLogEffectedPlayer.FromSqlRaw($"SELECT a.audit_log_effected_players_id, a.audit_log_id, a.effected_user_id FROM audit_log_effected_player AS a WHERE a.audit_log_id = {{0}}", log.Id).ToListAsync();
log.Effected = await context.AuditLogEffectedPlayer.Where(p => p.AuditLogId == log.Id).ToListAsync();
}
return logs;
}
}
84 changes: 84 additions & 0 deletions SS14.Admin/Helpers/AuditHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System.Text;
using Content.Server.Database;
using Content.Shared.Database;
using Microsoft.EntityFrameworkCore;

namespace SS14.Admin.Helpers;

public static class AuditHelper
{
public static async Task AddUnsavedLogAsync(ServerDbContext db, AuditLogType ty, LogImpact impact, Guid? author, string message, List<Guid>? effected = null)
{
var dbEffected= effected == null
? []
: effected.Select(id => new AuditLogEffectedPlayer() { EffectedUserId = id }).ToList();
var log = new AuditLog()
{
Type = ty,
Impact = impact,
AuthorUserId = author,
Message = message,
Date = DateTime.UtcNow,
Effected = dbEffected,
};
db.AuditLog.Add(log);
}

public static async Task<string> GetNameFromUidOrDefault(ServerDbContext db, Guid? author)
{
if (author == null)
return "System";

return (await db.Player.FirstOrDefaultAsync(p => p.UserId == author))?.LastSeenUserName ?? "System";
}

// I know this takes 1000 parameters, but this is needed for a consistent format for reachability.
public static async Task UnsavedLogForAddRemarkAsync(ServerDbContext db, NoteType type, int noteId, bool secret, Guid? authorUid, string message, DateTime? expiryTime, Guid? target, NoteSeverity? severity = null)
{
var authorName = await GetNameFromUidOrDefault(db, authorUid);
var sb = new StringBuilder($"{authorName} added");

if (secret && type == NoteType.Note)
{
sb.Append(" secret");
}

sb.Append($" {type} {noteId} with message \"{message}\"");

switch (type)
{
case NoteType.Note:
sb.Append($" with {severity} severity");
break;
case NoteType.Message:
break;
case NoteType.Watchlist:
break;
case NoteType.ServerBan:
break;
case NoteType.RoleBan:
break;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown note type");
}

if (expiryTime is not null)
{
sb.Append($" which expires on {expiryTime.Value.ToUniversalTime(): yyyy-MM-dd HH:mm:ss} UTC");
}

await AddUnsavedLogAsync(
db, AuditLogUtil.LogTypeForRemark(type), AuditLogUtil.LogImpactForRemark(type, severity),
authorUid, sb.ToString(), target != null ? [target.Value] : []);
}

public static async Task UnsavedLogForRemoveRemakAsync(ServerDbContext db, NoteType type, int noteId, Guid? remover,
Guid? target, NoteSeverity? severity = null)
{
var removerName = await GetNameFromUidOrDefault(db, remover);
var logStr = $"{removerName} has deleted {type} {noteId}";
await AddUnsavedLogAsync(db, AuditLogUtil.LogTypeForRemark(type),
AuditLogUtil.LogImpactForRemark(type, severity),
remover, logStr, target != null ? [target.Value] : []);
}
}
20 changes: 20 additions & 0 deletions SS14.Admin/Helpers/AuditLogTypeHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Text.Json;
using Content.Shared.Database;

namespace SS14.Admin.Helpers;

internal static class AuditLogTypeHelper
{
public static string AuditLogTypeJson { get; }

static AuditLogTypeHelper()
{
var dict = new Dictionary<string, int>();
foreach (var type in Enum.GetNames<AuditLogType>())
{
dict[type] = (int)Enum.Parse<AuditLogType>(type);
}

AuditLogTypeJson = JsonSerializer.Serialize(dict);
}
}
33 changes: 33 additions & 0 deletions SS14.Admin/Helpers/RawSqlHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Text.RegularExpressions;
using Content.Server.Database;

namespace SS14.Admin.Helpers;

/// <summary>
/// Misc utilities for working with raw SQL
/// </summary>
public static class RawSqlHelper
{
private static readonly Regex ParameterRegex = new(Regex.Escape("#"));

public static string TextSearchForContext(ServerDbContext context)
{
return context is PostgresServerDbContext ? "to_tsvector('english'::regconfig, a.message) @@ websearch_to_tsquery('english'::regconfig, #)" : " a.message LIKE %#%";
}

/// <summary>
/// Replace "#" with the next parameter number. Used for when you have a variable ammount of parameters.
/// </summary>
public static string EnumerateParameters(string query)
{
var index = 0;

while (ParameterRegex.IsMatch(query))
{
query = ParameterRegex.Replace(query, $"{{{index}}}", 1);
index += 1;
}

return query;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@page
@model SS14.Admin.Pages.Logs.LogsIndexModel
@model AdminLogsIndexModel
@using SS14.Admin.AdminLogs
@using SS14.Admin.Helpers
@using Microsoft.AspNetCore.Mvc.TagHelpers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
using System.Runtime.InteropServices.JavaScript;
using System.Text.Json;
using Content.Server.Database;
using Content.Server.Database;
using Content.Shared.Database;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using SS14.Admin.AdminLogs;
using SS14.Admin.Models;

namespace SS14.Admin.Pages.Logs;
namespace SS14.Admin.Pages.AdminLogs;

public class LogsIndexModel : PageModel
public class AdminLogsIndexModel : PageModel
{
private readonly PostgresServerDbContext _dbContext;

Expand Down Expand Up @@ -48,7 +45,7 @@ public class LogsIndexModel : PageModel
public string? ServerSearch { get; set; }
public string? Search { get; set; }

public LogsIndexModel(PostgresServerDbContext dbContext)
public AdminLogsIndexModel(PostgresServerDbContext dbContext)
{
_dbContext = dbContext;
}
Expand Down
Loading
Loading