SboxSuperuser/Core/Permissions/ClaimMemberships.cs

Tracks access granted by claims in the engine's config/users.json, mapping claim names like superuser, admin and moderator to owner status or groups. Lives in memory only, so access is re-derived each time a player connects and dropped when they leave.

using SboxSuperuser.Core.Identity;

namespace SboxSuperuser.Core.Permissions;

/// <summary>
/// Access granted by the engine's config/users.json claims to connected players. It is live, never saved: removing
/// someone from users.json removes the access the next time they connect, and leaving removes it at once.
/// </summary>
internal sealed class ClaimMemberships
{
	/// <summary>Each claim Superuser understands and what it grants; null means owner.</summary>
	internal static readonly IReadOnlyDictionary<string, string> Grants = new Dictionary<string, string>( StringComparer.OrdinalIgnoreCase )
	{
		[ "superuser" ] = null, [ "owner" ] = null, [ "admin" ] = "admin", [ "moderator" ] = "moderator", [ "mod" ] = "moderator"
	};

	private readonly Dictionary<string, (bool Owner, string[] Groups)> _players = new( StringComparer.Ordinal );

	/// <summary>Raised whenever any player's claimed access changes, so cached decisions can be dropped.</summary>
	internal event Action Changed;

	/// <summary>Records what a player's claims grant; claims Superuser doesn't know are ignored.</summary>
	internal void Set( string steamId, IEnumerable<string> claims )
	{
		SteamIdentity.Validate( steamId, nameof( steamId ) );
		var owner = false;
		var groups = new SortedSet<string>( StringComparer.Ordinal );
		foreach ( var claim in claims )
		{
			if ( !Grants.TryGetValue( claim, out var group ) ) continue;
			if ( group is null ) owner = true;
			else groups.Add( group );
		}
		if ( !owner && groups.Count == 0 ) { Remove( steamId ); return; }
		_players[ steamId ] = (owner, groups.ToArray());
		Changed?.Invoke();
	}

	internal void Remove( string steamId )
	{
		if ( _players.Remove( steamId ) ) Changed?.Invoke();
	}

	internal bool IsOwner( string steamId ) => _players.TryGetValue( steamId, out var access ) && access.Owner;

	internal IReadOnlyList<string> GroupsOf( string steamId ) =>
		_players.TryGetValue( steamId, out var access ) ? access.Groups : Array.Empty<string>();
}