SboxSuperuser/Engine/Admin/HostAdminQueries.Administration.cs

A partial class member of HostAdminQueries that builds read responses for the admin panel: about/module status, permitted command list, recent chat, and the permission policy. Owner-only gating is enforced on modules, diagnostics and permission policy edits, while chat needs admin.chat.read.

namespace SboxSuperuser.Engine.Admin;

internal sealed partial class HostAdminQueries
{
	private const string SecurityPermission = "admin.security.manage";

	private bool IsOwner( ActorIdentity actor ) => actor.Kind == ActorKind.Player && _policy.IsOwner( actor.SubjectId );

	// Module IDs and states match "admin modules list", and the status rows match "admin diagnostics"; both are owner-only.
	private AdminReply ReadAbout( Guid id, ActorIdentity actor )
	{
		var owner = IsOwner( actor );
		return new AdminReply
		{
			RequestId = id, Status = CommandStatus.Success, Message = "About Superuser.", ModulesAllowed = owner,
			Modules = owner
				? _modules().Select( status => new AdminModuleRow
				{
					Id = status.Descriptor.Id, ApiVersion = status.Descriptor.ApiVersion,
					State = status.State.ToString(), Message = status.Message
				} ).OrderBy( row => row.Id, StringComparer.Ordinal ).ToArray()
				: Array.Empty<AdminModuleRow>(),
			Data = owner ? _diagnostics().ToDictionary( pair => pair.Key, pair => pair.Value ) : new Dictionary<string, string>()
		};
	}

	// Same visibility as "admin commands": permitted commands, with security management limited to owners.
	private AdminReply ReadCommands( Guid id, ActorIdentity actor, Func<string, bool> can )
	{
		var owner = IsOwner( actor );
		var rows = _commands()
			.Where( command => can( command.Permission ) && (command.Permission != SecurityPermission || owner) )
			.Select( command => new AdminCommandRow
			{
				Name = command.Name, Description = command.Description, ModuleId = command.ModuleId,
				Mutation = command.ExecutionPolicy == CommandExecutionPolicy.Mutation,
				Arguments = command.Arguments.Select( argument => new AdminCommandArgumentRow
				{
					Name = argument.Name, Description = argument.Description, Kind = argument.Kind,
					Optional = argument.IsOptional, Remainder = argument.IsRemainder,
					Minimum = argument.Minimum, Maximum = argument.Maximum, Choices = argument.Choices.ToArray()
				} ).ToArray()
			} ).ToArray();
		return new AdminReply { RequestId = id, Status = CommandStatus.Success, Message = "Permitted commands.", Commands = rows };
	}

	private const string ChatPermission = "admin.chat.read";

	// Chat lines come from memory only and were already public; reading them still needs its own permission.
	private AdminReply ReadChat( Guid id, AdminQuery query, Func<string, bool> can )
	{
		if ( !can( ChatPermission ) ) return Failure( id, CommandStatus.Denied, "Reading recent chat requires admin.chat.read." );
		return new AdminReply
		{
			RequestId = id, Status = CommandStatus.Success, Message = "Recent chat.",
			Chat = _chat.Read( query.Search ).Select( line => new AdminChatRow
			{
				SteamId = line.SteamId, Name = line.Name, Message = line.Message, CreatedUtc = line.CreatedUtc
			} ).ToArray()
		};
	}

	// Policy edits are owner-only commands, so only owners see the policy they would edit.
	private AdminReply ReadPermissions( Guid id, ActorIdentity actor )
	{
		var policy = IsOwner( actor ) ? _policy.ReadPolicy( actor ) : null;
		if ( policy is null ) return Failure( id, CommandStatus.Denied, "Only owners can view and edit permissions." );

		static string[] With( IReadOnlyDictionary<string, int> entries, PermissionEffect effect ) => entries
			.Where( entry => entry.Value == (int)effect ).Select( entry => entry.Key ).Order( StringComparer.Ordinal ).ToArray();

		return new AdminReply
		{
			RequestId = id, Status = CommandStatus.Success, Message = "Permission policy.",
			Groups = policy.Groups.Values.OrderBy( group => group.Id, StringComparer.Ordinal ).Select( group => new AdminGroupRow
			{
				Id = group.Id, Parents = group.Parents.ToArray(), Protection = group.ProtectionLevel,
				Allowed = With( group.Entries, PermissionEffect.Allow ), Denied = With( group.Entries, PermissionEffect.Deny ),
				Members = policy.Users.Values.Where( user => user.Groups.Contains( group.Id ) )
					.Select( user => user.SteamId ).Order( StringComparer.Ordinal ).ToArray()
			} ).ToArray(),
			Users = policy.Users.Values.OrderBy( user => user.SteamId, StringComparer.Ordinal ).Select( user => new AdminUserRow
			{
				SteamId = user.SteamId, Groups = user.Groups.ToArray(),
				Allowed = With( user.Entries, PermissionEffect.Allow ), Denied = With( user.Entries, PermissionEffect.Deny )
			} ).ToArray(),
			PermissionKeys = _policy.RegisteredPermissions.Order( StringComparer.Ordinal ).ToArray()
		};
	}
}