Permissions panel.

This commit is contained in:
Pieter-Jan Briers
2020-11-10 16:50:28 +01:00
parent c9236d88ac
commit e39ddd4802
41 changed files with 3355 additions and 35 deletions

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Threading.Tasks;
using Content.Server.Database;
using Content.Server.Interfaces.Chat;
using Content.Server.Players;
@@ -40,6 +41,8 @@ namespace Content.Server.Administration
private readonly Dictionary<IPlayerSession, AdminReg> _admins = new Dictionary<IPlayerSession, AdminReg>();
public event Action<AdminPermsChangedEventArgs>? OnPermsChanged;
public IEnumerable<IPlayerSession> ActiveAdmins => _admins
.Where(p => p.Value.Data.Active)
.Select(p => p.Key);
@@ -78,6 +81,7 @@ namespace Content.Server.Administration
plyData.ExplicitlyDeadminned = true;
reg.Data.Active = false;
SendPermsChangedEvent(session);
UpdateAdminStatus(session);
}
@@ -96,9 +100,70 @@ namespace Content.Server.Administration
_chat.SendAdminAnnouncement(Loc.GetString("{0} re-adminned themselves.", session.Name));
SendPermsChangedEvent(session);
UpdateAdminStatus(session);
}
public async void ReloadAdmin(IPlayerSession player)
{
var data = await LoadAdminData(player);
var curAdmin = _admins.GetValueOrDefault(player);
if (data == null && curAdmin == null)
{
// Wasn't admin before or after.
return;
}
if (data == null)
{
// No longer admin.
_admins.Remove(player);
_chat.DispatchServerMessage(player, Loc.GetString("You are no longer an admin."));
}
else
{
var (aData, rankId, special) = data.Value;
if (curAdmin == null)
{
// Now an admin.
var reg = new AdminReg(player, aData)
{
IsSpecialLogin = special,
RankId = rankId
};
_admins.Add(player, reg);
_chat.DispatchServerMessage(player, Loc.GetString("You are now an admin."));
}
else
{
// Perms changed.
curAdmin.IsSpecialLogin = special;
curAdmin.RankId = rankId;
curAdmin.Data = aData;
}
if (!player.ContentData()!.ExplicitlyDeadminned)
{
aData.Active = true;
_chat.DispatchServerMessage(player, Loc.GetString("Your admin permissions have been updated."));
}
}
SendPermsChangedEvent(player);
UpdateAdminStatus(player);
}
public void ReloadAdminsWithRank(int rankId)
{
foreach (var dat in _admins.Values.Where(p => p.RankId == rankId).ToArray())
{
ReloadAdmin(dat.Session);
}
}
public void Initialize()
{
_netMgr.RegisterNetMessage<MsgUpdateAdminStatus>(MsgUpdateAdminStatus.NAME);
@@ -143,7 +208,7 @@ namespace Content.Server.Administration
{
if (!_adminCommands.TryGetValue(cmd, out var exFlags))
{
_adminCommands.Add(cmd, new []{flags});
_adminCommands.Add(cmd, new[] {flags});
}
else
{
@@ -213,7 +278,39 @@ namespace Content.Server.Administration
private async void LoginAdminMaybe(IPlayerSession session)
{
AdminReg reg;
var adminDat = await LoadAdminData(session);
if (adminDat == null)
{
// Not an admin.
return;
}
var (dat, rankId, specialLogin) = adminDat.Value;
var reg = new AdminReg(session, dat)
{
IsSpecialLogin = specialLogin,
RankId = rankId
};
_admins.Add(session, reg);
if (!session.ContentData()!.ExplicitlyDeadminned)
{
reg.Data.Active = true;
if (_cfg.GetCVar(CCVars.AdminAnnounceLogin))
{
_chat.SendAdminAnnouncement(Loc.GetString("Admin login: {0}", session.Name));
}
SendPermsChangedEvent(session);
}
UpdateAdminStatus(session);
}
private async Task<(AdminData dat, int? rankId, bool specialLogin)?> LoadAdminData(IPlayerSession session)
{
if (IsLocal(session) && _cfg.GetCVar(CCVars.ConsoleLoginLocal))
{
var data = new AdminData
@@ -222,10 +319,7 @@ namespace Content.Server.Administration
Flags = AdminFlagsExt.Everything,
};
reg = new AdminReg(session, data)
{
IsSpecialLogin = true,
};
return (data, null, true);
}
else
{
@@ -234,7 +328,7 @@ namespace Content.Server.Administration
if (dbData == null)
{
// Not an admin!
return;
return null;
}
var flags = AdminFlags.None;
@@ -271,22 +365,8 @@ namespace Content.Server.Administration
data.Title = dbData.AdminRank.Name;
}
reg = new AdminReg(session, data);
return (data, dbData.AdminRankId, false);
}
_admins.Add(session, reg);
if (!session.ContentData()!.ExplicitlyDeadminned)
{
reg.Data.Active = true;
if (_cfg.GetCVar(CCVars.AdminAnnounceLogin))
{
_chat.SendAdminAnnouncement(Loc.GetString("Admin login: {0}", session.Name));
}
}
UpdateAdminStatus(session);
}
private static bool IsLocal(IPlayerSession player)
@@ -372,14 +452,20 @@ namespace Content.Server.Administration
return GetAdminData(session)?.CanAdminMenu() ?? false;
}
private void SendPermsChangedEvent(IPlayerSession session)
{
var flags = GetAdminData(session)?.Flags;
OnPermsChanged?.Invoke(new AdminPermsChangedEventArgs(session, flags));
}
private sealed class AdminReg
{
public IPlayerSession Session;
public AdminData Data;
public int? RankId;
// Such as console.loginlocal
// Means that stuff like permissions editing is blocked.
public bool IsSpecialLogin;
public AdminReg(IPlayerSession session, AdminData data)

View File

@@ -0,0 +1,33 @@
using System;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Player;
namespace Content.Server.Administration
{
/// <summary>
/// Sealed when the permissions of an admin on the server change.
/// </summary>
public sealed class AdminPermsChangedEventArgs : EventArgs
{
public AdminPermsChangedEventArgs(IPlayerSession player, AdminFlags? flags)
{
Player = player;
Flags = flags;
}
/// <summary>
/// The player that had their admin permissions changed.
/// </summary>
public IPlayerSession Player { get; }
/// <summary>
/// The admin flags of the player. Null if the player is no longer an admin.
/// </summary>
public AdminFlags? Flags { get; }
/// <summary>
/// Whether the player is now an admin.
/// </summary>
public bool IsAdmin => Flags.HasValue;
}
}

View File

@@ -0,0 +1,31 @@
using Content.Server.Eui;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.IoC;
#nullable enable
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Permissions)]
public sealed class OpenPermissionsCommand : IClientCommand
{
public string Command => "permissions";
public string Description => "Opens the admin permissions panel.";
public string Help => "Usage: permissions";
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
if (player == null)
{
shell.SendText(player, "This does not work from the server console.");
return;
}
var eui = IoCManager.Resolve<EuiManager>();
var ui = new PermissionsEui();
eui.OpenEui(ui, player);
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Player;
@@ -11,6 +12,11 @@ namespace Content.Server.Administration
/// </summary>
public interface IAdminManager
{
/// <summary>
/// Fired when the permissions of an admin on the server changed.
/// </summary>
event Action<AdminPermsChangedEventArgs> OnPermsChanged;
/// <summary>
/// Gets all active admins currently on the server.
/// </summary>
@@ -29,6 +35,16 @@ namespace Content.Server.Administration
/// <returns><see langword="null" /> if the player is not an admin.</returns>
AdminData? GetAdminData(IPlayerSession session, bool includeDeAdmin = false);
/// <summary>
/// See if a player has an admin flag.
/// </summary>
/// <returns>True if the player is and admin and has the specified flags.</returns>
bool HasAdminFlag(IPlayerSession player, AdminFlags flag)
{
var data = GetAdminData(player);
return data != null && data.HasFlag(flag);
}
/// <summary>
/// De-admins an admin temporarily so they are effectively a normal player.
/// </summary>
@@ -42,6 +58,19 @@ namespace Content.Server.Administration
/// </summary>
void ReAdmin(IPlayerSession session);
/// <summary>
/// Re-loads the permissions of an player in case their admin data changed DB-side.
/// </summary>
/// <seealso cref="ReloadAdminsWithRank"/>
void ReloadAdmin(IPlayerSession player);
/// <summary>
/// Reloads admin permissions for all admins with a certain rank.
/// </summary>
/// <param name="rankId">The database ID of the rank.</param>
/// <seealso cref="ReloadAdmin"/>
void ReloadAdminsWithRank(int rankId);
void Initialize();
}
}

View File

@@ -0,0 +1,460 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.Database;
using Content.Server.Eui;
using Content.Shared.Administration;
using Content.Shared.Eui;
using Robust.Server.Interfaces.Player;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using DbAdminRank = Content.Server.Database.AdminRank;
using static Content.Shared.Administration.PermissionsEuiMsg;
#nullable enable
namespace Content.Server.Administration
{
public sealed class PermissionsEui : BaseEui
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
private bool _isLoading;
private readonly List<(Admin a, string? lastUserName)> _admins = new List<(Admin, string? lastUserName)>();
private readonly List<DbAdminRank> _adminRanks = new List<DbAdminRank>();
public PermissionsEui()
{
IoCManager.InjectDependencies(this);
}
public override void Opened()
{
base.Opened();
StateDirty();
LoadFromDb();
_adminManager.OnPermsChanged += AdminManagerOnOnPermsChanged;
}
public override void Closed()
{
base.Closed();
_adminManager.OnPermsChanged -= AdminManagerOnOnPermsChanged;
}
private void AdminManagerOnOnPermsChanged(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 = AdminFlagsExt.NamesToFlags(p.a.Flags.Where(f => !f.Negative).Select(f => f.Flag)),
NegFlags = AdminFlagsExt.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 = AdminFlagsExt.NamesToFlags(a.Flags.Select(p => p.Flag)),
Name = a.Name
})
};
}
public override async void HandleMessage(EuiMessageBase msg)
{
switch (msg)
{
case Close _:
{
Close();
break;
}
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))
{
Logger.WarningS("admin.perms", $"{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))
{
Logger.WarningS("admin.perms", $"{Player} tried to update higher-ranked admin rank {rank.Name}");
return;
}
if (!UserAdminFlagCheck(ur.Flags))
{
Logger.WarningS("admin.perms", $"{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(' ', AdminFlagsExt.FlagsToNames(ur.Flags).Select(f => $"+{f}"));
Logger.InfoS("admin.perms", $"{Player} updated admin rank {rank.Name}/{flagText}.");
_adminManager.ReloadAdminsWithRank(ur.Id);
}
private async Task HandleAddAdminRank(AddAdminRank ar)
{
if (!UserAdminFlagCheck(ar.Flags))
{
Logger.WarningS("admin.perms", $"{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(' ', AdminFlagsExt.FlagsToNames(ar.Flags).Select(f => $"+{f}"));
Logger.InfoS("admin.perms", $"{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))
{
Logger.WarningS("admin.perms", $"{Player} tried to remove higher-ranked admin {ra.UserId.ToString()}");
return;
}
await _db.RemoveAdminAsync(ra.UserId);
var record = await _db.GetPlayerRecordByUserId(ra.UserId);
Logger.InfoS("admin.perms", $"{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))
{
Logger.WarningS("admin.perms", $"{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 = AdminFlagsExt.PosNegFlagsText(ua.PosFlags, ua.NegFlags);
Logger.InfoS("admin.perms", $"{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.
Logger.WarningS("admin.perms",
$"{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 = AdminFlagsExt.PosNegFlagsText(ca.PosFlags, ca.NegFlags);
Logger.InfoS("admin.perms", $"{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.
Logger.WarningS("admin.perms", $"{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.
Logger.WarningS("admin.perms", $"{Player} tried to assign nonexistent admin rank.");
return (true, null);
}
ret = rank.Name;
var rankFlags = AdminFlagsExt.NamesToFlags(rank.Flags.Select(p => p.Flag));
if (!UserAdminFlagCheck(rankFlags))
{
// Can't assign a rank with flags you don't have yourself.
Logger.WarningS("admin.perms", $"{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 = AdminFlagsExt.FlagsToNames(posFlags);
var negFlagList = AdminFlagsExt.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 AdminFlagsExt.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 = AdminFlagsExt.NamesToFlags(admin.Flags.Where(f => !f.Negative).Select(f => f.Flag));
var rankFlags = AdminFlagsExt.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 = AdminFlagsExt.NamesToFlags(rank.Flags.Select(f => f.Flag));
return UserAdminFlagCheck(rankFlags);
}
}
}