From 1ae7ed8ed5934ddcdff9b9ca126f8fd6c75de29f Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Wed, 16 Apr 2025 00:38:59 +1200 Subject: [PATCH 1/7] Enable client-side toolshed commands --- .../Managers/ClientAdminManager.cs | 110 +++---- .../Managers/IClientAdminManager.cs | 2 - Content.Client/Entry/EntryPoint.cs | 8 +- .../Administration/AdminCommandAttribute.cs | 25 -- .../Administration/Managers/AdminManager.cs | 284 ++---------------- .../Administration/Managers/IAdminManager.cs | 2 - Content.Server/Entry/EntryPoint.cs | 12 +- Content.Server/Maps/GridDraggingSystem.cs | 1 + Content.Server/Sandbox/SandboxSystem.cs | 1 + .../Administration/AdminCommandAttribute.cs | 17 ++ .../Administration/AdminCommandPermissions.cs | 14 +- .../Managers/ISharedAdminManager.cs | 5 + .../Administration/SharedAdminManager.cs | 237 +++++++++++++++ Content.Shared/Entry/EntryPoint.cs | 6 +- .../clientToolshedEngineCommandPerms.yml | 118 ++++++++ 15 files changed, 474 insertions(+), 368 deletions(-) delete mode 100644 Content.Server/Administration/AdminCommandAttribute.cs create mode 100644 Content.Shared/Administration/AdminCommandAttribute.cs create mode 100644 Content.Shared/Administration/SharedAdminManager.cs create mode 100644 Resources/clientToolshedEngineCommandPerms.yml diff --git a/Content.Client/Administration/Managers/ClientAdminManager.cs b/Content.Client/Administration/Managers/ClientAdminManager.cs index 0f740c810459a..e4547d792473e 100644 --- a/Content.Client/Administration/Managers/ClientAdminManager.cs +++ b/Content.Client/Administration/Managers/ClientAdminManager.cs @@ -1,31 +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 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() { @@ -37,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() @@ -72,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(); - var host = IoCManager.Resolve(); - - // 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; @@ -142,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 5c1f94f333291..2940f5f757476 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; @@ -46,7 +45,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!; @@ -73,10 +71,13 @@ public sealed class EntryPoint : GameClient [Dependency] private readonly DebugMonitorManager _debugMonitorManager = default!; [Dependency] private readonly TitleWindowManager _titleWindowManager = default!; - public override void Init() + public override void PreInit() { ClientContentIoC.Register(); + } + public override void Init() + { foreach (var callback in TestingCallbacks) { var cast = (ClientModuleTestingCallbacks) callback; @@ -123,7 +124,6 @@ public override void Init() _prototypeManager.RegisterIgnore("ghostRoleRaffleDecider"); _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..5bd5c77625101 100644 --- a/Content.Server/Administration/Managers/AdminManager.cs +++ b/Content.Server/Administration/Managers/AdminManager.cs @@ -1,39 +1,24 @@ -using System.Diagnostics; using System.Linq; -using System.Reflection; using System.Threading.Tasks; using Content.Server.Chat.Managers; using Content.Server.Database; using Content.Shared.Administration; 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 { - [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!; private readonly Dictionary _admins = new(); private readonly HashSet _promotedPlayers = new(); @@ -46,17 +31,17 @@ 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(); } - 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 +51,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 +224,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 +231,26 @@ 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); + + // For the client, server-side toolshed commands are indistinguishable from "normal" server side commands. + // Hence, we just lump them together. + commands.AddRange(ToolshedCommandPermissions.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)); + + commands.AddRange(ToolshedCommandPermissions.AdminCommands .Where(p => p.Value.Any(f => adminData.Data.HasFlag(f))) .Select(p => p.Key)); } @@ -512,152 +430,23 @@ 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; - } + 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 +471,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 9d5cb0b10ea7b..e45f7c8fbcf32 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -14,8 +14,6 @@ using Content.Server.IoC; using Content.Server.Maps; using Content.Server.NodeContainer.NodeGroups; -using Content.Server.Objectives; -using Content.Server.Players; using Content.Server.Players.JobWhitelist; using Content.Server.Players.PlayTimeTracking; using Content.Server.Players.RateLimiting; @@ -45,11 +43,15 @@ public sealed class EntryPoint : GameServer private IVoteManager _voteManager = default!; private ServerUpdateManager _updateManager = default!; private PlayTimeTrackingManager? _playTimeTracking; - private IEntitySystemManager? _sysMan; private IServerDbManager? _dbManager; private IWatchlistWebhookManager _watchlistWebhookManager = default!; private IConnectionManager? _connectionManager; + public override void PreInit() + { + ServerContentIoC.Register(); + } + /// public override void Init() { @@ -74,8 +76,6 @@ public override void Init() prototypes.RegisterIgnore("parallax"); - ServerContentIoC.Register(); - foreach (var callback in TestingCallbacks) { var cast = (ServerModuleTestingCallbacks) callback; @@ -94,7 +94,6 @@ public override void Init() _updateManager = IoCManager.Resolve(); _playTimeTracking = IoCManager.Resolve(); _connectionManager = IoCManager.Resolve(); - _sysMan = IoCManager.Resolve(); _dbManager = IoCManager.Resolve(); _watchlistWebhookManager = IoCManager.Resolve(); @@ -143,7 +142,6 @@ public override void PostInit() else { IoCManager.Resolve().Initialize(); - IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); _euiManager.Initialize(); diff --git a/Content.Server/Maps/GridDraggingSystem.cs b/Content.Server/Maps/GridDraggingSystem.cs index cf71e012f48a6..1e723608b6b57 100644 --- a/Content.Server/Maps/GridDraggingSystem.cs +++ b/Content.Server/Maps/GridDraggingSystem.cs @@ -1,5 +1,6 @@ using Content.Shared.Maps; using Robust.Server.Console; +using Robust.Shared.Console; using Robust.Shared.Utility; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Systems; diff --git a/Content.Server/Sandbox/SandboxSystem.cs b/Content.Server/Sandbox/SandboxSystem.cs index 30fe4e0fe95e3..15a5aed5fc5dc 100644 --- a/Content.Server/Sandbox/SandboxSystem.cs +++ b/Content.Server/Sandbox/SandboxSystem.cs @@ -12,6 +12,7 @@ using Robust.Server.Console; using Robust.Server.Placement; using Robust.Server.Player; +using Robust.Shared.Console; using Robust.Shared.Enums; using Robust.Shared.Player; using Robust.Shared.Prototypes; 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..777e899de43c9 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); @@ -58,15 +64,15 @@ public bool CanCommand(string cmdName, AdminData? admin) return true; } - if (!AdminCommands.TryGetValue(cmdName, out var flagsReq)) + if (admin == null) { - // Server-console only. + // Player isn't an admin. return false; } - if (admin == null) + if (!AdminCommands.TryGetValue(cmdName, out var flagsReq)) { - // Player isn't an admin. + // Server-console only. return false; } diff --git a/Content.Shared/Administration/Managers/ISharedAdminManager.cs b/Content.Shared/Administration/Managers/ISharedAdminManager.cs index f24d7f37d9ad3..00a0a0799ef34 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. /// @@ -92,4 +94,7 @@ bool IsAdmin(ICommonSession session, bool includeDeAdmin = false) { return GetAdminData(session, includeDeAdmin) != null; } + + void ReloadCommandPermissions(); + void ReloadToolshedPermissions(); } diff --git a/Content.Shared/Administration/SharedAdminManager.cs b/Content.Shared/Administration/SharedAdminManager.cs new file mode 100644 index 0000000000000..232b11d654699 --- /dev/null +++ b/Content.Shared/Administration/SharedAdminManager.cs @@ -0,0 +1,237 @@ +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, IConGroupControllerImplementation +{ + [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!; + [Dependency] private readonly IConGroupController _conGroup = 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; + _conGroup.Implementation = this; + Toolshed.ActivePermissionController = this; + + Log = _logManager.GetSawmill("admin"); + ReloadCommandPermissions(); + ReloadToolshedPermissions(); + Toolshed.CommandsLoaded += ReloadToolshedPermissions; + } + + public virtual void ReloadCommandPermissions() + { + CommandPermissions.Clear(); + + foreach (var (cmdName, cmd) in ConsoleHost.RegisteredCommands) + { + 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() + { + if (!Toolshed.Started) + return; + + 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 bool IsAdmin(ICommonSession session, bool includeDeAdmin = false) + { + return GetAdminData(session, includeDeAdmin) != null; + } + + 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 + { + // Method in somewhere that was explicitly registered via IConsoleHost.Register + // Alas, sandboxing prevents us from checking this. + // TODO FIX THIS + // ConsoleHost.RegisteredCommand registered => registered.Callback.Method, + + CommandSpec spec => spec.Cmd.GetType(), // Toolshed command + 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, + // 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; + } +} + +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 df267b08cb110..269596478534b 100644 --- a/Content.Shared/Entry/EntryPoint.cs +++ b/Content.Shared/Entry/EntryPoint.cs @@ -1,6 +1,7 @@ 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; @@ -21,12 +22,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 ISharedAdminManager _adminManager = default!; private readonly ResPath _ignoreFileDirectory = new("/IgnoredPrototypes/"); public override void PreInit() { - IoCManager.InjectDependencies(this); SharedContentIoC.Register(); } @@ -37,6 +38,8 @@ public override void Shutdown() public override void Init() { + IoCManager.BuildGraph(); + IoCManager.InjectDependencies(this); IgnorePrototypes(); } @@ -44,6 +47,7 @@ public override void PostInit() { base.PostInit(); + _adminManager.Initialize(); InitTileDefinitions(); IoCManager.Resolve().Initialize(); diff --git a/Resources/clientToolshedEngineCommandPerms.yml b/Resources/clientToolshedEngineCommandPerms.yml new file mode 100644 index 0000000000000..0f599247e3abd --- /dev/null +++ b/Resources/clientToolshedEngineCommandPerms.yml @@ -0,0 +1,118 @@ +# Most server-side toolshed are arbitrarily locked behind the DEBUG permissions. +# This is largely a precaution to avoid allowing clients to somehow spam the sever with expensive math operations. +# But we allow most of these when running client-side: +- Commands: + - iota + - to + - select + - where + - count + - '=>' + - '?' + - 'or?' + - '??' + - rng + - self + - sum + - take + - join + - search + - first + - unique + - any + - contains + - isnull + - isempty + - stopwatch + - append + - min + - max + - average + - '+' + - '-' + - '*' + - '/' + - '%' + - '%/' + - '&~' + - bitornot + - '^~' + - '~' + - '<' + - '>' + - '<=' + - '>=' + - '==' + - '!=' + - '+/' + - '-/' + - '*/' + - '//' + - '&' + - bitor + - '^' + - neg + - abs + - bibytecount + - shortestbitlength + - countleadzeros + - counttrailingzeros + - fpi + - fe + - ftau + - fepsilon + - dpi + - de + - dtau + - depsilon + - hpi + - he + - htau + - hepsilon + - floor + - ceil + - round + - trunc + - round2frac + - exponentbytecount + - significandbytecount + - significandbitcount + - exponentshortestbitcount + - stepnext + - stepprev + - checkedto + - saturateto + - truncto + - iscanonical + - iscomplex + - iseven + - isodd + - isfinite + - isimaginary + - isinfinite + - isinteger + - isnan + - isnegative + - ispositive + - isreal + - issubnormal + - iszero + - pow + - sqrt + - cbrt + - root + - hypot + - sin + - sinpi + - asin + - asinpi + - cos + - cospi + - acos + - acospi + - tan + - tanpi + - atan + - atanpi + - pick + - tee From 49212497d525d4d7074dc9c8e94743e1f138e9b7 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Sat, 19 Apr 2025 22:05:02 +1200 Subject: [PATCH 2/7] Test fix? --- Content.Client/Entry/EntryPoint.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 2940f5f757476..eef3479c6268a 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -74,16 +74,18 @@ public sealed class EntryPoint : GameClient public override void PreInit() { ClientContentIoC.Register(); - } - public override void Init() - { + // Content.Shared.Entry.EntryPoint.Init() will call BuildGraph() & InjectDependencies() + // Hence this needs to be called in PreInit, instead of in Init() foreach (var callback in TestingCallbacks) { - var cast = (ClientModuleTestingCallbacks) callback; + var cast = (ClientModuleTestingCallbacks)callback; cast.ClientBeforeIoC?.Invoke(); } + } + public override void Init() + { IoCManager.BuildGraph(); IoCManager.InjectDependencies(this); From 7221355110b9231ddd26c0028e4257838fe3e711 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Sun, 21 Sep 2025 15:20:43 +1200 Subject: [PATCH 3/7] Undo engine related changes --- .../Managers/ClientAdminManager.cs | 2 +- .../Administration/Managers/AdminManager.cs | 6 +- Content.Server/Maps/GridDraggingSystem.cs | 1 - Content.Server/Sandbox/SandboxSystem.cs | 1 - .../Administration/SharedAdminManager.cs | 15 +-- .../clientToolshedEngineCommandPerms.yml | 118 ------------------ 6 files changed, 10 insertions(+), 133 deletions(-) delete mode 100644 Resources/clientToolshedEngineCommandPerms.yml diff --git a/Content.Client/Administration/Managers/ClientAdminManager.cs b/Content.Client/Administration/Managers/ClientAdminManager.cs index e4547d792473e..d46a5149ec755 100644 --- a/Content.Client/Administration/Managers/ClientAdminManager.cs +++ b/Content.Client/Administration/Managers/ClientAdminManager.cs @@ -36,7 +36,7 @@ public override void ReloadCommandPermissions() public override void ReloadToolshedPermissions() { - base.ReloadToolshedPermissions(); + // base.ReloadToolshedPermissions(); if (ResMan.TryContentFileRead(new ResPath("/clientToolshedEngineCommandPerms.yml"), out var f)) ToolshedCommandPermissions.LoadPermissionsFromStream(f); diff --git a/Content.Server/Administration/Managers/AdminManager.cs b/Content.Server/Administration/Managers/AdminManager.cs index 5bd5c77625101..b2f0534948ed6 100644 --- a/Content.Server/Administration/Managers/AdminManager.cs +++ b/Content.Server/Administration/Managers/AdminManager.cs @@ -5,6 +5,7 @@ using Content.Shared.Administration; using Content.Shared.CCVar; using Content.Shared.Players; +using Robust.Server.Console; using Robust.Shared.Configuration; using Robust.Shared.Console; using Robust.Shared.Enums; @@ -13,12 +14,13 @@ namespace Content.Server.Administration.Managers { - public sealed partial class AdminManager : SharedAdminManager, IAdminManager + public sealed partial class AdminManager : SharedAdminManager, IAdminManager, IConGroupControllerImplementation { [Dependency] private readonly IServerDbManager _dbManager = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IServerNetManager _netMgr = default!; [Dependency] private readonly IChatManager _chat = default!; + [Dependency] private readonly IConGroupController _conGroup = default!; private readonly Dictionary _admins = new(); private readonly HashSet _promotedPlayers = new(); @@ -39,6 +41,8 @@ public override void Initialize() _netMgr.RegisterNetMessage(); PlayerMan.PlayerStatusChanged += PlayerStatusChanged; InitializeMetrics(); + _conGroup.Implementation = this; + Toolshed.ActivePermissionController = this; } public override AdminData? GetAdminData(ICommonSession session, bool includeDeAdmin = false) diff --git a/Content.Server/Maps/GridDraggingSystem.cs b/Content.Server/Maps/GridDraggingSystem.cs index 1e723608b6b57..cf71e012f48a6 100644 --- a/Content.Server/Maps/GridDraggingSystem.cs +++ b/Content.Server/Maps/GridDraggingSystem.cs @@ -1,6 +1,5 @@ using Content.Shared.Maps; using Robust.Server.Console; -using Robust.Shared.Console; using Robust.Shared.Utility; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Systems; diff --git a/Content.Server/Sandbox/SandboxSystem.cs b/Content.Server/Sandbox/SandboxSystem.cs index 15a5aed5fc5dc..30fe4e0fe95e3 100644 --- a/Content.Server/Sandbox/SandboxSystem.cs +++ b/Content.Server/Sandbox/SandboxSystem.cs @@ -12,7 +12,6 @@ using Robust.Server.Console; using Robust.Server.Placement; using Robust.Server.Player; -using Robust.Shared.Console; using Robust.Shared.Enums; using Robust.Shared.Player; using Robust.Shared.Prototypes; diff --git a/Content.Shared/Administration/SharedAdminManager.cs b/Content.Shared/Administration/SharedAdminManager.cs index 232b11d654699..66b3976c68657 100644 --- a/Content.Shared/Administration/SharedAdminManager.cs +++ b/Content.Shared/Administration/SharedAdminManager.cs @@ -9,7 +9,7 @@ namespace Content.Shared.Administration; -public abstract class SharedAdminManager : ISharedAdminManager, IConGroupControllerImplementation +public abstract class SharedAdminManager : ISharedAdminManager { [Dependency] protected readonly IConsoleHost ConsoleHost = default!; [Dependency] protected readonly ToolshedManager Toolshed = default!; @@ -17,7 +17,6 @@ public abstract class SharedAdminManager : ISharedAdminManager, IConGroupControl [Dependency] protected readonly IResourceManager ResMan = default!; [Dependency] protected readonly ISharedPlayerManager PlayerMan = default!; [Dependency] private readonly ILogManager _logManager = default!; - [Dependency] private readonly IConGroupController _conGroup = default!; protected readonly AdminCommandPermissions CommandPermissions = new(); protected readonly AdminCommandPermissions ToolshedCommandPermissions = new(); @@ -30,20 +29,17 @@ public virtual void Initialize() if (Initialized) throw new InvalidOperationException("Already initialized."); Initialized = true; - _conGroup.Implementation = this; - Toolshed.ActivePermissionController = this; Log = _logManager.GetSawmill("admin"); ReloadCommandPermissions(); ReloadToolshedPermissions(); - Toolshed.CommandsLoaded += ReloadToolshedPermissions; } public virtual void ReloadCommandPermissions() { CommandPermissions.Clear(); - foreach (var (cmdName, cmd) in ConsoleHost.RegisteredCommands) + foreach (var (cmdName, cmd) in ConsoleHost.AvailableCommands) { var (isAvail, flagsReq) = GetRequiredFlags(cmd); if (!isAvail) @@ -62,9 +58,6 @@ public virtual void ReloadCommandPermissions() public virtual void ReloadToolshedPermissions() { - if (!Toolshed.Started) - return; - ToolshedCommandPermissions.Clear(); foreach (var spec in Toolshed.DefaultEnvironment.AllCommands()) { @@ -172,8 +165,8 @@ protected virtual (bool isAvail, AdminFlags[] flagsReq) GetRequiredFlags(object // TODO FIX THIS // ConsoleHost.RegisteredCommand registered => registered.Callback.Method, - CommandSpec spec => spec.Cmd.GetType(), // Toolshed command - ToolshedProxyCommand proxy => proxy.Spec.Cmd.GetType(), // Toolshed command + CommandSpec spec => spec.Cmd.GetType(), + // ToolshedProxyCommand proxy => proxy.Spec.Cmd.GetType(), // Toolshed command _ => cmd.GetType() // Normal IConsoleCommand or some other object }; diff --git a/Resources/clientToolshedEngineCommandPerms.yml b/Resources/clientToolshedEngineCommandPerms.yml deleted file mode 100644 index 0f599247e3abd..0000000000000 --- a/Resources/clientToolshedEngineCommandPerms.yml +++ /dev/null @@ -1,118 +0,0 @@ -# Most server-side toolshed are arbitrarily locked behind the DEBUG permissions. -# This is largely a precaution to avoid allowing clients to somehow spam the sever with expensive math operations. -# But we allow most of these when running client-side: -- Commands: - - iota - - to - - select - - where - - count - - '=>' - - '?' - - 'or?' - - '??' - - rng - - self - - sum - - take - - join - - search - - first - - unique - - any - - contains - - isnull - - isempty - - stopwatch - - append - - min - - max - - average - - '+' - - '-' - - '*' - - '/' - - '%' - - '%/' - - '&~' - - bitornot - - '^~' - - '~' - - '<' - - '>' - - '<=' - - '>=' - - '==' - - '!=' - - '+/' - - '-/' - - '*/' - - '//' - - '&' - - bitor - - '^' - - neg - - abs - - bibytecount - - shortestbitlength - - countleadzeros - - counttrailingzeros - - fpi - - fe - - ftau - - fepsilon - - dpi - - de - - dtau - - depsilon - - hpi - - he - - htau - - hepsilon - - floor - - ceil - - round - - trunc - - round2frac - - exponentbytecount - - significandbytecount - - significandbitcount - - exponentshortestbitcount - - stepnext - - stepprev - - checkedto - - saturateto - - truncto - - iscanonical - - iscomplex - - iseven - - isodd - - isfinite - - isimaginary - - isinfinite - - isinteger - - isnan - - isnegative - - ispositive - - isreal - - issubnormal - - iszero - - pow - - sqrt - - cbrt - - root - - hypot - - sin - - sinpi - - asin - - asinpi - - cos - - cospi - - acos - - acospi - - tan - - tanpi - - atan - - atanpi - - pick - - tee From aa73f343f2232799c6e3db4cb680d7ca764a5df8 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Sun, 21 Sep 2025 15:44:04 +1200 Subject: [PATCH 4/7] cleanup --- Content.Client/Entry/EntryPoint.cs | 2 +- Content.Shared/Administration/AdminCommandPermissions.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 68b6782c66076..232a8dcda5f33 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -83,7 +83,7 @@ public override void PreInit() // Hence this needs to be called in PreInit, instead of in Init() foreach (var callback in TestingCallbacks) { - var cast = (ClientModuleTestingCallbacks)callback; + var cast = (ClientModuleTestingCallbacks) callback; cast.ClientBeforeIoC?.Invoke(); } } diff --git a/Content.Shared/Administration/AdminCommandPermissions.cs b/Content.Shared/Administration/AdminCommandPermissions.cs index 777e899de43c9..3fa35b081ced7 100644 --- a/Content.Shared/Administration/AdminCommandPermissions.cs +++ b/Content.Shared/Administration/AdminCommandPermissions.cs @@ -64,15 +64,15 @@ public bool CanCommand(string cmdName, AdminData? admin) return true; } - if (admin == null) + if (!AdminCommands.TryGetValue(cmdName, out var flagsReq)) { - // Player isn't an admin. + // Server-console only. return false; } - if (!AdminCommands.TryGetValue(cmdName, out var flagsReq)) + if (admin == null) { - // Server-console only. + // Player isn't an admin. return false; } From c2120339b635d3731cebe37a6b6c05e5fa3178ac Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Sun, 21 Sep 2025 15:55:45 +1200 Subject: [PATCH 5/7] remove toolshed --- Content.Server/Administration/Managers/AdminManager.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Content.Server/Administration/Managers/AdminManager.cs b/Content.Server/Administration/Managers/AdminManager.cs index b2f0534948ed6..740d4d923a0ea 100644 --- a/Content.Server/Administration/Managers/AdminManager.cs +++ b/Content.Server/Administration/Managers/AdminManager.cs @@ -242,10 +242,6 @@ private void UpdateAdminStatus(ICommonSession session) var commands = new List(CommandPermissions.AnyCommands); - // For the client, server-side toolshed commands are indistinguishable from "normal" server side commands. - // Hence, we just lump them together. - commands.AddRange(ToolshedCommandPermissions.AnyCommands); - if (_admins.TryGetValue(session, out var adminData)) { msg.Admin = adminData.Data; @@ -253,10 +249,6 @@ private void UpdateAdminStatus(ICommonSession session) commands.AddRange(CommandPermissions.AdminCommands .Where(p => p.Value.Any(f => adminData.Data.HasFlag(f))) .Select(p => p.Key)); - - commands.AddRange(ToolshedCommandPermissions.AdminCommands - .Where(p => p.Value.Any(f => adminData.Data.HasFlag(f))) - .Select(p => p.Key)); } msg.AvailableCommands = commands.ToArray(); From 4f0dff05c3780065402050b320b4d40bbced8d4f Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Sun, 21 Sep 2025 16:03:48 +1200 Subject: [PATCH 6/7] clarify comments --- Content.Server/Administration/Managers/AdminManager.cs | 3 +++ Content.Shared/Administration/SharedAdminManager.cs | 8 +------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Content.Server/Administration/Managers/AdminManager.cs b/Content.Server/Administration/Managers/AdminManager.cs index 740d4d923a0ea..3ecf5cbdb0486 100644 --- a/Content.Server/Administration/Managers/AdminManager.cs +++ b/Content.Server/Administration/Managers/AdminManager.cs @@ -431,6 +431,9 @@ protected override (bool isAvail, AdminFlags[] flagsReq) GetRequiredFlags(object if (cmd is not ConsoleHost.RegisteredCommand registered) return base.GetRequiredFlags(cmd); + // 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, []); diff --git a/Content.Shared/Administration/SharedAdminManager.cs b/Content.Shared/Administration/SharedAdminManager.cs index 66b3976c68657..c654a1d9c72bb 100644 --- a/Content.Shared/Administration/SharedAdminManager.cs +++ b/Content.Shared/Administration/SharedAdminManager.cs @@ -160,11 +160,6 @@ protected virtual (bool isAvail, AdminFlags[] flagsReq) GetRequiredFlags(object { var type = cmd switch { - // Method in somewhere that was explicitly registered via IConsoleHost.Register - // Alas, sandboxing prevents us from checking this. - // TODO FIX THIS - // ConsoleHost.RegisteredCommand registered => registered.Callback.Method, - CommandSpec spec => spec.Cmd.GetType(), // ToolshedProxyCommand proxy => proxy.Spec.Cmd.GetType(), // Toolshed command _ => cmd.GetType() // Normal IConsoleCommand or some other object @@ -181,8 +176,7 @@ protected virtual (bool isAvail, AdminFlags[] flagsReq) GetRequiredFlags(object .Select(p => p.Flags) .ToArray(); - // If attribs.length == 0 then no access attribute is specified, - // and this is a server-only command. + // If attribs.length == 0 then no access attribute is specified, meaning that this is a server-only command. return (attribs.Length != 0, attribs); } From 0546dd61b709224947b4007c50a4bfc379235050 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Tue, 23 Sep 2025 16:39:35 +1200 Subject: [PATCH 7/7] make interface methods explicit --- .../Managers/ISharedAdminManager.cs | 22 +++---------- .../Administration/SharedAdminManager.cs | 32 ++++++++++++++++--- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/Content.Shared/Administration/Managers/ISharedAdminManager.cs b/Content.Shared/Administration/Managers/ISharedAdminManager.cs index 00a0a0799ef34..e7056fe796031 100644 --- a/Content.Shared/Administration/Managers/ISharedAdminManager.cs +++ b/Content.Shared/Administration/Managers/ISharedAdminManager.cs @@ -43,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. @@ -59,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. @@ -75,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. @@ -90,10 +79,7 @@ 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 index c654a1d9c72bb..36e7ffc2d2acc 100644 --- a/Content.Shared/Administration/SharedAdminManager.cs +++ b/Content.Shared/Administration/SharedAdminManager.cs @@ -75,13 +75,9 @@ public virtual void ReloadToolshedPermissions() ToolshedCommandPermissions.LoadPermissionsFromStream(toolshedPerms); } - public bool IsAdmin(ICommonSession session, bool includeDeAdmin = false) - { - return GetAdminData(session, includeDeAdmin) != null; - } - public abstract AdminData? GetAdminData(ICommonSession session, bool includeDeAdmin = false); + public AdminData? GetAdminData(EntityUid uid, bool includeDeAdmin = false) { return PlayerMan.TryGetSessionByEntity(uid, out var session) @@ -180,6 +176,8 @@ protected virtual (bool isAvail, AdminFlags[] flagsReq) GetRequiredFlags(object return (attribs.Length != 0, attribs); } + #region Public Helpers + public bool CanViewVar(ICommonSession session) { return CanCommand(session, "vv"); @@ -204,6 +202,30 @@ 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