diff --git a/Content.Client/Administration/Managers/ClientAdminManager.cs b/Content.Client/Administration/Managers/ClientAdminManager.cs index 3f072691de62f..d46a5149ec755 100644 --- a/Content.Client/Administration/Managers/ClientAdminManager.cs +++ b/Content.Client/Administration/Managers/ClientAdminManager.cs @@ -1,32 +1,46 @@ using Content.Shared.Administration; -using Content.Shared.Administration.Managers; using Robust.Client.Console; -using Robust.Client.Player; using Robust.Client.UserInterface; -using Robust.Shared.ContentPack; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Utility; namespace Content.Client.Administration.Managers { - public sealed class ClientAdminManager : IClientAdminManager, IClientConGroupImplementation, IPostInjectInit, ISharedAdminManager + public sealed class ClientAdminManager : SharedAdminManager, IClientAdminManager, IClientConGroupImplementation { - [Dependency] private readonly IPlayerManager _player = default!; [Dependency] private readonly IClientNetManager _netMgr = default!; [Dependency] private readonly IClientConGroupController _conGroup = default!; - [Dependency] private readonly IClientConsoleHost _host = default!; - [Dependency] private readonly IResourceManager _res = default!; - [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly IUserInterfaceManager _userInterface = default!; + private readonly AdminCommandPermissions _serverCommands = new(); private AdminData? _adminData; - private readonly HashSet _availableCommands = new(); - - private readonly AdminCommandPermissions _localCommandPermissions = new(); - private ISawmill _sawmill = default!; public event Action? AdminStatusUpdated; + public event Action? ConGroupUpdated; + + public override void Initialize() + { + base.Initialize(); + _netMgr.RegisterNetMessage(UpdateMessageRx); + _conGroup.Implementation = this; + } + + public override void ReloadCommandPermissions() + { + base.ReloadCommandPermissions(); + + if (ResMan.TryContentFileRead(new ResPath("/clientCommandPerms.yml"), out var efs)) + CommandPermissions.LoadPermissionsFromStream(efs); + } + + public override void ReloadToolshedPermissions() + { + // base.ReloadToolshedPermissions(); + + if (ResMan.TryContentFileRead(new ResPath("/clientToolshedEngineCommandPerms.yml"), out var f)) + ToolshedCommandPermissions.LoadPermissionsFromStream(f); + } public bool IsActive() { @@ -38,19 +52,20 @@ public bool HasFlag(AdminFlags flag) return _adminData?.HasFlag(flag) ?? false; } + public override bool CanCommand(ICommonSession session, string cmdName) + { + return PlayerMan.LocalUser == session.UserId + ? CanCommand(cmdName) + : base.CanCommand(session, cmdName); + } + public bool CanCommand(string cmdName) { if (_adminData != null && _adminData.HasFlag(AdminFlags.Host)) - { - // Host can execute all commands when connected. - // Kind of a shortcut to avoid pains during development. return true; - } - if (_localCommandPermissions.CanCommand(cmdName, _adminData)) - return true; - - return _availableCommands.Contains(cmdName); + return CommandPermissions.CanCommand(cmdName, _adminData) + || _serverCommands.CanCommand(cmdName, _adminData); } public bool CanViewVar() @@ -73,69 +88,37 @@ public bool CanAdminMenu() return _adminData?.CanAdminMenu() ?? false; } - public void Initialize() - { - _netMgr.RegisterNetMessage(UpdateMessageRx); - - // Load flags for engine commands, since those don't have the attributes. - if (_res.TryContentFileRead(new ResPath("/clientCommandPerms.yml"), out var efs)) - { - _localCommandPermissions.LoadPermissionsFromStream(efs); - } - } - private void UpdateMessageRx(MsgUpdateAdminStatus message) { - _availableCommands.Clear(); - - // Anything marked as Any we'll just add even if the server doesn't know about it. - foreach (var (command, instance) in _host.AvailableCommands) - { - if (Attribute.GetCustomAttribute(instance.GetType(), typeof(AnyCommandAttribute)) == null) - continue; - _availableCommands.Add(command); - } - - _availableCommands.UnionWith(message.AvailableCommands); - _sawmill.Debug($"Have {message.AvailableCommands.Length} commands available"); + // The server sends us a list of commands we are currently allowed to execute. + // It doesn't provide the full set of permission information. + // Hence, we just bodge it and pretend that the server-side commands we are allowed to run actually have no + // restrictions at all. + _serverCommands.Clear(); + _serverCommands.AnyCommands.UnionWith(message.AvailableCommands); _adminData = message.Admin; if (_adminData != null) { var flagsText = string.Join("|", AdminFlagsHelper.FlagsToNames(_adminData.Flags)); - _sawmill.Info($"Updated admin status: {_adminData.Active}/{_adminData.Title}/{flagsText}"); + Log.Info($"Updated admin status: {_adminData.Active}/{_adminData.Title}/{flagsText}"); if (_adminData.Active) _userInterface.DebugMonitors.SetMonitor(DebugMonitor.Coords, true); } else { - _sawmill.Info("Updated admin status: Not admin"); + Log.Info("Updated admin status: Not admin"); } + Log.Debug($"Have {_serverCommands.AnyCommands.Count} server-side commands available"); AdminStatusUpdated?.Invoke(); ConGroupUpdated?.Invoke(); } - public event Action? ConGroupUpdated; - - void IPostInjectInit.PostInject() - { - _conGroup.Implementation = this; - _sawmill = _logManager.GetSawmill("admin"); - } - - public AdminData? GetAdminData(EntityUid uid, bool includeDeAdmin = false) - { - if (uid == _player.LocalEntity && (_adminData?.Active ?? includeDeAdmin)) - return _adminData; - - return null; - } - - public AdminData? GetAdminData(ICommonSession session, bool includeDeAdmin = false) + public override AdminData? GetAdminData(ICommonSession session, bool includeDeAdmin = false) { - if (_player.LocalUser == session.UserId && (_adminData?.Active ?? includeDeAdmin)) + if (PlayerMan.LocalUser == session.UserId && (_adminData?.Active ?? includeDeAdmin)) return _adminData; return null; @@ -143,7 +126,7 @@ void IPostInjectInit.PostInject() public AdminData? GetAdminData(bool includeDeAdmin = false) { - if (_player.LocalSession is { } session) + if (PlayerMan.LocalSession is { } session) return GetAdminData(session, includeDeAdmin); return null; diff --git a/Content.Client/Administration/Managers/IClientAdminManager.cs b/Content.Client/Administration/Managers/IClientAdminManager.cs index b4b5b48b81488..898342f8c16c9 100644 --- a/Content.Client/Administration/Managers/IClientAdminManager.cs +++ b/Content.Client/Administration/Managers/IClientAdminManager.cs @@ -59,8 +59,6 @@ public interface IClientAdminManager /// bool CanAdminMenu(); - void Initialize(); - /// /// Checks if the client is an admin. /// diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index f35272b63a201..7a8bb1a7f1c4d 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -1,4 +1,3 @@ -using Content.Client.Administration.Managers; using Content.Client.Changelog; using Content.Client.Chat.Managers; using Content.Client.DebugMon; @@ -48,7 +47,6 @@ public sealed class EntryPoint : GameClient [Dependency] private readonly IStateManager _stateManager = default!; [Dependency] private readonly IComponentFactory _componentFactory = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IClientAdminManager _adminManager = default!; [Dependency] private readonly IParallaxManager _parallaxManager = default!; [Dependency] private readonly IConfigurationManager _configManager = default!; [Dependency] private readonly IStylesheetManager _stylesheetManager = default!; @@ -132,7 +130,6 @@ public override void Init() _prototypeManager.RegisterIgnore("codewordFaction"); _componentFactory.GenerateNetIds(); - _adminManager.Initialize(); _screenshotHook.Initialize(); _fullscreenHook.Initialize(); _changelogManager.Initialize(); diff --git a/Content.Server/Administration/AdminCommandAttribute.cs b/Content.Server/Administration/AdminCommandAttribute.cs deleted file mode 100644 index 409d9fe984dcf..0000000000000 --- a/Content.Server/Administration/AdminCommandAttribute.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Content.Shared.Administration; -using JetBrains.Annotations; -using Robust.Shared.Console; - -namespace Content.Server.Administration -{ - /// - /// Specifies that a command can only be executed by an admin with the specified flags. - /// - /// - /// If this attribute is used multiple times, either attribute's flag sets can be used to get access. - /// - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] - [MeansImplicitUse] - public sealed class AdminCommandAttribute : Attribute - { - public AdminCommandAttribute(AdminFlags flags) - { - Flags = flags; - } - - public AdminFlags Flags { get; } - } -} diff --git a/Content.Server/Administration/Managers/AdminManager.cs b/Content.Server/Administration/Managers/AdminManager.cs index 59fc11f7bbc00..3ecf5cbdb0486 100644 --- a/Content.Server/Administration/Managers/AdminManager.cs +++ b/Content.Server/Administration/Managers/AdminManager.cs @@ -1,6 +1,4 @@ -using System.Diagnostics; using System.Linq; -using System.Reflection; using System.Threading.Tasks; using Content.Server.Chat.Managers; using Content.Server.Database; @@ -8,32 +6,21 @@ using Content.Shared.CCVar; using Content.Shared.Players; using Robust.Server.Console; -using Robust.Server.Player; using Robust.Shared.Configuration; using Robust.Shared.Console; -using Robust.Shared.ContentPack; using Robust.Shared.Enums; using Robust.Shared.Network; using Robust.Shared.Player; -using Robust.Shared.Toolshed; -using Robust.Shared.Toolshed.Errors; -using Robust.Shared.Utility; - namespace Content.Server.Administration.Managers { - public sealed partial class AdminManager : IAdminManager, IPostInjectInit, IConGroupControllerImplementation + public sealed partial class AdminManager : SharedAdminManager, IAdminManager, 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 IServerConsoleHost _consoleHost = default!; [Dependency] private readonly IChatManager _chat = default!; - [Dependency] private readonly ToolshedManager _toolshed = default!; - [Dependency] private readonly ILogManager _logManager = default!; + [Dependency] private readonly IConGroupController _conGroup = default!; private readonly Dictionary _admins = new(); private readonly HashSet _promotedPlayers = new(); @@ -46,17 +33,19 @@ public sealed partial class AdminManager : IAdminManager, IPostInjectInit, IConG public IEnumerable AllAdmins => _admins.Select(p => p.Key); - private readonly AdminCommandPermissions _commandPermissions = new(); - private readonly AdminCommandPermissions _toolshedCommandPermissions = new(); - private ISawmill _sawmill = default!; - public bool IsAdmin(ICommonSession session, bool includeDeAdmin = false) + public override void Initialize() { - return GetAdminData(session, includeDeAdmin) != null; + base.Initialize(); + _netMgr.RegisterNetMessage(); + PlayerMan.PlayerStatusChanged += PlayerStatusChanged; + InitializeMetrics(); + _conGroup.Implementation = this; + Toolshed.ActivePermissionController = this; } - public AdminData? GetAdminData(ICommonSession session, bool includeDeAdmin = false) + public override AdminData? GetAdminData(ICommonSession session, bool includeDeAdmin = false) { if (_admins.TryGetValue(session, out var reg) && (reg.Data.Active || includeDeAdmin)) { @@ -66,14 +55,6 @@ public bool IsAdmin(ICommonSession session, bool includeDeAdmin = false) return null; } - public AdminData? GetAdminData(EntityUid uid, bool includeDeAdmin = false) - { - if (_playerManager.TryGetSessionByEntity(uid, out var session)) - return GetAdminData(session, includeDeAdmin); - - return null; - } - public void DeAdmin(ICommonSession session) { if (!_admins.TryGetValue(session, out var reg)) @@ -247,67 +228,6 @@ public void ReloadAdminsWithRank(int rankId) } } - public void Initialize() - { - _sawmill = _logManager.GetSawmill("admin"); - - _netMgr.RegisterNetMessage(); - - // Cache permissions for loaded console commands with the requisite attributes. - foreach (var (cmdName, cmd) in _consoleHost.AvailableCommands) - { - var (isAvail, flagsReq) = GetRequiredFlag(cmd); - - if (!isAvail) - { - continue; - } - - if (flagsReq.Length != 0) - { - _commandPermissions.AdminCommands.Add(cmdName, flagsReq); - } - else - { - _commandPermissions.AnyCommands.Add(cmdName); - } - } - - foreach (var spec in _toolshed.DefaultEnvironment.AllCommands()) - { - var (isAvail, flagsReq) = GetRequiredFlag(spec.Cmd); - - if (!isAvail) - { - continue; - } - - if (flagsReq.Length != 0) - { - _toolshedCommandPermissions.AdminCommands.TryAdd(spec.Cmd.Name, flagsReq); - } - else - { - _toolshedCommandPermissions.AnyCommands.Add(spec.Cmd.Name); - } - } - - // Load flags for engine commands, since those don't have the attributes. - if (_res.TryContentFileRead(new ResPath("/engineCommandPerms.yml"), out var efs)) - { - _commandPermissions.LoadPermissionsFromStream(efs); - } - - if (_res.TryContentFileRead(new ResPath("/toolshedEngineCommandPerms.yml"), out var toolshedPerms)) - { - _toolshedCommandPermissions.LoadPermissionsFromStream(toolshedPerms); - } - - _toolshed.ActivePermissionController = this; - - InitializeMetrics(); - } - public void PromoteHost(ICommonSession player) { _promotedPlayers.Add(player.UserId); @@ -315,24 +235,18 @@ public void PromoteHost(ICommonSession player) ReloadAdmin(player); } - void IPostInjectInit.PostInject() - { - _playerManager.PlayerStatusChanged += PlayerStatusChanged; - _conGroup.Implementation = this; - } - // NOTE: Also sends commands list for non admins.. private void UpdateAdminStatus(ICommonSession session) { var msg = new MsgUpdateAdminStatus(); - var commands = new List(_commandPermissions.AnyCommands); + var commands = new List(CommandPermissions.AnyCommands); if (_admins.TryGetValue(session, out var adminData)) { msg.Admin = adminData.Data; - commands.AddRange(_commandPermissions.AdminCommands + commands.AddRange(CommandPermissions.AdminCommands .Where(p => p.Value.Any(f => adminData.Data.HasFlag(f))) .Select(p => p.Key)); } @@ -512,152 +426,26 @@ private static bool IsLocal(ICommonSession player) return Equals(addr, System.Net.IPAddress.Loopback) || Equals(addr, System.Net.IPAddress.IPv6Loopback); } - public bool TryGetCommandFlags(CommandSpec command, out AdminFlags[]? flags) - { - var cmdName = command.Cmd.Name; - - if (_toolshedCommandPermissions.AnyCommands.Contains(cmdName)) - { - // Anybody can use this command. - flags = null; - return true; - } - - if (_toolshedCommandPermissions.AdminCommands.TryGetValue(cmdName, out flags)) - { - return true; - } - - flags = null; - return false; - } - - public bool CanCommand(ICommonSession session, string cmdName) - { - if (_commandPermissions.AnyCommands.Contains(cmdName)) - { - // Anybody can use this command. - return true; - } - - if (!_commandPermissions.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; - } - - public bool CheckInvokable(CommandSpec command, ICommonSession? user, out IConError? error) - { - if (user is null) - { - error = null; - return true; // Server console. - } - - var name = command.Cmd.Name; - if (!TryGetCommandFlags(command, out var flags)) - { - // Command is missing permissions. - error = new CommandPermissionsUnassignedError(command); - return false; - } - - if (flags is null) - { - // Anyone can execute this. - error = null; - return true; - } - - var data = GetAdminData(user); - if (data == null) - { - // Player isn't an admin. - error = new NoPermissionError(command); - return false; - } - - foreach (var flag in flags) - { - if (data.HasFlag(flag)) - { - error = null; - return true; - } - } - - error = new NoPermissionError(command); - return false; - } - - private static (bool isAvail, AdminFlags[] flagsReq) GetRequiredFlag(object cmd) + protected override (bool isAvail, AdminFlags[] flagsReq) GetRequiredFlags(object cmd) { - MemberInfo type = cmd.GetType(); + if (cmd is not ConsoleHost.RegisteredCommand registered) + return base.GetRequiredFlags(cmd); - if (cmd is ConsoleHost.RegisteredCommand registered) - { - type = registered.Callback.Method; - } + // This command is just a method that was registered explicitly via IConsoleHost.Register() + // Sandboxing currently prevents us from checking attribute on this method in shared code + // TODO FIX THIS + var method = registered.Callback.Method; + if (Attribute.IsDefined(method, typeof(AnyCommandAttribute))) + return (true, []); - if (Attribute.IsDefined(type, typeof(AnyCommandAttribute))) - { - // Available to everybody. - return (true, Array.Empty()); - } - - var attribs = type.GetCustomAttributes(typeof(AdminCommandAttribute)) + var attribs = Attribute.GetCustomAttributes(method, typeof(AdminCommandAttribute)) .Cast() .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(ICommonSession session) - { - return CanCommand(session, "vv"); - } - - public bool CanAdminPlace(ICommonSession session) - { - return GetAdminData(session)?.CanAdminPlace() ?? false; - } - - public bool CanScript(ICommonSession session) - { - return GetAdminData(session)?.CanScript() ?? false; - } - - public bool CanAdminMenu(ICommonSession session) - { - return GetAdminData(session)?.CanAdminMenu() ?? false; - } - - public bool CanAdminReloadPrototypes(ICommonSession session) - { - return GetAdminData(session)?.CanAdminReloadPrototypes() ?? false; - } - private void SendPermsChangedEvent(ICommonSession session) { var flags = GetAdminData(session)?.Flags; @@ -682,28 +470,3 @@ public AdminReg(ICommonSession session, AdminData data) } } } - -public record struct CommandPermissionsUnassignedError(CommandSpec Command) : IConError -{ - public FormattedMessage DescribeInner() - { - return FormattedMessage.FromMarkupOrThrow($"The command {Command.FullName()} is missing permission flags and cannot be executed."); - } - - public string? Expression { get; set; } - public Vector2i? IssueSpan { get; set; } - public StackTrace? Trace { get; set; } -} - - -public record struct NoPermissionError(CommandSpec Command) : IConError -{ - public FormattedMessage DescribeInner() - { - return FormattedMessage.FromMarkupOrThrow($"You do not have permission to execute {Command.FullName()}"); - } - - public string? Expression { get; set; } - public Vector2i? IssueSpan { get; set; } - public StackTrace? Trace { get; set; } -} diff --git a/Content.Server/Administration/Managers/IAdminManager.cs b/Content.Server/Administration/Managers/IAdminManager.cs index 95967b24acae1..9931917611f18 100644 --- a/Content.Server/Administration/Managers/IAdminManager.cs +++ b/Content.Server/Administration/Managers/IAdminManager.cs @@ -64,8 +64,6 @@ public interface IAdminManager : ISharedAdminManager /// void ReloadAdminsWithRank(int rankId); - void Initialize(); - void PromoteHost(ICommonSession player); bool TryGetCommandFlags(CommandSpec command, out AdminFlags[]? flags); diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index 7ebb54687974c..aa943dd57db9b 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -47,7 +47,6 @@ public sealed class EntryPoint : GameServer [Dependency] private readonly DiscordLink _discordLink = default!; [Dependency] private readonly EuiManager _euiManager = default!; [Dependency] private readonly GhostKickManager _ghostKick = default!; - [Dependency] private readonly IAdminManager _admin = default!; [Dependency] private readonly IAdminLogManager _adminLog = default!; [Dependency] private readonly IAfkManager _afk = default!; [Dependency] private readonly IBanManager _ban = default!; @@ -153,7 +152,6 @@ public override void PostInit() } _recipe.Initialize(); - _admin.Initialize(); _afk.Initialize(); _rules.Initialize(); _discordLink.Initialize(); diff --git a/Content.Shared/Administration/AdminCommandAttribute.cs b/Content.Shared/Administration/AdminCommandAttribute.cs new file mode 100644 index 0000000000000..5d66c3097fced --- /dev/null +++ b/Content.Shared/Administration/AdminCommandAttribute.cs @@ -0,0 +1,17 @@ +using JetBrains.Annotations; + +namespace Content.Shared.Administration; + +/// +/// Specifies that a command can only be executed by an admin with the specified flags. +/// +/// +/// If this attribute is used multiple times, either attribute's flag sets can be used to get access. +/// +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] +[MeansImplicitUse] +public sealed class AdminCommandAttribute(AdminFlags flags) : Attribute +{ + public AdminFlags Flags { get; } = flags; +} diff --git a/Content.Shared/Administration/AdminCommandPermissions.cs b/Content.Shared/Administration/AdminCommandPermissions.cs index 130409352fe55..3fa35b081ced7 100644 --- a/Content.Shared/Administration/AdminCommandPermissions.cs +++ b/Content.Shared/Administration/AdminCommandPermissions.cs @@ -13,6 +13,12 @@ public sealed class AdminCommandPermissions // Commands only executable by admins with one of the given flag masks. public readonly Dictionary AdminCommands = new(); + public void Clear() + { + AnyCommands.Clear(); + AdminCommands.Clear(); + } + public void LoadPermissionsFromStream(Stream fs) { using var reader = new StreamReader(fs, EncodingHelpers.UTF8); diff --git a/Content.Shared/Administration/Managers/ISharedAdminManager.cs b/Content.Shared/Administration/Managers/ISharedAdminManager.cs index f24d7f37d9ad3..e7056fe796031 100644 --- a/Content.Shared/Administration/Managers/ISharedAdminManager.cs +++ b/Content.Shared/Administration/Managers/ISharedAdminManager.cs @@ -7,6 +7,8 @@ namespace Content.Shared.Administration.Managers; /// public interface ISharedAdminManager { + void Initialize(); + /// /// Gets the admin data for a player, if they are an admin. /// @@ -41,11 +43,7 @@ public interface ISharedAdminManager /// Whether to check flags even for admins that are current de-adminned. /// /// True if the player is and admin and has the specified flags. - bool HasAdminFlag(EntityUid player, AdminFlags flag, bool includeDeAdmin = false) - { - var data = GetAdminData(player, includeDeAdmin); - return data != null && data.HasFlag(flag, includeDeAdmin); - } + bool HasAdminFlag(EntityUid player, AdminFlags flag, bool includeDeAdmin = false); /// /// See if a player has an admin flag. @@ -57,11 +55,7 @@ bool HasAdminFlag(EntityUid player, AdminFlags flag, bool includeDeAdmin = false /// Whether to check flags even for admins that are current de-adminned. /// /// True if the player is and admin and has the specified flags. - bool HasAdminFlag(ICommonSession player, AdminFlags flag, bool includeDeAdmin = false) - { - var data = GetAdminData(player, includeDeAdmin); - return data != null && data.HasFlag(flag, includeDeAdmin); - } + bool HasAdminFlag(ICommonSession player, AdminFlags flag, bool includeDeAdmin = false); /// /// Checks if a player is an admin. @@ -73,10 +67,7 @@ bool HasAdminFlag(ICommonSession player, AdminFlags flag, bool includeDeAdmin = /// Whether to return admin data for admins that are current de-adminned. /// /// true if the player is an admin, false otherwise. - bool IsAdmin(EntityUid uid, bool includeDeAdmin = false) - { - return GetAdminData(uid, includeDeAdmin) != null; - } + bool IsAdmin(EntityUid uid, bool includeDeAdmin = false); /// /// Checks if a player is an admin. @@ -88,8 +79,8 @@ bool IsAdmin(EntityUid uid, bool includeDeAdmin = false) /// Whether to return admin data for admins that are current de-adminned. /// /// true if the player is an admin, false otherwise. - bool IsAdmin(ICommonSession session, bool includeDeAdmin = false) - { - return GetAdminData(session, includeDeAdmin) != null; - } + bool IsAdmin(ICommonSession session, bool includeDeAdmin = false); + + void ReloadCommandPermissions(); + void ReloadToolshedPermissions(); } diff --git a/Content.Shared/Administration/SharedAdminManager.cs b/Content.Shared/Administration/SharedAdminManager.cs new file mode 100644 index 0000000000000..36e7ffc2d2acc --- /dev/null +++ b/Content.Shared/Administration/SharedAdminManager.cs @@ -0,0 +1,246 @@ +using System.Linq; +using Content.Shared.Administration.Managers; +using Robust.Shared.Console; +using Robust.Shared.ContentPack; +using Robust.Shared.Player; +using Robust.Shared.Toolshed; +using Robust.Shared.Toolshed.Errors; +using Robust.Shared.Utility; + +namespace Content.Shared.Administration; + +public abstract class SharedAdminManager : ISharedAdminManager +{ + [Dependency] protected readonly IConsoleHost ConsoleHost = default!; + [Dependency] protected readonly ToolshedManager Toolshed = default!; + [Dependency] protected readonly ILocalizationManager Loc = default!; + [Dependency] protected readonly IResourceManager ResMan = default!; + [Dependency] protected readonly ISharedPlayerManager PlayerMan = default!; + [Dependency] private readonly ILogManager _logManager = default!; + + protected readonly AdminCommandPermissions CommandPermissions = new(); + protected readonly AdminCommandPermissions ToolshedCommandPermissions = new(); + protected ISawmill Log = default!; + + public bool Initialized { get; private set; } + + public virtual void Initialize() + { + if (Initialized) + throw new InvalidOperationException("Already initialized."); + Initialized = true; + + Log = _logManager.GetSawmill("admin"); + ReloadCommandPermissions(); + ReloadToolshedPermissions(); + } + + public virtual void ReloadCommandPermissions() + { + CommandPermissions.Clear(); + + foreach (var (cmdName, cmd) in ConsoleHost.AvailableCommands) + { + var (isAvail, flagsReq) = GetRequiredFlags(cmd); + if (!isAvail) + continue; + + if (flagsReq.Length != 0) + CommandPermissions.AdminCommands.Add(cmdName, flagsReq); + else + CommandPermissions.AnyCommands.Add(cmdName); + } + + // Load flags for engine commands, since those don't have the attributes. + if (ResMan.TryContentFileRead(new ResPath("/engineCommandPerms.yml"), out var efs)) + CommandPermissions.LoadPermissionsFromStream(efs); + } + + public virtual void ReloadToolshedPermissions() + { + ToolshedCommandPermissions.Clear(); + foreach (var spec in Toolshed.DefaultEnvironment.AllCommands()) + { + var (isAvail, flagsReq) = GetRequiredFlags(spec); + if (!isAvail) + continue; + + if (flagsReq.Length != 0) + ToolshedCommandPermissions.AdminCommands.TryAdd(spec.Cmd.Name, flagsReq); + else + ToolshedCommandPermissions.AnyCommands.Add(spec.Cmd.Name); + } + + if (ResMan.TryContentFileRead(new ResPath("/toolshedEngineCommandPerms.yml"), out var toolshedPerms)) + ToolshedCommandPermissions.LoadPermissionsFromStream(toolshedPerms); + } + + public abstract AdminData? GetAdminData(ICommonSession session, bool includeDeAdmin = false); + + + public AdminData? GetAdminData(EntityUid uid, bool includeDeAdmin = false) + { + return PlayerMan.TryGetSessionByEntity(uid, out var session) + ? GetAdminData(session, includeDeAdmin) + : null; + } + + public bool TryGetCommandFlags(CommandSpec command, out AdminFlags[]? flags) + { + var cmdName = command.Cmd.Name; + + if (ToolshedCommandPermissions.AnyCommands.Contains(cmdName)) + { + // Anybody can use this command. + flags = null; + return true; + } + + if (ToolshedCommandPermissions.AdminCommands.TryGetValue(cmdName, out flags)) + { + return true; + } + + flags = null; + return false; + } + + public virtual bool CanCommand(ICommonSession session, string cmdName) + { + return CommandPermissions.CanCommand(cmdName, GetAdminData(session)); + } + + public bool CheckInvokable(CommandSpec command, ICommonSession? user, out IConError? error) + { + if (user is null) + { + error = null; + return true; // Server console. + } + + if (!TryGetCommandFlags(command, out var flags)) + { + // Command is missing permissions. + error = new CommandPermissionsUnassignedError(command); + return false; + } + + if (flags is null) + { + // Anyone can execute this. + error = null; + return true; + } + + var data = GetAdminData(user); + if (data == null) + { + error = new NoPermissionError(Loc.GetString("cmd-insufficient-permissions", ("cmd", command.FullName()))); + return false; + } + + foreach (var flag in flags) + { + if (data.HasFlag(flag)) + { + error = null; + return true; + } + } + + error = new NoPermissionError(Loc.GetString("cmd-insufficient-permissions", ("cmd", command.FullName()))); + return false; + } + + protected virtual (bool isAvail, AdminFlags[] flagsReq) GetRequiredFlags(object cmd) + { + var type = cmd switch + { + CommandSpec spec => spec.Cmd.GetType(), + // ToolshedProxyCommand proxy => proxy.Spec.Cmd.GetType(), // Toolshed command + _ => cmd.GetType() // Normal IConsoleCommand or some other object + }; + + if (Attribute.IsDefined(type, typeof(AnyCommandAttribute))) + { + // Available to everybody. + return (true, []); + } + + var attribs = Attribute.GetCustomAttributes(type, typeof(AdminCommandAttribute)) + .Cast() + .Select(p => p.Flags) + .ToArray(); + + // If attribs.length == 0 then no access attribute is specified, meaning that this is a server-only command. + return (attribs.Length != 0, attribs); + } + + #region Public Helpers + + public bool CanViewVar(ICommonSession session) + { + return CanCommand(session, "vv"); + } + + public bool CanAdminPlace(ICommonSession session) + { + return GetAdminData(session)?.CanAdminPlace() ?? false; + } + + public bool CanScript(ICommonSession session) + { + return GetAdminData(session)?.CanScript() ?? false; + } + + public bool CanAdminMenu(ICommonSession session) + { + return GetAdminData(session)?.CanAdminMenu() ?? false; + } + + public bool CanAdminReloadPrototypes(ICommonSession session) + { + return GetAdminData(session)?.CanAdminReloadPrototypes() ?? false; + } + + public bool IsAdmin(EntityUid uid, bool includeDeAdmin = false) + { + return GetAdminData(uid, includeDeAdmin) != null; + } + + public bool IsAdmin(ICommonSession session, bool includeDeAdmin = false) + { + return GetAdminData(session, includeDeAdmin) != null; + } + + public bool HasAdminFlag(EntityUid player, AdminFlags flag, bool includeDeAdmin = false) + { + var data = GetAdminData(player, includeDeAdmin); + return data != null && data.HasFlag(flag, includeDeAdmin); + } + + public bool HasAdminFlag(ICommonSession player, AdminFlags flag, bool includeDeAdmin = false) + { + var data = GetAdminData(player, includeDeAdmin); + return data != null && data.HasFlag(flag, includeDeAdmin); + } + + #endregion +} + +public sealed class CommandPermissionsUnassignedError(CommandSpec cmd) : ConError +{ + public override FormattedMessage DescribeInner() + { + return FormattedMessage.FromMarkupOrThrow( + $"The command {cmd.FullName()} is missing permission flags and cannot be executed."); + } +} + +public sealed class NoPermissionError(string msg) : ConError +{ + public override FormattedMessage DescribeInner() + { + return FormattedMessage.FromMarkupOrThrow(msg); + } +} diff --git a/Content.Shared/Entry/EntryPoint.cs b/Content.Shared/Entry/EntryPoint.cs index db8d6a6abddf2..51f3ac18d3a13 100644 --- a/Content.Shared/Entry/EntryPoint.cs +++ b/Content.Shared/Entry/EntryPoint.cs @@ -1,8 +1,8 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using Content.Shared.Administration.Managers; using Content.Shared.Humanoid.Markings; -using Content.Shared.IoC; using Content.Shared.Maps; using Robust.Shared; using Robust.Shared.Configuration; @@ -21,14 +21,12 @@ public sealed class EntryPoint : GameShared [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!; [Dependency] private readonly IResourceManager _resMan = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly ISharedAdminManager _adminManager = default!; + [Dependency] private readonly MarkingManager _marking = default!; private readonly ResPath _ignoreFileDirectory = new("/IgnoredPrototypes/"); - public override void PreInit() - { - IoCManager.InjectDependencies(this); - } - public override void Shutdown() { _prototypeManager.PrototypesReloaded -= PrototypeReload; @@ -36,6 +34,8 @@ public override void Shutdown() public override void Init() { + Dependencies.BuildGraph(); + Dependencies.InjectDependencies(this); IgnorePrototypes(); } @@ -43,14 +43,14 @@ public override void PostInit() { base.PostInit(); + _adminManager.Initialize(); InitTileDefinitions(); - IoCManager.Resolve().Initialize(); + _marking.Initialize(); #if DEBUG - var configMan = IoCManager.Resolve(); - configMan.OverrideDefault(CVars.NetFakeLagMin, 0.075f); - configMan.OverrideDefault(CVars.NetFakeLoss, 0.005f); - configMan.OverrideDefault(CVars.NetFakeDuplicates, 0.005f); + _cfg.OverrideDefault(CVars.NetFakeLagMin, 0.075f); + _cfg.OverrideDefault(CVars.NetFakeLoss, 0.005f); + _cfg.OverrideDefault(CVars.NetFakeDuplicates, 0.005f); #endif }