Persist deadmin to database, add admin suspension system (#34048)

This commit is contained in:
Pieter-Jan Briers
2025-01-15 00:46:45 +01:00
committed by GitHub
parent 47042cc8dd
commit c2e050ced0
15 changed files with 4284 additions and 25 deletions

View File

@@ -130,6 +130,7 @@ namespace Content.Client.Administration.UI
} }
var title = string.IsNullOrWhiteSpace(popup.TitleEdit.Text) ? null : popup.TitleEdit.Text; var title = string.IsNullOrWhiteSpace(popup.TitleEdit.Text) ? null : popup.TitleEdit.Text;
var suspended = popup.SuspendedCheckbox.Pressed;
if (popup.SourceData is { } src) if (popup.SourceData is { } src)
{ {
@@ -139,7 +140,8 @@ namespace Content.Client.Administration.UI
Title = title, Title = title,
PosFlags = pos, PosFlags = pos,
NegFlags = neg, NegFlags = neg,
RankId = rank RankId = rank,
Suspended = suspended,
}); });
} }
else else
@@ -152,7 +154,8 @@ namespace Content.Client.Administration.UI
Title = title, Title = title,
PosFlags = pos, PosFlags = pos,
NegFlags = neg, NegFlags = neg,
RankId = rank RankId = rank,
Suspended = suspended,
}); });
} }
@@ -171,7 +174,7 @@ namespace Content.Client.Administration.UI
{ {
Id = src, Id = src,
Flags = flags, Flags = flags,
Name = name Name = name,
}); });
} }
else else
@@ -351,6 +354,7 @@ namespace Content.Client.Administration.UI
public readonly OptionButton RankButton; public readonly OptionButton RankButton;
public readonly Button SaveButton; public readonly Button SaveButton;
public readonly Button? RemoveButton; public readonly Button? RemoveButton;
public readonly CheckBox SuspendedCheckbox;
public readonly Dictionary<AdminFlags, (Button inherit, Button sub, Button plus)> FlagButtons public readonly Dictionary<AdminFlags, (Button inherit, Button sub, Button plus)> FlagButtons
= new(); = new();
@@ -381,6 +385,12 @@ namespace Content.Client.Administration.UI
RankButton = new OptionButton(); RankButton = new OptionButton();
SaveButton = new Button { Text = Loc.GetString("permissions-eui-edit-admin-window-save-button"), HorizontalAlignment = HAlignment.Right }; SaveButton = new Button { Text = Loc.GetString("permissions-eui-edit-admin-window-save-button"), HorizontalAlignment = HAlignment.Right };
SuspendedCheckbox = new CheckBox
{
Text = Loc.GetString("permissions-eui-edit-admin-window-suspended"),
Pressed = data?.Suspended ?? false,
};
RankButton.AddItem(Loc.GetString("permissions-eui-edit-admin-window-no-rank-button"), NoRank); RankButton.AddItem(Loc.GetString("permissions-eui-edit-admin-window-no-rank-button"), NoRank);
foreach (var (rId, rank) in ui._ranks) foreach (var (rId, rank) in ui._ranks)
{ {
@@ -488,7 +498,8 @@ namespace Content.Client.Administration.UI
{ {
nameControl, nameControl,
TitleEdit, TitleEdit,
RankButton RankButton,
SuspendedCheckbox,
} }
}, },
permGrid permGrid

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class AdminStatus : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "deadminned",
table: "admin",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "suspended",
table: "admin",
type: "boolean",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "deadminned",
table: "admin");
migrationBuilder.DropColumn(
name: "suspended",
table: "admin");
}
}
}

View File

@@ -36,6 +36,14 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("integer") .HasColumnType("integer")
.HasColumnName("admin_rank_id"); .HasColumnName("admin_rank_id");
b.Property<bool>("Deadminned")
.HasColumnType("boolean")
.HasColumnName("deadminned");
b.Property<bool>("Suspended")
.HasColumnType("boolean")
.HasColumnName("suspended");
b.Property<string>("Title") b.Property<string>("Title")
.HasColumnType("text") .HasColumnType("text")
.HasColumnName("title"); .HasColumnName("title");

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class AdminStatus : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "deadminned",
table: "admin",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "suspended",
table: "admin",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "deadminned",
table: "admin");
migrationBuilder.DropColumn(
name: "suspended",
table: "admin");
}
}
}

View File

@@ -28,6 +28,14 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("INTEGER") .HasColumnType("INTEGER")
.HasColumnName("admin_rank_id"); .HasColumnName("admin_rank_id");
b.Property<bool>("Deadminned")
.HasColumnType("INTEGER")
.HasColumnName("deadminned");
b.Property<bool>("Suspended")
.HasColumnType("INTEGER")
.HasColumnName("suspended");
b.Property<string>("Title") b.Property<string>("Title")
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasColumnName("title"); .HasColumnName("title");

View File

@@ -610,6 +610,16 @@ namespace Content.Server.Database
[Key] public Guid UserId { get; set; } [Key] public Guid UserId { get; set; }
public string? Title { get; set; } public string? Title { get; set; }
/// <summary>
/// If true, the admin is voluntarily deadminned. They can re-admin at any time.
/// </summary>
public bool Deadminned { get; set; }
/// <summary>
/// If true, the admin is suspended by an admin with <c>PERMISSIONS</c>. They will not have in-game permissions.
/// </summary>
public bool Suspended { get; set; }
public int? AdminRankId { get; set; } public int? AdminRankId { get; set; }
public AdminRank? AdminRank { get; set; } public AdminRank? AdminRank { get; set; }
public List<AdminFlag> Flags { get; set; } = default!; public List<AdminFlag> Flags { get; set; } = default!;

View File

@@ -91,14 +91,29 @@ namespace Content.Server.Administration.Managers
_chat.SendAdminAnnouncement(Loc.GetString("admin-manager-self-de-admin-message", ("exAdminName", session.Name))); _chat.SendAdminAnnouncement(Loc.GetString("admin-manager-self-de-admin-message", ("exAdminName", session.Name)));
_chat.DispatchServerMessage(session, Loc.GetString("admin-manager-became-normal-player-message")); _chat.DispatchServerMessage(session, Loc.GetString("admin-manager-became-normal-player-message"));
var plyData = session.ContentData()!; UpdateDatabaseDeadminnedState(session, true);
plyData.ExplicitlyDeadminned = true;
reg.Data.Active = false; reg.Data.Active = false;
SendPermsChangedEvent(session); SendPermsChangedEvent(session);
UpdateAdminStatus(session); UpdateAdminStatus(session);
} }
private async void UpdateDatabaseDeadminnedState(ICommonSession player, bool newState)
{
try
{
// NOTE: This function gets called if you deadmin/readmin from a transient admin status.
// (e.g. loginlocal)
// In which case there may not be a database record.
// The DB function handles this scenario fine, but it's worth noting.
await _dbManager.UpdateAdminDeadminnedAsync(player.UserId, newState);
}
catch (Exception e)
{
_sawmill.Error("Failed to save deadmin state to database for {Admin}", player.UserId);
}
}
public void Stealth(ICommonSession session) public void Stealth(ICommonSession session)
{ {
if (!_admins.TryGetValue(session, out var reg)) if (!_admins.TryGetValue(session, out var reg))
@@ -151,8 +166,7 @@ namespace Content.Server.Administration.Managers
_chat.DispatchServerMessage(session, Loc.GetString("admin-manager-became-admin-message")); _chat.DispatchServerMessage(session, Loc.GetString("admin-manager-became-admin-message"));
var plyData = session.ContentData()!; UpdateDatabaseDeadminnedState(session, false);
plyData.ExplicitlyDeadminned = false;
reg.Data.Active = true; reg.Data.Active = true;
if (!reg.Data.Stealth) if (!reg.Data.Stealth)
@@ -208,14 +222,14 @@ namespace Content.Server.Administration.Managers
curAdmin.IsSpecialLogin = special; curAdmin.IsSpecialLogin = special;
curAdmin.RankId = rankId; curAdmin.RankId = rankId;
curAdmin.Data = aData; curAdmin.Data = aData;
}
if (!player.ContentData()!.ExplicitlyDeadminned) if (curAdmin.Data.Active)
{ {
aData.Active = true; aData.Active = true;
_chat.DispatchServerMessage(player, Loc.GetString("admin-manager-admin-permissions-updated-message")); _chat.DispatchServerMessage(player, Loc.GetString("admin-manager-admin-permissions-updated-message"));
} }
}
if (player.ContentData()!.Stealthed) if (player.ContentData()!.Stealthed)
{ {
@@ -381,10 +395,8 @@ namespace Content.Server.Administration.Managers
if (session.ContentData()!.Stealthed) if (session.ContentData()!.Stealthed)
reg.Data.Stealth = true; reg.Data.Stealth = true;
if (!session.ContentData()!.ExplicitlyDeadminned) if (reg.Data.Active)
{ {
reg.Data.Active = true;
if (_cfg.GetCVar(CCVars.AdminAnnounceLogin)) if (_cfg.GetCVar(CCVars.AdminAnnounceLogin))
{ {
if (reg.Data.Stealth) if (reg.Data.Stealth)
@@ -430,6 +442,7 @@ namespace Content.Server.Administration.Managers
{ {
Title = Loc.GetString("admin-manager-admin-data-host-title"), Title = Loc.GetString("admin-manager-admin-data-host-title"),
Flags = AdminFlagsHelper.Everything, Flags = AdminFlagsHelper.Everything,
Active = true,
}; };
return (data, null, true); return (data, null, true);
@@ -444,6 +457,12 @@ namespace Content.Server.Administration.Managers
return null; return null;
} }
if (dbData.Suspended)
{
// Suspended admins don't count.
return null;
}
var flags = AdminFlags.None; var flags = AdminFlags.None;
if (dbData.AdminRank != null) if (dbData.AdminRank != null)
@@ -466,7 +485,8 @@ namespace Content.Server.Administration.Managers
var data = new AdminData var data = new AdminData
{ {
Flags = flags Flags = flags,
Active = !dbData.Deadminned,
}; };
if (dbData.Title != null && _cfg.GetCVar(CCVars.AdminUseCustomNamesAdminRank)) if (dbData.Title != null && _cfg.GetCVar(CCVars.AdminUseCustomNamesAdminRank))

View File

@@ -76,7 +76,8 @@ namespace Content.Server.Administration.UI
Title = p.a.Title, Title = p.a.Title,
RankId = p.a.AdminRankId, RankId = p.a.AdminRankId,
UserId = new NetUserId(p.a.UserId), UserId = new NetUserId(p.a.UserId),
UserName = p.lastUserName UserName = p.lastUserName,
Suspended = p.a.Suspended,
}).ToArray(), }).ToArray(),
AdminRanks = _adminRanks.ToDictionary(a => a.Id, a => new PermissionsEuiState.AdminRankData AdminRanks = _adminRanks.ToDictionary(a => a.Id, a => new PermissionsEuiState.AdminRankData
@@ -255,6 +256,7 @@ namespace Content.Server.Administration.UI
admin.Title = ua.Title; admin.Title = ua.Title;
admin.AdminRankId = ua.RankId; admin.AdminRankId = ua.RankId;
admin.Flags = GenAdminFlagList(ua.PosFlags, ua.NegFlags); admin.Flags = GenAdminFlagList(ua.PosFlags, ua.NegFlags);
admin.Suspended = ua.Suspended;
await _db.UpdateAdminAsync(admin); await _db.UpdateAdminAsync(admin);
@@ -335,7 +337,8 @@ namespace Content.Server.Administration.UI
Flags = GenAdminFlagList(ca.PosFlags, ca.NegFlags), Flags = GenAdminFlagList(ca.PosFlags, ca.NegFlags),
AdminRankId = ca.RankId, AdminRankId = ca.RankId,
UserId = userId.UserId, UserId = userId.UserId,
Title = ca.Title Title = ca.Title,
Suspended = ca.Suspended,
}; };
await _db.AddAdminAsync(admin); await _db.AddAdminAsync(admin);

View File

@@ -751,6 +751,20 @@ namespace Content.Server.Database
existing.Flags = admin.Flags; existing.Flags = admin.Flags;
existing.Title = admin.Title; existing.Title = admin.Title;
existing.AdminRankId = admin.AdminRankId; existing.AdminRankId = admin.AdminRankId;
existing.Deadminned = admin.Deadminned;
existing.Suspended = admin.Suspended;
await db.DbContext.SaveChangesAsync(cancel);
}
public async Task UpdateAdminDeadminnedAsync(NetUserId userId, bool deadminned, CancellationToken cancel)
{
await using var db = await GetDb(cancel);
var adminRecord = db.DbContext.Admin.Where(a => a.UserId == userId);
await adminRecord.ExecuteUpdateAsync(
set => set.SetProperty(p => p.Deadminned, deadminned),
cancellationToken: cancel);
await db.DbContext.SaveChangesAsync(cancel); await db.DbContext.SaveChangesAsync(cancel);
} }

View File

@@ -217,6 +217,16 @@ namespace Content.Server.Database
Task AddAdminAsync(Admin admin, CancellationToken cancel = default); Task AddAdminAsync(Admin admin, CancellationToken cancel = default);
Task UpdateAdminAsync(Admin admin, CancellationToken cancel = default); Task UpdateAdminAsync(Admin admin, CancellationToken cancel = default);
/// <summary>
/// Update whether an admin has voluntarily deadminned.
/// </summary>
/// <remarks>
/// This does nothing if the player is not an admin.
/// </remarks>
/// <param name="userId">The user ID of the admin.</param>
/// <param name="deadminned">Whether the admin is deadminned or not.</param>
Task UpdateAdminDeadminnedAsync(NetUserId userId, bool deadminned, CancellationToken cancel = default);
Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default); Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default);
Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel = default); Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel = default);
Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel = default); Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel = default);
@@ -674,6 +684,12 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.UpdateAdminAsync(admin, cancel)); return RunDbCommand(() => _db.UpdateAdminAsync(admin, cancel));
} }
public Task UpdateAdminDeadminnedAsync(NetUserId userId, bool deadminned, CancellationToken cancel = default)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.UpdateAdminDeadminnedAsync(userId, deadminned, cancel));
}
public Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default) public Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default)
{ {
DbWriteOpsMetric.Inc(); DbWriteOpsMetric.Inc();

View File

@@ -18,6 +18,7 @@ namespace Content.Shared.Administration
public NetUserId UserId; public NetUserId UserId;
public string? UserName; public string? UserName;
public string? Title; public string? Title;
public bool Suspended;
public AdminFlags PosFlags; public AdminFlags PosFlags;
public AdminFlags NegFlags; public AdminFlags NegFlags;
public int? RankId; public int? RankId;
@@ -41,6 +42,7 @@ namespace Content.Shared.Administration
public AdminFlags PosFlags; public AdminFlags PosFlags;
public AdminFlags NegFlags; public AdminFlags NegFlags;
public int? RankId; public int? RankId;
public bool Suspended;
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]
@@ -57,6 +59,7 @@ namespace Content.Shared.Administration
public AdminFlags PosFlags; public AdminFlags PosFlags;
public AdminFlags NegFlags; public AdminFlags NegFlags;
public int? RankId; public int? RankId;
public bool Suspended;
} }

View File

@@ -32,12 +32,6 @@ public sealed class ContentPlayerData
[ViewVariables, Access(typeof(SharedMindSystem), typeof(SharedGameTicker))] [ViewVariables, Access(typeof(SharedMindSystem), typeof(SharedGameTicker))]
public EntityUid? Mind { get; set; } public EntityUid? Mind { get; set; }
/// <summary>
/// If true, the player is an admin and they explicitly de-adminned mid-game,
/// so they should not regain admin if they reconnect.
/// </summary>
public bool ExplicitlyDeadminned { get; set; }
/// <summary> /// <summary>
/// If true, the admin will not show up in adminwho except to admins with the <see cref="AdminFlags.Stealth"/> flag. /// If true, the admin will not show up in adminwho except to admins with the <see cref="AdminFlags.Stealth"/> flag.
/// </summary> /// </summary>

View File

@@ -14,6 +14,7 @@ permissions-eui-edit-admin-window-title-edit-placeholder = Custom title, leave b
permissions-eui-edit-admin-window-no-rank-button = No rank permissions-eui-edit-admin-window-no-rank-button = No rank
permissions-eui-edit-admin-rank-window-name-edit-placeholder = Rank name permissions-eui-edit-admin-rank-window-name-edit-placeholder = Rank name
permissions-eui-edit-admin-title-control-text = none permissions-eui-edit-admin-title-control-text = none
permissions-eui-edit-admin-window-suspended = Suspended?
permissions-eui-edit-no-rank-text = none permissions-eui-edit-no-rank-text = none
permissions-eui-edit-title-button = Edit permissions-eui-edit-title-button = Edit
permissions-eui-edit-admin-rank-button = Edit permissions-eui-edit-admin-rank-button = Edit