1
0

AdminManager.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. using System.Diagnostics;
  2. using System.Linq;
  3. using System.Reflection;
  4. using System.Threading.Tasks;
  5. using Content.Server.Chat.Managers;
  6. using Content.Server.Database;
  7. using Content.Server.Players;
  8. using Content.Shared.Administration;
  9. using Content.Shared.CCVar;
  10. using Content.Shared.Info;
  11. using Content.Shared.Players;
  12. using Robust.Server.Console;
  13. using Robust.Server.Player;
  14. using Robust.Shared.Configuration;
  15. using Robust.Shared.Console;
  16. using Robust.Shared.ContentPack;
  17. using Robust.Shared.Enums;
  18. using Robust.Shared.Network;
  19. using Robust.Shared.Player;
  20. using Robust.Shared.Toolshed;
  21. using Robust.Shared.Toolshed.Errors;
  22. using Robust.Shared.Utility;
  23. namespace Content.Server.Administration.Managers
  24. {
  25. public sealed partial class AdminManager : IAdminManager, IPostInjectInit, IConGroupControllerImplementation
  26. {
  27. [Dependency] private readonly IPlayerManager _playerManager = default!;
  28. [Dependency] private readonly IServerDbManager _dbManager = default!;
  29. [Dependency] private readonly IConfigurationManager _cfg = default!;
  30. [Dependency] private readonly IServerNetManager _netMgr = default!;
  31. [Dependency] private readonly IConGroupController _conGroup = default!;
  32. [Dependency] private readonly IResourceManager _res = default!;
  33. [Dependency] private readonly IServerConsoleHost _consoleHost = default!;
  34. [Dependency] private readonly IChatManager _chat = default!;
  35. [Dependency] private readonly ToolshedManager _toolshed = default!;
  36. [Dependency] private readonly ILogManager _logManager = default!;
  37. private readonly Dictionary<ICommonSession, AdminReg> _admins = new();
  38. private readonly HashSet<NetUserId> _promotedPlayers = new();
  39. public event Action<AdminPermsChangedEventArgs>? OnPermsChanged;
  40. public IEnumerable<ICommonSession> ActiveAdmins => _admins
  41. .Where(p => p.Value.Data.Active)
  42. .Select(p => p.Key);
  43. public IEnumerable<ICommonSession> AllAdmins => _admins.Select(p => p.Key);
  44. private readonly AdminCommandPermissions _commandPermissions = new();
  45. private readonly AdminCommandPermissions _toolshedCommandPermissions = new();
  46. private ISawmill _sawmill = default!;
  47. public bool IsAdmin(ICommonSession session, bool includeDeAdmin = false)
  48. {
  49. return GetAdminData(session, includeDeAdmin) != null;
  50. }
  51. public AdminData? GetAdminData(ICommonSession session, bool includeDeAdmin = false)
  52. {
  53. if (_admins.TryGetValue(session, out var reg) && (reg.Data.Active || includeDeAdmin))
  54. {
  55. return reg.Data;
  56. }
  57. return null;
  58. }
  59. public AdminData? GetAdminData(EntityUid uid, bool includeDeAdmin = false)
  60. {
  61. if (_playerManager.TryGetSessionByEntity(uid, out var session))
  62. return GetAdminData(session, includeDeAdmin);
  63. return null;
  64. }
  65. public void DeAdmin(ICommonSession session)
  66. {
  67. if (!_admins.TryGetValue(session, out var reg))
  68. {
  69. throw new ArgumentException($"Player {session} is not an admin");
  70. }
  71. if (!reg.Data.Active)
  72. {
  73. return;
  74. }
  75. _chat.SendAdminAnnouncement(Loc.GetString("admin-manager-self-de-admin-message", ("exAdminName", session.Name)));
  76. _chat.DispatchServerMessage(session, Loc.GetString("admin-manager-became-normal-player-message"));
  77. UpdateDatabaseDeadminnedState(session, true);
  78. reg.Data.Active = false;
  79. SendPermsChangedEvent(session);
  80. UpdateAdminStatus(session);
  81. }
  82. private async void UpdateDatabaseDeadminnedState(ICommonSession player, bool newState)
  83. {
  84. try
  85. {
  86. // NOTE: This function gets called if you deadmin/readmin from a transient admin status.
  87. // (e.g. loginlocal)
  88. // In which case there may not be a database record.
  89. // The DB function handles this scenario fine, but it's worth noting.
  90. await _dbManager.UpdateAdminDeadminnedAsync(player.UserId, newState);
  91. }
  92. catch (Exception e)
  93. {
  94. _sawmill.Error("Failed to save deadmin state to database for {Admin}", player.UserId);
  95. }
  96. }
  97. public void Stealth(ICommonSession session)
  98. {
  99. if (!_admins.TryGetValue(session, out var reg))
  100. {
  101. throw new ArgumentException($"Player {session} is not an admin");
  102. }
  103. if (reg.Data.Stealth)
  104. return;
  105. var playerData = session.ContentData()!;
  106. playerData.Stealthed = true;
  107. reg.Data.Stealth = true;
  108. _chat.DispatchServerMessage(session, Loc.GetString("admin-manager-stealthed-message"));
  109. _chat.SendAdminAnnouncement(Loc.GetString("admin-manager-self-de-admin-message", ("exAdminName", session.Name)), AdminFlags.Stealth);
  110. _chat.SendAdminAnnouncement(Loc.GetString("admin-manager-self-enable-stealth", ("stealthAdminName", session.Name)), flagWhitelist: AdminFlags.Stealth);
  111. }
  112. public void UnStealth(ICommonSession session)
  113. {
  114. if (!_admins.TryGetValue(session, out var reg))
  115. {
  116. throw new ArgumentException($"Player {session} is not an admin");
  117. }
  118. if (!reg.Data.Stealth)
  119. return;
  120. var playerData = session.ContentData()!;
  121. playerData.Stealthed = false;
  122. reg.Data.Stealth = false;
  123. _chat.DispatchServerMessage(session, Loc.GetString("admin-manager-unstealthed-message"));
  124. _chat.SendAdminAnnouncement(Loc.GetString("admin-manager-self-re-admin-message", ("newAdminName", session.Name)), flagBlacklist: AdminFlags.Stealth);
  125. _chat.SendAdminAnnouncement(Loc.GetString("admin-manager-self-disable-stealth", ("exStealthAdminName", session.Name)), flagWhitelist: AdminFlags.Stealth);
  126. }
  127. public void ReAdmin(ICommonSession session)
  128. {
  129. if (!_admins.TryGetValue(session, out var reg))
  130. {
  131. throw new ArgumentException($"Player {session} is not an admin");
  132. }
  133. if (reg.Data.Active)
  134. {
  135. return;
  136. }
  137. _chat.DispatchServerMessage(session, Loc.GetString("admin-manager-became-admin-message"));
  138. UpdateDatabaseDeadminnedState(session, false);
  139. reg.Data.Active = true;
  140. if (!reg.Data.Stealth)
  141. {
  142. _chat.SendAdminAnnouncement(Loc.GetString("admin-manager-self-re-admin-message", ("newAdminName", session.Name)));
  143. }
  144. else
  145. {
  146. _chat.DispatchServerMessage(session, Loc.GetString("admin-manager-stealthed-message"));
  147. _chat.SendAdminAnnouncement(Loc.GetString("admin-manager-self-re-admin-message",
  148. ("newAdminName", session.Name)), flagWhitelist: AdminFlags.Stealth);
  149. }
  150. SendPermsChangedEvent(session);
  151. UpdateAdminStatus(session);
  152. }
  153. public async void ReloadAdmin(ICommonSession player)
  154. {
  155. var data = await LoadAdminData(player);
  156. var curAdmin = _admins.GetValueOrDefault(player);
  157. if (data == null && curAdmin == null)
  158. {
  159. // Wasn't admin before or after.
  160. return;
  161. }
  162. if (data == null)
  163. {
  164. // No longer admin.
  165. _admins.Remove(player);
  166. _chat.DispatchServerMessage(player, Loc.GetString("admin-manager-no-longer-admin-message"));
  167. }
  168. else
  169. {
  170. var (aData, rankId, special) = data.Value;
  171. if (curAdmin == null)
  172. {
  173. // Now an admin.
  174. var reg = new AdminReg(player, aData)
  175. {
  176. IsSpecialLogin = special,
  177. RankId = rankId
  178. };
  179. _admins.Add(player, reg);
  180. _chat.DispatchServerMessage(player, Loc.GetString("admin-manager-became-admin-message"));
  181. }
  182. else
  183. {
  184. // Perms changed.
  185. curAdmin.IsSpecialLogin = special;
  186. curAdmin.RankId = rankId;
  187. curAdmin.Data = aData;
  188. if (curAdmin.Data.Active)
  189. {
  190. aData.Active = true;
  191. _chat.DispatchServerMessage(player, Loc.GetString("admin-manager-admin-permissions-updated-message"));
  192. }
  193. }
  194. if (player.ContentData()!.Stealthed)
  195. {
  196. aData.Stealth = true;
  197. }
  198. }
  199. SendPermsChangedEvent(player);
  200. UpdateAdminStatus(player);
  201. }
  202. public void ReloadAdminsWithRank(int rankId)
  203. {
  204. foreach (var dat in _admins.Values.Where(p => p.RankId == rankId).ToArray())
  205. {
  206. ReloadAdmin(dat.Session);
  207. }
  208. }
  209. public void Initialize()
  210. {
  211. _sawmill = _logManager.GetSawmill("admin");
  212. _netMgr.RegisterNetMessage<MsgUpdateAdminStatus>();
  213. // Cache permissions for loaded console commands with the requisite attributes.
  214. foreach (var (cmdName, cmd) in _consoleHost.AvailableCommands)
  215. {
  216. var (isAvail, flagsReq) = GetRequiredFlag(cmd);
  217. if (!isAvail)
  218. {
  219. continue;
  220. }
  221. if (flagsReq.Length != 0)
  222. {
  223. _commandPermissions.AdminCommands.Add(cmdName, flagsReq);
  224. }
  225. else
  226. {
  227. _commandPermissions.AnyCommands.Add(cmdName);
  228. }
  229. }
  230. foreach (var spec in _toolshed.DefaultEnvironment.AllCommands())
  231. {
  232. var (isAvail, flagsReq) = GetRequiredFlag(spec.Cmd);
  233. if (!isAvail)
  234. {
  235. continue;
  236. }
  237. if (flagsReq.Length != 0)
  238. {
  239. _toolshedCommandPermissions.AdminCommands.TryAdd(spec.Cmd.Name, flagsReq);
  240. }
  241. else
  242. {
  243. _toolshedCommandPermissions.AnyCommands.Add(spec.Cmd.Name);
  244. }
  245. }
  246. // Load flags for engine commands, since those don't have the attributes.
  247. if (_res.TryContentFileRead(new ResPath("/engineCommandPerms.yml"), out var efs))
  248. {
  249. _commandPermissions.LoadPermissionsFromStream(efs);
  250. }
  251. if (_res.TryContentFileRead(new ResPath("/toolshedEngineCommandPerms.yml"), out var toolshedPerms))
  252. {
  253. _toolshedCommandPermissions.LoadPermissionsFromStream(toolshedPerms);
  254. }
  255. _toolshed.ActivePermissionController = this;
  256. InitializeMetrics();
  257. }
  258. public void PromoteHost(ICommonSession player)
  259. {
  260. _promotedPlayers.Add(player.UserId);
  261. ReloadAdmin(player);
  262. }
  263. void IPostInjectInit.PostInject()
  264. {
  265. _playerManager.PlayerStatusChanged += PlayerStatusChanged;
  266. _conGroup.Implementation = this;
  267. }
  268. // NOTE: Also sends commands list for non admins..
  269. private void UpdateAdminStatus(ICommonSession session)
  270. {
  271. var msg = new MsgUpdateAdminStatus();
  272. var commands = new List<string>(_commandPermissions.AnyCommands);
  273. if (_admins.TryGetValue(session, out var adminData))
  274. {
  275. msg.Admin = adminData.Data;
  276. commands.AddRange(_commandPermissions.AdminCommands
  277. .Where(p => p.Value.Any(f => adminData.Data.HasFlag(f)))
  278. .Select(p => p.Key));
  279. }
  280. msg.AvailableCommands = commands.ToArray();
  281. _netMgr.ServerSendMessage(msg, session.Channel);
  282. }
  283. private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e)
  284. {
  285. if (e.NewStatus == SessionStatus.Connected)
  286. {
  287. // Run this so that available commands list gets sent.
  288. UpdateAdminStatus(e.Session);
  289. }
  290. else if (e.NewStatus == SessionStatus.InGame)
  291. {
  292. LoginAdminMaybe(e.Session);
  293. }
  294. else if (e.NewStatus == SessionStatus.Disconnected)
  295. {
  296. if (_admins.Remove(e.Session, out var reg ) && _cfg.GetCVar(CCVars.AdminAnnounceLogout))
  297. {
  298. if (reg.Data.Stealth)
  299. {
  300. _chat.SendAdminAnnouncement(Loc.GetString("admin-manager-admin-logout-message",
  301. ("name", e.Session.Name)), flagWhitelist: AdminFlags.Stealth);
  302. }
  303. else
  304. {
  305. _chat.SendAdminAnnouncement(Loc.GetString("admin-manager-admin-logout-message",
  306. ("name", e.Session.Name)));
  307. }
  308. }
  309. }
  310. }
  311. private async void LoginAdminMaybe(ICommonSession session)
  312. {
  313. var adminDat = await LoadAdminData(session);
  314. if (adminDat == null)
  315. {
  316. // Not an admin.
  317. return;
  318. }
  319. var (dat, rankId, specialLogin) = adminDat.Value;
  320. var reg = new AdminReg(session, dat)
  321. {
  322. IsSpecialLogin = specialLogin,
  323. RankId = rankId
  324. };
  325. _admins.Add(session, reg);
  326. if (session.ContentData()!.Stealthed)
  327. reg.Data.Stealth = true;
  328. if (reg.Data.Active)
  329. {
  330. if (_cfg.GetCVar(CCVars.AdminAnnounceLogin))
  331. {
  332. if (reg.Data.Stealth)
  333. {
  334. _chat.DispatchServerMessage(session, Loc.GetString("admin-manager-stealthed-message"));
  335. _chat.SendAdminAnnouncement(Loc.GetString("admin-manager-admin-login-message",
  336. ("name", session.Name)), flagWhitelist: AdminFlags.Stealth);
  337. }
  338. else
  339. {
  340. _chat.SendAdminAnnouncement(Loc.GetString("admin-manager-admin-login-message",
  341. ("name", session.Name)));
  342. }
  343. }
  344. SendPermsChangedEvent(session);
  345. }
  346. UpdateAdminStatus(session);
  347. }
  348. private async Task<(AdminData dat, int? rankId, bool specialLogin)?> LoadAdminData(ICommonSession session)
  349. {
  350. var result = await LoadAdminDataCore(session);
  351. // Make sure admin didn't disconnect while data was loading.
  352. if (session.Status != SessionStatus.InGame)
  353. return null;
  354. return result;
  355. }
  356. private async Task<(AdminData dat, int? rankId, bool specialLogin)?> LoadAdminDataCore(ICommonSession session)
  357. {
  358. var promoteHost = IsLocal(session) && _cfg.GetCVar(CCVars.ConsoleLoginLocal)
  359. || _promotedPlayers.Contains(session.UserId)
  360. || session.Name == _cfg.GetCVar(CCVars.ConsoleLoginHostUser);
  361. if (promoteHost)
  362. {
  363. var data = new AdminData
  364. {
  365. Title = Loc.GetString("admin-manager-admin-data-host-title"),
  366. Flags = AdminFlagsHelper.Everything,
  367. Active = true,
  368. };
  369. return (data, null, true);
  370. }
  371. else
  372. {
  373. var dbData = await _dbManager.GetAdminDataForAsync(session.UserId);
  374. if (dbData == null)
  375. {
  376. // Not an admin!
  377. return null;
  378. }
  379. if (dbData.Suspended)
  380. {
  381. // Suspended admins don't count.
  382. return null;
  383. }
  384. var flags = AdminFlags.None;
  385. if (dbData.AdminRank != null)
  386. {
  387. flags = AdminFlagsHelper.NamesToFlags(dbData.AdminRank.Flags.Select(p => p.Flag));
  388. }
  389. foreach (var dbFlag in dbData.Flags)
  390. {
  391. var flag = AdminFlagsHelper.NameToFlag(dbFlag.Flag);
  392. if (dbFlag.Negative)
  393. {
  394. flags &= ~flag;
  395. }
  396. else
  397. {
  398. flags |= flag;
  399. }
  400. }
  401. var data = new AdminData
  402. {
  403. Flags = flags,
  404. Active = !dbData.Deadminned,
  405. };
  406. if (dbData.Title != null && _cfg.GetCVar(CCVars.AdminUseCustomNamesAdminRank))
  407. {
  408. data.Title = dbData.Title;
  409. }
  410. else if (dbData.AdminRank != null)
  411. {
  412. data.Title = dbData.AdminRank.Name;
  413. }
  414. return (data, dbData.AdminRankId, false);
  415. }
  416. }
  417. private static bool IsLocal(ICommonSession player)
  418. {
  419. var ep = player.Channel.RemoteEndPoint;
  420. var addr = ep.Address;
  421. if (addr.IsIPv4MappedToIPv6)
  422. {
  423. addr = addr.MapToIPv4();
  424. }
  425. return Equals(addr, System.Net.IPAddress.Loopback) || Equals(addr, System.Net.IPAddress.IPv6Loopback);
  426. }
  427. public bool TryGetCommandFlags(CommandSpec command, out AdminFlags[]? flags)
  428. {
  429. var cmdName = command.Cmd.Name;
  430. if (_toolshedCommandPermissions.AnyCommands.Contains(cmdName))
  431. {
  432. // Anybody can use this command.
  433. flags = null;
  434. return true;
  435. }
  436. if (_toolshedCommandPermissions.AdminCommands.TryGetValue(cmdName, out flags))
  437. {
  438. return true;
  439. }
  440. flags = null;
  441. return false;
  442. }
  443. public bool CanCommand(ICommonSession session, string cmdName)
  444. {
  445. if (_commandPermissions.AnyCommands.Contains(cmdName))
  446. {
  447. // Anybody can use this command.
  448. return true;
  449. }
  450. if (!_commandPermissions.AdminCommands.TryGetValue(cmdName, out var flagsReq))
  451. {
  452. // Server-console only.
  453. return false;
  454. }
  455. var data = GetAdminData(session);
  456. if (data == null)
  457. {
  458. // Player isn't an admin.
  459. return false;
  460. }
  461. foreach (var flagReq in flagsReq)
  462. {
  463. if (data.HasFlag(flagReq))
  464. {
  465. return true;
  466. }
  467. }
  468. return false;
  469. }
  470. public bool CheckInvokable(CommandSpec command, ICommonSession? user, out IConError? error)
  471. {
  472. if (user is null)
  473. {
  474. error = null;
  475. return true; // Server console.
  476. }
  477. var name = command.Cmd.Name;
  478. if (!TryGetCommandFlags(command, out var flags))
  479. {
  480. // Command is missing permissions.
  481. error = new CommandPermissionsUnassignedError(command);
  482. return false;
  483. }
  484. if (flags is null)
  485. {
  486. // Anyone can execute this.
  487. error = null;
  488. return true;
  489. }
  490. var data = GetAdminData(user);
  491. if (data == null)
  492. {
  493. // Player isn't an admin.
  494. error = new NoPermissionError(command);
  495. return false;
  496. }
  497. foreach (var flag in flags)
  498. {
  499. if (data.HasFlag(flag))
  500. {
  501. error = null;
  502. return true;
  503. }
  504. }
  505. error = new NoPermissionError(command);
  506. return false;
  507. }
  508. private static (bool isAvail, AdminFlags[] flagsReq) GetRequiredFlag(object cmd)
  509. {
  510. MemberInfo type = cmd.GetType();
  511. if (cmd is ConsoleHost.RegisteredCommand registered)
  512. {
  513. type = registered.Callback.Method;
  514. }
  515. if (Attribute.IsDefined(type, typeof(AnyCommandAttribute)))
  516. {
  517. // Available to everybody.
  518. return (true, Array.Empty<AdminFlags>());
  519. }
  520. var attribs = type.GetCustomAttributes(typeof(AdminCommandAttribute))
  521. .Cast<AdminCommandAttribute>()
  522. .Select(p => p.Flags)
  523. .ToArray();
  524. // If attribs.length == 0 then no access attribute is specified,
  525. // and this is a server-only command.
  526. return (attribs.Length != 0, attribs);
  527. }
  528. public bool CanViewVar(ICommonSession session)
  529. {
  530. return CanCommand(session, "vv");
  531. }
  532. public bool CanAdminPlace(ICommonSession session)
  533. {
  534. return GetAdminData(session)?.CanAdminPlace() ?? false;
  535. }
  536. public bool CanScript(ICommonSession session)
  537. {
  538. return GetAdminData(session)?.CanScript() ?? false;
  539. }
  540. public bool CanAdminMenu(ICommonSession session)
  541. {
  542. return GetAdminData(session)?.CanAdminMenu() ?? false;
  543. }
  544. public bool CanAdminReloadPrototypes(ICommonSession session)
  545. {
  546. return GetAdminData(session)?.CanAdminReloadPrototypes() ?? false;
  547. }
  548. private void SendPermsChangedEvent(ICommonSession session)
  549. {
  550. var flags = GetAdminData(session)?.Flags;
  551. OnPermsChanged?.Invoke(new AdminPermsChangedEventArgs(session, flags));
  552. }
  553. private sealed class AdminReg
  554. {
  555. public readonly ICommonSession Session;
  556. public AdminData Data;
  557. public int? RankId;
  558. // Such as console.loginlocal or promotehost
  559. public bool IsSpecialLogin;
  560. public AdminReg(ICommonSession session, AdminData data)
  561. {
  562. Data = data;
  563. Session = session;
  564. }
  565. }
  566. }
  567. }
  568. public record struct CommandPermissionsUnassignedError(CommandSpec Command) : IConError
  569. {
  570. public FormattedMessage DescribeInner()
  571. {
  572. return FormattedMessage.FromMarkupOrThrow($"The command {Command.FullName()} is missing permission flags and cannot be executed.");
  573. }
  574. public string? Expression { get; set; }
  575. public Vector2i? IssueSpan { get; set; }
  576. public StackTrace? Trace { get; set; }
  577. }
  578. public record struct NoPermissionError(CommandSpec Command) : IConError
  579. {
  580. public FormattedMessage DescribeInner()
  581. {
  582. return FormattedMessage.FromMarkupOrThrow($"You do not have permission to execute {Command.FullName()}");
  583. }
  584. public string? Expression { get; set; }
  585. public Vector2i? IssueSpan { get; set; }
  586. public StackTrace? Trace { get; set; }
  587. }