Files
tbd-station-14/Content.Server/Access/Systems/AccessOverriderSystem.cs
chromiumboy 2df70799f8 Add access configurator (#18638)
The access configurator programs the access levels of any access reader. To use the access configurator, players must:

- Insert an ID card
- Click a nearby entity with an access reader with the access configurator in hand
- Change the access list

Note that players only need one of the access levels listed on the device to lock/unlock it, but will only be able to alter access settings when they all of the access levels listed on the device

For example, an airlock which has 'Science' and 'Engineering' access listed can be opened by any player with either 'Science' or 'Engineering' access. However, to change the access settings on this airlock, a player must have both 'Science' and 'Engineering' access. This is to prevent people from easily breaking into secure areas with this tool, by adding one of their own access levels to the target device

Obviously, the most useful ID card to use with this tool is one with all access, since it can change the settings of any device. Removing all access requirements from a device will make it useable by anyone.

---------

Co-authored-by: Kevin Zheng <kevinz5000@gmail.com>
2023-08-08 10:30:46 -08:00

266 lines
10 KiB
C#

using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Interaction;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using System.Linq;
using static Content.Shared.Access.Components.AccessOverriderComponent;
using Content.Server.Popups;
using Content.Shared.DoAfter;
namespace Content.Server.Access.Systems;
[UsedImplicitly]
public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
{
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AccessOverriderComponent, WriteToTargetAccessReaderIdMessage>(OnWriteToTargetAccessReaderIdMessage);
SubscribeLocalEvent<AccessOverriderComponent, ComponentStartup>(UpdateUserInterface);
SubscribeLocalEvent<AccessOverriderComponent, EntInsertedIntoContainerMessage>(UpdateUserInterface);
SubscribeLocalEvent<AccessOverriderComponent, EntRemovedFromContainerMessage>(UpdateUserInterface);
SubscribeLocalEvent<AccessOverriderComponent, AfterInteractEvent>(AfterInteractOn);
SubscribeLocalEvent<AccessOverriderComponent, AccessOverriderDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<AccessOverriderComponent, BoundUIOpenedEvent>(UpdateUserInterface);
SubscribeLocalEvent<AccessOverriderComponent, BoundUIClosedEvent>(OnClose);
}
private void AfterInteractOn(EntityUid uid, AccessOverriderComponent component, AfterInteractEvent args)
{
if (args.Target == null || !TryComp(args.Target, out AccessReaderComponent? accessReader))
return;
if (!_interactionSystem.InRangeUnobstructed(args.User, (EntityUid) args.Target))
return;
var doAfterEventArgs = new DoAfterArgs(args.User, component.DoAfterTime, new AccessOverriderDoAfterEvent(), uid, target: args.Target, used: uid)
{
BreakOnTargetMove = true,
BreakOnUserMove = true,
BreakOnDamage = true,
NeedHand = true,
};
_doAfterSystem.TryStartDoAfter(doAfterEventArgs);
}
private void OnDoAfter(EntityUid uid, AccessOverriderComponent component, AccessOverriderDoAfterEvent args)
{
if (!TryComp(args.User, out ActorComponent? actor))
return;
if (args.Handled || args.Cancelled)
return;
if (args.Args.Target != null)
{
component.TargetAccessReaderId = args.Args.Target.Value;
_userInterface.TryOpen(uid, AccessOverriderUiKey.Key, actor.PlayerSession);
UpdateUserInterface(uid, component, args);
}
args.Handled = true;
}
private void OnClose(EntityUid uid, AccessOverriderComponent component, BoundUIClosedEvent args)
{
if (args.UiKey.Equals(AccessOverriderUiKey.Key))
{
component.TargetAccessReaderId = new();
}
}
private void OnWriteToTargetAccessReaderIdMessage(EntityUid uid, AccessOverriderComponent component, WriteToTargetAccessReaderIdMessage args)
{
if (args.Session.AttachedEntity is not { Valid: true } player)
return;
TryWriteToTargetAccessReaderId(uid, args.AccessList, player, component);
UpdateUserInterface(uid, component, args);
}
private void UpdateUserInterface(EntityUid uid, AccessOverriderComponent component, EntityEventArgs args)
{
if (!component.Initialized)
return;
var privilegedIdName = string.Empty;
var targetLabel = Loc.GetString("access-overrider-window-no-target");
var targetLabelColor = Color.Red;
string[]? possibleAccess = null;
string[]? currentAccess = null;
string[]? missingAccess = null;
if (component.TargetAccessReaderId is { Valid: true } accessReader)
{
targetLabel = Loc.GetString("access-overrider-window-target-label") + " " + EntityManager.GetComponent<MetaDataComponent>(component.TargetAccessReaderId).EntityName;
targetLabelColor = Color.White;
List<HashSet<string>> currentAccessHashsets = EntityManager.GetComponent<AccessReaderComponent>(accessReader).AccessLists;
currentAccess = ConvertAccessHashSetsToList(currentAccessHashsets)?.ToArray();
}
if (component.PrivilegedIdSlot.Item is { Valid: true } idCard)
{
privilegedIdName = EntityManager.GetComponent<MetaDataComponent>(idCard).EntityName;
if (component.TargetAccessReaderId is { Valid: true })
{
possibleAccess = _accessReader.FindAccessTags(idCard).ToArray();
}
if (currentAccess != null && possibleAccess != null)
{
missingAccess = currentAccess.Except(possibleAccess).ToArray();
}
}
AccessOverriderBoundUserInterfaceState newState;
newState = new AccessOverriderBoundUserInterfaceState(
component.PrivilegedIdSlot.HasItem,
PrivilegedIdIsAuthorized(uid, component),
currentAccess,
possibleAccess,
missingAccess,
privilegedIdName,
targetLabel,
targetLabelColor);
_userInterface.TrySetUiState(uid, AccessOverriderUiKey.Key, newState);
}
private List<string> ConvertAccessHashSetsToList(List<HashSet<string>> accessHashsets)
{
List<string> accessList = new List<string>();
if (accessHashsets != null && accessHashsets.Any())
{
foreach (HashSet<string> hashSet in accessHashsets)
{
foreach (string hash in hashSet.ToArray())
{
accessList.Add(hash);
}
}
}
return accessList;
}
private List<HashSet<string>> ConvertAccessListToHashSet(List<string> accessList)
{
List<HashSet<string>> accessHashsets = new List<HashSet<string>>();
if (accessList != null && accessList.Any())
{
foreach (string access in accessList)
{
accessHashsets.Add(new HashSet<string>() { access });
}
}
return accessHashsets;
}
/// <summary>
/// Called whenever an access button is pressed, adding or removing that access requirement from the target access reader.
/// </summary>
private void TryWriteToTargetAccessReaderId(EntityUid uid,
List<string> newAccessList,
EntityUid player,
AccessOverriderComponent? component = null)
{
if (!Resolve(uid, ref component) || component.TargetAccessReaderId is not { Valid: true })
return;
if (!PrivilegedIdIsAuthorized(uid, component))
return;
if (!_interactionSystem.InRangeUnobstructed(uid, component.TargetAccessReaderId))
{
_popupSystem.PopupEntity(Loc.GetString("access-overrider-out-of-range"), player, player);
return;
}
if (newAccessList.Count > 0 && !newAccessList.TrueForAll(x => component.AccessLevels.Contains(x)))
{
_sawmill.Warning($"User {ToPrettyString(uid)} tried to write unknown access tag.");
return;
}
TryComp(component.TargetAccessReaderId, out AccessReaderComponent? accessReader);
if (accessReader == null)
return;
var oldTags = ConvertAccessHashSetsToList(accessReader.AccessLists);
var privilegedId = component.PrivilegedIdSlot.Item;
if (oldTags.SequenceEqual(newAccessList))
return;
var difference = newAccessList.Union(oldTags).Except(newAccessList.Intersect(oldTags)).ToHashSet();
var privilegedPerms = _accessReader.FindAccessTags(privilegedId!.Value).ToHashSet();
if (!difference.IsSubsetOf(privilegedPerms))
{
_sawmill.Warning($"User {ToPrettyString(uid)} tried to modify permissions they could not give/take!");
return;
}
if (!oldTags.ToHashSet().IsSubsetOf(privilegedPerms))
{
_sawmill.Warning($"User {ToPrettyString(uid)} tried to modify permissions when they do not have sufficient access!");
_popupSystem.PopupEntity(Loc.GetString("access-overrider-cannot-modify-access"), player, player);
_audioSystem.PlayPvs(component.DenialSound, uid);
return;
}
var addedTags = newAccessList.Except(oldTags).Select(tag => "+" + tag).ToList();
var removedTags = oldTags.Except(newAccessList).Select(tag => "-" + tag).ToList();
_adminLogger.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(player):player} has modified {ToPrettyString(component.TargetAccessReaderId):entity} with the following allowed access level holders: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]");
accessReader.AccessLists = ConvertAccessListToHashSet(newAccessList);
Dirty(accessReader);
}
/// <summary>
/// Returns true if there is an ID in <see cref="AccessOverriderComponent.PrivilegedIdSlot"/> and said ID satisfies the requirements of <see cref="AccessReaderComponent"/>.
/// </summary>
/// <remarks>
/// Other code relies on the fact this returns false if privileged Id is null. Don't break that invariant.
/// </remarks>
private bool PrivilegedIdIsAuthorized(EntityUid uid, AccessOverriderComponent? component = null)
{
if (!Resolve(uid, ref component))
return true;
if (!EntityManager.TryGetComponent<AccessReaderComponent>(uid, out var reader))
return true;
var privilegedId = component.PrivilegedIdSlot.Item;
return privilegedId != null && _accessReader.IsAllowed(privilegedId.Value, reader);
}
}