ConGroups are gone. Long live admin flags in content.

This commit is contained in:
Pieter-Jan Briers
2020-10-30 16:06:48 +01:00
parent f04818437d
commit ad58a056d7
62 changed files with 2673 additions and 289 deletions

View File

@@ -0,0 +1,27 @@
using System;
using Content.Shared.Administration;
using JetBrains.Annotations;
using Robust.Server.Interfaces.Console;
namespace Content.Server.Administration
{
/// <summary>
/// Specifies that a command can only be executed by an admin with the specified flags.
/// </summary>
/// <remarks>
/// If this attribute is used multiple times, either attribute's flag sets can be used to get access.
/// </remarks>
/// <seealso cref="AnyCommandAttribute"/>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
[BaseTypeRequired(typeof(IClientCommand))]
[MeansImplicitUse]
public sealed class AdminCommandAttribute : Attribute
{
public AdminCommandAttribute(AdminFlags flags)
{
Flags = flags;
}
public AdminFlags Flags { get; }
}
}

View File

@@ -0,0 +1,377 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using Content.Server.Database;
using Content.Server.Players;
using Content.Shared;
using Content.Shared.Administration;
using Content.Shared.Network.NetMessages;
using Robust.Server.Console;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.Interfaces.Configuration;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Interfaces.Resources;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
#nullable enable
namespace Content.Server.Administration
{
public sealed class AdminManager : IAdminManager, IPostInjectInit, IConGroupControllerImplementation
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IServerDbManager _dbManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IServerNetManager _netMgr = default!;
[Dependency] private readonly IConGroupController _conGroup = default!;
[Dependency] private readonly IResourceManager _res = default!;
[Dependency] private readonly IConsoleShell _consoleShell = default!;
private readonly Dictionary<IPlayerSession, AdminReg> _admins = new Dictionary<IPlayerSession, AdminReg>();
public IEnumerable<IPlayerSession> ActiveAdmins => _admins
.Where(p => p.Value.Data.Active)
.Select(p => p.Key);
// If a command isn't in this list it's server-console only.
// if a command is in but the flags value is null it's available to everybody.
private readonly HashSet<string> _anyCommands = new HashSet<string>();
private readonly Dictionary<string, AdminFlags[]> _adminCommands = new Dictionary<string, AdminFlags[]>();
public AdminData? GetAdminData(IPlayerSession session, bool includeDeAdmin = false)
{
if (_admins.TryGetValue(session, out var reg) && (reg.Data.Active || includeDeAdmin))
{
return reg.Data;
}
return null;
}
public void DeAdmin(IPlayerSession session)
{
if (!_admins.TryGetValue(session, out var reg))
{
throw new ArgumentException($"Player {session} is not an admin");
}
if (!reg.Data.Active)
{
return;
}
var plyData = session.ContentData()!;
plyData.ExplicitlyDeadminned = true;
reg.Data.Active = false;
// TODO: Send messages to all admins.
UpdateAdminStatus(session);
}
public void ReAdmin(IPlayerSession session)
{
if (!_admins.TryGetValue(session, out var reg))
{
throw new ArgumentException($"Player {session} is not an admin");
}
var plyData = session.ContentData()!;
plyData.ExplicitlyDeadminned = true;
reg.Data.Active = true;
// TODO: Send messages to all admins.
UpdateAdminStatus(session);
}
public void Initialize()
{
_netMgr.RegisterNetMessage<MsgUpdateAdminStatus>(MsgUpdateAdminStatus.NAME);
// Cache permissions for loaded console commands with the requisite attributes.
foreach (var (cmdName, cmd) in _consoleShell.AvailableCommands)
{
var (isAvail, flagsReq) = GetRequiredFlag(cmd);
if (!isAvail)
{
continue;
}
if (flagsReq.Length != 0)
{
_adminCommands.Add(cmdName, flagsReq);
}
else
{
_anyCommands.Add(cmdName);
}
}
// Load flags for engine commands, since those don't have the attributes.
if (_res.TryContentFileRead(new ResourcePath("/engineCommandPerms.yml"), out var fs))
{
using var reader = new StreamReader(fs, EncodingHelpers.UTF8);
var yStream = new YamlStream();
yStream.Load(reader);
var root = (YamlSequenceNode) yStream.Documents[0].RootNode;
foreach (var child in root)
{
var map = (YamlMappingNode) child;
var commands = map.GetNode<YamlSequenceNode>("Commands").Select(p => p.AsString());
if (map.TryGetNode("Flags", out var flagsNode))
{
var flagNames = flagsNode.AsString().Split(",", StringSplitOptions.RemoveEmptyEntries);
var flags = AdminFlagsExt.NamesToFlags(flagNames);
foreach (var cmd in commands)
{
if (!_adminCommands.TryGetValue(cmd, out var exFlags))
{
_adminCommands.Add(cmd, new []{flags});
}
else
{
var newArr = new AdminFlags[exFlags.Length + 1];
exFlags.CopyTo(newArr, 0);
exFlags[^1] = flags;
_adminCommands[cmd] = newArr;
}
}
}
else
{
_anyCommands.UnionWith(commands);
}
}
}
}
void IPostInjectInit.PostInject()
{
_playerManager.PlayerStatusChanged += PlayerStatusChanged;
_conGroup.Implementation = this;
}
// NOTE: Also sends commands list for non admins..
private void UpdateAdminStatus(IPlayerSession session)
{
var msg = _netMgr.CreateNetMessage<MsgUpdateAdminStatus>();
var commands = new List<string>(_anyCommands);
if (_admins.TryGetValue(session, out var adminData))
{
msg.Admin = adminData.Data;
commands.AddRange(_adminCommands
.Where(p => p.Value.Any(f => adminData.Data.HasFlag(f)))
.Select(p => p.Key));
}
msg.AvailableCommands = commands.ToArray();
_netMgr.ServerSendMessage(msg, session.ConnectedClient);
}
private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus == SessionStatus.Connected)
{
// Run this so that available commands list gets sent.
UpdateAdminStatus(e.Session);
}
else if (e.NewStatus == SessionStatus.InGame)
{
LoginAdminMaybe(e.Session);
}
else if (e.NewStatus == SessionStatus.Disconnected)
{
_admins.Remove(e.Session);
}
}
private async void LoginAdminMaybe(IPlayerSession session)
{
AdminReg reg;
if (IsLocal(session) && _cfg.GetCVar(CCVars.ConsoleLoginLocal))
{
var data = new AdminData
{
Title = Loc.GetString("Host"),
Flags = AdminFlagsExt.Everything,
};
reg = new AdminReg(session, data)
{
IsSpecialLogin = true,
};
}
else
{
var dbData = await _dbManager.GetAdminDataForAsync(session.UserId);
if (dbData == null)
{
// Not an admin!
return;
}
var flags = AdminFlags.None;
if (dbData.AdminRank != null)
{
flags = AdminFlagsExt.NamesToFlags(dbData.AdminRank.Flags.Select(p => p.Flag));
}
foreach (var dbFlag in dbData.Flags)
{
var flag = AdminFlagsExt.NameToFlag(dbFlag.Flag);
if (dbFlag.Negative)
{
flags &= ~flag;
}
else
{
flags |= flag;
}
}
var data = new AdminData
{
Flags = flags
};
if (dbData.Title != null)
{
data.Title = dbData.Title;
}
else if (dbData.AdminRank != null)
{
data.Title = dbData.AdminRank.Name;
}
reg = new AdminReg(session, data);
}
_admins.Add(session, reg);
if (!session.ContentData()!.ExplicitlyDeadminned)
{
reg.Data.Active = true;
}
UpdateAdminStatus(session);
}
private static bool IsLocal(IPlayerSession player)
{
var ep = player.ConnectedClient.RemoteEndPoint;
var addr = ep.Address;
if (addr.IsIPv4MappedToIPv6)
{
addr = addr.MapToIPv4();
}
return Equals(addr, IPAddress.Loopback) || Equals(addr, IPAddress.IPv6Loopback);
}
public bool CanCommand(IPlayerSession session, string cmdName)
{
if (_anyCommands.Contains(cmdName))
{
// Anybody can use this command.
return true;
}
if (!_adminCommands.TryGetValue(cmdName, out var flagsReq))
{
// Server-console only.
return false;
}
var data = GetAdminData(session);
if (data == null)
{
// Player isn't an admin.
return false;
}
foreach (var flagReq in flagsReq)
{
if (data.HasFlag(flagReq))
{
return true;
}
}
return false;
}
private static (bool isAvail, AdminFlags[] flagsReq) GetRequiredFlag(IClientCommand cmd)
{
var type = cmd.GetType();
if (Attribute.IsDefined(type, typeof(AnyCommandAttribute)))
{
// Available to everybody.
return (true, Array.Empty<AdminFlags>());
}
var attribs = type.GetCustomAttributes(typeof(AdminCommandAttribute))
.Cast<AdminCommandAttribute>()
.Select(p => p.Flags)
.ToArray();
// If attribs.length == 0 then no access attribute is specified,
// and this is a server-only command.
return (attribs.Length != 0, attribs);
}
public bool CanViewVar(IPlayerSession session)
{
return GetAdminData(session)?.CanViewVar() ?? false;
}
public bool CanAdminPlace(IPlayerSession session)
{
return GetAdminData(session)?.CanAdminPlace() ?? false;
}
public bool CanScript(IPlayerSession session)
{
return GetAdminData(session)?.CanScript() ?? false;
}
public bool CanAdminMenu(IPlayerSession session)
{
return GetAdminData(session)?.CanAdminMenu() ?? false;
}
private sealed class AdminReg
{
public IPlayerSession Session;
public AdminData Data;
// Such as console.loginlocal
// Means that stuff like permissions editing is blocked.
public bool IsSpecialLogin;
public AdminReg(IPlayerSession session, AdminData data)
{
Data = data;
Session = session;
}
}
}
}

View File

@@ -0,0 +1,18 @@
using Content.Shared.Administration;
#nullable enable
namespace Content.Server.Administration
{
public sealed class AdminRank
{
public AdminRank(string name, AdminFlags flags)
{
Name = name;
Flags = flags;
}
public string Name { get; }
public AdminFlags Flags { get; }
}
}

View File

@@ -0,0 +1,18 @@
using System;
using JetBrains.Annotations;
using Robust.Server.Interfaces.Console;
namespace Content.Server.Administration
{
/// <summary>
/// Specifies that a command can be executed by any player.
/// </summary>
/// <seealso cref="AdminCommandAttribute"/>
[AttributeUsage(AttributeTargets.Class)]
[BaseTypeRequired(typeof(IClientCommand))]
[MeansImplicitUse]
public sealed class AnyCommandAttribute : Attribute
{
}
}

View File

@@ -1,12 +1,14 @@
using Content.Server.GameObjects.Components.Observer;
using Content.Server.Players;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.Administration
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Admin)]
public class AGhost : IClientCommand
{
public string Command => "aghost";

View File

@@ -1,6 +1,6 @@
using System;
using System.Net;
using Content.Server.Database;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.IoC;
@@ -8,8 +8,9 @@ using Robust.Shared.Network;
#nullable enable
namespace Content.Server.Administration
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Ban)]
public sealed class BanCommand : IClientCommand
{
public string Command => "ban";

View File

@@ -1,6 +1,7 @@
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Observer;
using Content.Server.Players;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects;
@@ -8,8 +9,9 @@ using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
namespace Content.Server.Administration
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Admin)]
class ControlMob : IClientCommand
{
public string Command => "controlmob";

View File

@@ -0,0 +1,31 @@
using Content.Shared.Administration;
using JetBrains.Annotations;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.IoC;
#nullable enable
namespace Content.Server.Administration
{
[UsedImplicitly]
[AdminCommand(AdminFlags.None)]
public class DeAdminCommand : IClientCommand
{
public string Command => "deadmin";
public string Description => "Temporarily de-admins you so you can experience the round as a normal player.";
public string Help => "Usage: deadmin\nUse readmin to re-admin after using this.";
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
if (player == null)
{
shell.SendText(player, "You cannot use this command from the server console.");
return;
}
var mgr = IoCManager.Resolve<IAdminManager>();
mgr.DeAdmin(player);
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects;
@@ -7,8 +8,9 @@ using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
namespace Content.Server.Administration
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Admin)]
class DeleteEntitiesWithComponent : IClientCommand
{
public string Command => "deleteewc";

View File

@@ -1,12 +1,14 @@
#nullable enable
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.Administration
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Admin)]
public class DeleteEntitiesWithId : IClientCommand
{
public string Command => "deleteewi";

View File

@@ -0,0 +1,35 @@
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.IoC;
#nullable enable
namespace Content.Server.Administration
{
[AnyCommand]
public class ReAdminCommand : IClientCommand
{
public string Command => "readmin";
public string Description => "Re-admins you if you previously de-adminned.";
public string Help => "Usage: readmin";
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
if (player == null)
{
shell.SendText(player, "You cannot use this command from the server console.");
return;
}
var mgr = IoCManager.Resolve<IAdminManager>();
if (mgr.GetAdminData(player, includeDeAdmin: true) == null)
{
shell.SendText(player, "You're not an admin.");
return;
}
mgr.ReAdmin(player);
}
}
}

View File

@@ -1,12 +1,14 @@
#nullable enable
using Content.Server.GameTicking;
using Content.Server.Interfaces.GameTicking;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.IoC;
namespace Content.Server.Administration
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Server)]
public class ReadyAll : IClientCommand
{
public string Command => "readyall";

View File

@@ -1,4 +1,5 @@
using Content.Server.GlobalVerbs;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects;
@@ -6,8 +7,9 @@ using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
namespace Content.Server.Administration
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Admin)]
class Rejuvenate : IClientCommand
{
public string Command => "rejuvenate";

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.Markers;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Enums;
@@ -10,8 +11,9 @@ using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Map;
namespace Content.Server.Administration
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Admin)]
public class WarpCommand : IClientCommand
{
public string Command => "warp";

View File

@@ -0,0 +1,47 @@
using System.Collections.Generic;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Player;
#nullable enable
namespace Content.Server.Administration
{
/// <summary>
/// Manages server administrators and their permission flags.
/// </summary>
public interface IAdminManager
{
/// <summary>
/// Gets all active admins currently on the server.
/// </summary>
/// <remarks>
/// This does not include admins that are de-adminned.
/// </remarks>
IEnumerable<IPlayerSession> ActiveAdmins { get; }
/// <summary>
/// Gets the admin data for a player, if they are an admin.
/// </summary>
/// <param name="session">The player to get admin data for.</param>
/// <param name="includeDeAdmin">
/// Whether to return admin data for admins that are current de-adminned.
/// </param>
/// <returns><see langword="null" /> if the player is not an admin.</returns>
AdminData? GetAdminData(IPlayerSession session, bool includeDeAdmin = false);
/// <summary>
/// De-admins an admin temporarily so they are effectively a normal player.
/// </summary>
/// <remarks>
/// De-adminned admins are able to re-admin at any time if they so desire.
/// </remarks>
void DeAdmin(IPlayerSession session);
/// <summary>
/// Re-admins a de-adminned admin.
/// </summary>
void ReAdmin(IPlayerSession session);
void Initialize();
}
}