* Use new Subs.CVar helper Removes manual config OnValueChanged calls, removes need to remember to manually unsubscribe. This both reduces boilerplate and fixes many issues where subscriptions weren't removed on entity system shutdown. * Fix a bunch of warnings * More warning fixes * Use new DateTime serializer to get rid of ISerializationHooks in changelog code. * Get rid of some more ISerializationHooks for enums * And a little more * Apply suggestions from code review Co-authored-by: 0x6273 <0x40@keemail.me> --------- Co-authored-by: 0x6273 <0x40@keemail.me>
455 lines
14 KiB
C#
455 lines
14 KiB
C#
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Content.Server.Administration.Managers;
|
|
using Content.Server.Database;
|
|
using Content.Server.EUI;
|
|
using Content.Shared.Administration;
|
|
using Content.Shared.Eui;
|
|
using Robust.Server.Player;
|
|
using Robust.Shared.Network;
|
|
using DbAdminRank = Content.Server.Database.AdminRank;
|
|
using static Content.Shared.Administration.PermissionsEuiMsg;
|
|
|
|
|
|
namespace Content.Server.Administration.UI
|
|
{
|
|
public sealed class PermissionsEui : BaseEui
|
|
{
|
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
|
[Dependency] private readonly IServerDbManager _db = default!;
|
|
[Dependency] private readonly IAdminManager _adminManager = default!;
|
|
[Dependency] private readonly ILogManager _logManager = default!;
|
|
|
|
private readonly ISawmill _sawmill;
|
|
private bool _isLoading;
|
|
|
|
private readonly List<(Admin a, string? lastUserName)> _admins = new List<(Admin, string? lastUserName)>();
|
|
private readonly List<DbAdminRank> _adminRanks = new();
|
|
|
|
public PermissionsEui()
|
|
{
|
|
IoCManager.InjectDependencies(this);
|
|
_sawmill = _logManager.GetSawmill("admin.perms");
|
|
}
|
|
|
|
public override void Opened()
|
|
{
|
|
base.Opened();
|
|
|
|
StateDirty();
|
|
LoadFromDb();
|
|
_adminManager.OnPermsChanged += AdminManagerOnPermsChanged;
|
|
}
|
|
|
|
public override void Closed()
|
|
{
|
|
base.Closed();
|
|
|
|
_adminManager.OnPermsChanged -= AdminManagerOnPermsChanged;
|
|
}
|
|
|
|
private void AdminManagerOnPermsChanged(AdminPermsChangedEventArgs obj)
|
|
{
|
|
// Close UI if user loses +PERMISSIONS.
|
|
if (obj.Player == Player && !UserAdminFlagCheck(AdminFlags.Permissions))
|
|
{
|
|
Close();
|
|
}
|
|
}
|
|
|
|
public override EuiStateBase GetNewState()
|
|
{
|
|
if (_isLoading)
|
|
{
|
|
return new PermissionsEuiState
|
|
{
|
|
IsLoading = true
|
|
};
|
|
}
|
|
|
|
return new PermissionsEuiState
|
|
{
|
|
Admins = _admins.Select(p => new PermissionsEuiState.AdminData
|
|
{
|
|
PosFlags = AdminFlagsHelper.NamesToFlags(p.a.Flags.Where(f => !f.Negative).Select(f => f.Flag)),
|
|
NegFlags = AdminFlagsHelper.NamesToFlags(p.a.Flags.Where(f => f.Negative).Select(f => f.Flag)),
|
|
Title = p.a.Title,
|
|
RankId = p.a.AdminRankId,
|
|
UserId = new NetUserId(p.a.UserId),
|
|
UserName = p.lastUserName
|
|
}).ToArray(),
|
|
|
|
AdminRanks = _adminRanks.ToDictionary(a => a.Id, a => new PermissionsEuiState.AdminRankData
|
|
{
|
|
Flags = AdminFlagsHelper.NamesToFlags(a.Flags.Select(p => p.Flag)),
|
|
Name = a.Name
|
|
})
|
|
};
|
|
}
|
|
|
|
public override async void HandleMessage(EuiMessageBase msg)
|
|
{
|
|
base.HandleMessage(msg);
|
|
|
|
switch (msg)
|
|
{
|
|
case AddAdmin ca:
|
|
{
|
|
await HandleCreateAdmin(ca);
|
|
break;
|
|
}
|
|
|
|
case UpdateAdmin ua:
|
|
{
|
|
await HandleUpdateAdmin(ua);
|
|
break;
|
|
}
|
|
|
|
case RemoveAdmin ra:
|
|
{
|
|
await HandleRemoveAdmin(ra);
|
|
break;
|
|
}
|
|
|
|
case AddAdminRank ar:
|
|
{
|
|
await HandleAddAdminRank(ar);
|
|
break;
|
|
}
|
|
|
|
case UpdateAdminRank ur:
|
|
{
|
|
await HandleUpdateAdminRank(ur);
|
|
break;
|
|
}
|
|
|
|
case RemoveAdminRank ra:
|
|
{
|
|
await HandleRemoveAdminRank(ra);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!IsShutDown)
|
|
{
|
|
LoadFromDb();
|
|
}
|
|
}
|
|
|
|
private async Task HandleRemoveAdminRank(RemoveAdminRank rr)
|
|
{
|
|
var rank = await _db.GetAdminRankAsync(rr.Id);
|
|
if (rank == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!CanTouchRank(rank))
|
|
{
|
|
_sawmill.Warning($"{Player} tried to remove higher-ranked admin rank {rank.Name}");
|
|
return;
|
|
}
|
|
|
|
await _db.RemoveAdminRankAsync(rr.Id);
|
|
|
|
_adminManager.ReloadAdminsWithRank(rr.Id);
|
|
}
|
|
|
|
private async Task HandleUpdateAdminRank(UpdateAdminRank ur)
|
|
{
|
|
var rank = await _db.GetAdminRankAsync(ur.Id);
|
|
if (rank == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!CanTouchRank(rank))
|
|
{
|
|
_sawmill.Warning($"{Player} tried to update higher-ranked admin rank {rank.Name}");
|
|
return;
|
|
}
|
|
|
|
if (!UserAdminFlagCheck(ur.Flags))
|
|
{
|
|
_sawmill.Warning($"{Player} tried to give a rank permissions above their authorization.");
|
|
return;
|
|
}
|
|
|
|
rank.Flags = GenRankFlagList(ur.Flags);
|
|
rank.Name = ur.Name;
|
|
|
|
await _db.UpdateAdminRankAsync(rank);
|
|
|
|
var flagText = string.Join(' ', AdminFlagsHelper.FlagsToNames(ur.Flags).Select(f => $"+{f}"));
|
|
_sawmill.Info($"{Player} updated admin rank {rank.Name}/{flagText}.");
|
|
|
|
_adminManager.ReloadAdminsWithRank(ur.Id);
|
|
}
|
|
|
|
private async Task HandleAddAdminRank(AddAdminRank ar)
|
|
{
|
|
if (!UserAdminFlagCheck(ar.Flags))
|
|
{
|
|
_sawmill.Warning($"{Player} tried to give a rank permissions above their authorization.");
|
|
return;
|
|
}
|
|
|
|
var rank = new DbAdminRank
|
|
{
|
|
Name = ar.Name,
|
|
Flags = GenRankFlagList(ar.Flags)
|
|
};
|
|
|
|
await _db.AddAdminRankAsync(rank);
|
|
|
|
var flagText = string.Join(' ', AdminFlagsHelper.FlagsToNames(ar.Flags).Select(f => $"+{f}"));
|
|
_sawmill.Info($"{Player} added admin rank {rank.Name}/{flagText}.");
|
|
}
|
|
|
|
private async Task HandleRemoveAdmin(RemoveAdmin ra)
|
|
{
|
|
var admin = await _db.GetAdminDataForAsync(ra.UserId);
|
|
if (admin == null)
|
|
{
|
|
// Doesn't exist.
|
|
return;
|
|
}
|
|
|
|
if (!CanTouchAdmin(admin))
|
|
{
|
|
_sawmill.Warning($"{Player} tried to remove higher-ranked admin {ra.UserId.ToString()}");
|
|
return;
|
|
}
|
|
|
|
await _db.RemoveAdminAsync(ra.UserId);
|
|
|
|
var record = await _db.GetPlayerRecordByUserId(ra.UserId);
|
|
_sawmill.Info($"{Player} removed admin {record?.LastSeenUserName ?? ra.UserId.ToString()}");
|
|
|
|
if (_playerManager.TryGetSessionById(ra.UserId, out var player))
|
|
{
|
|
_adminManager.ReloadAdmin(player);
|
|
}
|
|
}
|
|
|
|
private async Task HandleUpdateAdmin(UpdateAdmin ua)
|
|
{
|
|
if (!CheckCreatePerms(ua.PosFlags, ua.NegFlags))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var admin = await _db.GetAdminDataForAsync(ua.UserId);
|
|
if (admin == null)
|
|
{
|
|
// Was removed in the mean time I guess?
|
|
return;
|
|
}
|
|
|
|
if (!CanTouchAdmin(admin))
|
|
{
|
|
_sawmill.Warning($"{Player} tried to modify higher-ranked admin {ua.UserId.ToString()}");
|
|
return;
|
|
}
|
|
|
|
admin.Title = ua.Title;
|
|
admin.AdminRankId = ua.RankId;
|
|
admin.Flags = GenAdminFlagList(ua.PosFlags, ua.NegFlags);
|
|
|
|
await _db.UpdateAdminAsync(admin);
|
|
|
|
var playerRecord = await _db.GetPlayerRecordByUserId(ua.UserId);
|
|
var (bad, rankName) = await FetchAndCheckRank(ua.RankId);
|
|
if (bad)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var name = playerRecord?.LastSeenUserName ?? ua.UserId.ToString();
|
|
var title = ua.Title ?? "<no title>";
|
|
var flags = AdminFlagsHelper.PosNegFlagsText(ua.PosFlags, ua.NegFlags);
|
|
|
|
_sawmill.Info($"{Player} updated admin {name} to {title}/{rankName}/{flags}");
|
|
|
|
if (_playerManager.TryGetSessionById(ua.UserId, out var player))
|
|
{
|
|
_adminManager.ReloadAdmin(player);
|
|
}
|
|
}
|
|
|
|
private async Task HandleCreateAdmin(AddAdmin ca)
|
|
{
|
|
if (!CheckCreatePerms(ca.PosFlags, ca.NegFlags))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string name;
|
|
NetUserId userId;
|
|
if (Guid.TryParse(ca.UserNameOrId, out var guid))
|
|
{
|
|
userId = new NetUserId(guid);
|
|
var playerRecord = await _db.GetPlayerRecordByUserId(userId);
|
|
if (playerRecord == null)
|
|
{
|
|
name = userId.ToString();
|
|
}
|
|
else
|
|
{
|
|
name = playerRecord.LastSeenUserName;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Username entered, resolve user ID from DB.
|
|
var dbPlayer = await _db.GetPlayerRecordByUserName(ca.UserNameOrId);
|
|
if (dbPlayer == null)
|
|
{
|
|
// username not in DB.
|
|
// TODO: Notify user.
|
|
_sawmill.Warning($"{Player} tried to add admin with unknown username {ca.UserNameOrId}.");
|
|
return;
|
|
}
|
|
|
|
userId = dbPlayer.UserId;
|
|
name = ca.UserNameOrId;
|
|
}
|
|
|
|
var existing = await _db.GetAdminDataForAsync(userId);
|
|
if (existing != null)
|
|
{
|
|
// Already exists.
|
|
return;
|
|
}
|
|
|
|
var (bad, rankName) = await FetchAndCheckRank(ca.RankId);
|
|
if (bad)
|
|
{
|
|
return;
|
|
}
|
|
|
|
rankName ??= "<no rank>";
|
|
|
|
var admin = new Admin
|
|
{
|
|
Flags = GenAdminFlagList(ca.PosFlags, ca.NegFlags),
|
|
AdminRankId = ca.RankId,
|
|
UserId = userId.UserId,
|
|
Title = ca.Title
|
|
};
|
|
|
|
await _db.AddAdminAsync(admin);
|
|
|
|
var title = ca.Title ?? "<no title>";
|
|
var flags = AdminFlagsHelper.PosNegFlagsText(ca.PosFlags, ca.NegFlags);
|
|
|
|
_sawmill.Info($"{Player} added admin {name} as {title}/{rankName}/{flags}");
|
|
|
|
if (_playerManager.TryGetSessionById(userId, out var player))
|
|
{
|
|
_adminManager.ReloadAdmin(player);
|
|
}
|
|
}
|
|
|
|
// ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local
|
|
private bool CheckCreatePerms(AdminFlags posFlags, AdminFlags negFlags)
|
|
{
|
|
if ((posFlags & negFlags) != 0)
|
|
{
|
|
// Can't have overlapping pos and neg flags.
|
|
// Just deny the entire message.
|
|
return false;
|
|
}
|
|
|
|
if (!UserAdminFlagCheck(posFlags))
|
|
{
|
|
// Can't create an admin with higher perms than yourself, obviously.
|
|
_sawmill.Warning($"{Player} tried to grant admin powers above their authorization.");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private async Task<(bool bad, string?)> FetchAndCheckRank(int? rankId)
|
|
{
|
|
string? ret = null;
|
|
if (rankId is { } r)
|
|
{
|
|
var rank = await _db.GetAdminRankAsync(r);
|
|
if (rank == null)
|
|
{
|
|
// Tried to set to nonexistent rank.
|
|
_sawmill.Warning($"{Player} tried to assign nonexistent admin rank.");
|
|
return (true, null);
|
|
}
|
|
|
|
ret = rank.Name;
|
|
|
|
var rankFlags = AdminFlagsHelper.NamesToFlags(rank.Flags.Select(p => p.Flag));
|
|
if (!UserAdminFlagCheck(rankFlags))
|
|
{
|
|
// Can't assign a rank with flags you don't have yourself.
|
|
_sawmill.Warning($"{Player} tried to assign admin rank above their authorization.");
|
|
return (true, null);
|
|
}
|
|
}
|
|
|
|
return (false, ret);
|
|
}
|
|
|
|
private async void LoadFromDb()
|
|
{
|
|
StateDirty();
|
|
_isLoading = true;
|
|
var (admins, ranks) = await _db.GetAllAdminAndRanksAsync();
|
|
|
|
_admins.Clear();
|
|
_admins.AddRange(admins);
|
|
_adminRanks.Clear();
|
|
_adminRanks.AddRange(ranks);
|
|
|
|
_isLoading = false;
|
|
StateDirty();
|
|
}
|
|
|
|
private static List<AdminFlag> GenAdminFlagList(AdminFlags posFlags, AdminFlags negFlags)
|
|
{
|
|
var posFlagList = AdminFlagsHelper.FlagsToNames(posFlags);
|
|
var negFlagList = AdminFlagsHelper.FlagsToNames(negFlags);
|
|
|
|
return posFlagList
|
|
.Select(f => new AdminFlag {Negative = false, Flag = f})
|
|
.Concat(negFlagList.Select(f => new AdminFlag {Negative = true, Flag = f}))
|
|
.ToList();
|
|
}
|
|
|
|
private static List<AdminRankFlag> GenRankFlagList(AdminFlags flags)
|
|
{
|
|
return AdminFlagsHelper.FlagsToNames(flags).Select(f => new AdminRankFlag {Flag = f}).ToList();
|
|
}
|
|
|
|
private bool UserAdminFlagCheck(AdminFlags flags)
|
|
{
|
|
return _adminManager.HasAdminFlag(Player, flags);
|
|
}
|
|
|
|
private bool CanTouchAdmin(Admin admin)
|
|
{
|
|
var posFlags = AdminFlagsHelper.NamesToFlags(admin.Flags.Where(f => !f.Negative).Select(f => f.Flag));
|
|
var rankFlags = AdminFlagsHelper.NamesToFlags(
|
|
admin.AdminRank?.Flags.Select(f => f.Flag) ?? Array.Empty<string>());
|
|
|
|
var totalFlags = posFlags | rankFlags;
|
|
return UserAdminFlagCheck(totalFlags);
|
|
}
|
|
|
|
private bool CanTouchRank(DbAdminRank rank)
|
|
{
|
|
var rankFlags = AdminFlagsHelper.NamesToFlags(rank.Flags.Select(f => f.Flag));
|
|
|
|
return UserAdminFlagCheck(rankFlags);
|
|
}
|
|
}
|
|
}
|