More GraphQL development

- Setup query structure for the potential for `RemoteGateway`s.
  - Add new `ErrorCode` indicating they are not implemented.
- Rename `ServerInformationBase` to `GatewayInformationBase`.
- Fix `TgsRestAuthorizeAttribute` lacking an `IAuthorizationFilter`.
This commit is contained in:
Jordan Dominion
2024-09-10 23:12:39 -04:00
parent 9ac92d1abc
commit ed2de22f79
23 changed files with 316 additions and 127 deletions
+3 -3
View File
@@ -5,10 +5,10 @@
<PropertyGroup>
<TgsCoreVersion>6.10.0</TgsCoreVersion>
<TgsConfigVersion>5.2.0</TgsConfigVersion>
<TgsApiVersion>10.9.0</TgsApiVersion>
<TgsApiVersion>10.10.0</TgsApiVersion>
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
<TgsApiLibraryVersion>15.0.0</TgsApiLibraryVersion>
<TgsClientVersion>18.0.0</TgsClientVersion>
<TgsApiLibraryVersion>16.0.0</TgsApiLibraryVersion>
<TgsClientVersion>19.0.0</TgsClientVersion>
<TgsDmapiVersion>7.3.0</TgsDmapiVersion>
<TgsInteropVersion>5.10.0</TgsInteropVersion>
<TgsHostWatchdogVersion>1.5.0</TgsHostWatchdogVersion>
+7 -1
View File
@@ -461,7 +461,7 @@ namespace Tgstation.Server.Api.Models
RepoTestMergeConflict,
/// <summary>
/// Attempted to create an instance outside of the <see cref="Internal.ServerInformationBase.ValidInstancePaths"/>.
/// Attempted to create an instance outside of the <see cref="Internal.GatewayInformationBase.ValidInstancePaths"/>.
/// </summary>
[Description("The new instance's path is not under a white-listed path.")]
InstanceNotAtWhitelistedPath,
@@ -663,5 +663,11 @@ namespace Tgstation.Server.Api.Models
/// </summary>
[Description("Provided repository username doesn't match the user of the corresponding access token!")]
RepoTokenUsernameMismatch,
/// <summary>
/// Attempted to make a cross swarm server request using the GraphQL API.
/// </summary>
[Description("GraphQL swarm remote gateways not implemented!")]
RemoteGatewaysNotImplemented,
}
}
@@ -5,7 +5,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// Information about the local tgstation-server.
/// </summary>
public class LocalServerInformation : ServerInformationBase
public class GatewayInformation : GatewayInformationBase
{
/// <summary>
/// If the server is running on a windows operating system.
@@ -5,7 +5,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// Base class for <see cref="Response.ServerInformationResponse"/>.
/// </summary>
public abstract class ServerInformationBase
public abstract class GatewayInformationBase
{
/// <summary>
/// Minimum length of database user passwords.
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// Information about a server in the swarm.
/// </summary>
public abstract class SwarmServer : IEquatable<SwarmServer>
public abstract class SwarmServer
{
/// <summary>
/// The public address of the server.
@@ -47,12 +47,5 @@ namespace Tgstation.Server.Api.Models.Internal
PublicAddress = copy.PublicAddress;
Identifier = copy.Identifier;
}
/// <inheritdoc />
public bool Equals(SwarmServer other)
=> other != null
&& other.Identifier == Identifier
&& other.PublicAddress == PublicAddress
&& other.Address == Address;
}
}
@@ -1,13 +1,11 @@
using System;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Api.Models.Response;
namespace Tgstation.Server.Api.Models.Internal
{
/// <summary>
/// Represents information about a running <see cref="SwarmServer"/>.
/// </summary>
public class SwarmServerInformation : SwarmServer, IEquatable<SwarmServerInformation>
public class SwarmServerInformation : SwarmServer
{
/// <summary>
/// If the <see cref="SwarmServerResponse"/> is the controller.
@@ -30,10 +28,5 @@ namespace Tgstation.Server.Api.Models.Internal
{
Controller = copy.Controller;
}
/// <inheritdoc />
public bool Equals(SwarmServerInformation other)
=> base.Equals(other)
&& other.Controller == Controller;
}
}
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Api.Models.Response
/// <summary>
/// Represents basic server information.
/// </summary>
public sealed class ServerInformationResponse : Internal.LocalServerInformation
public sealed class ServerInformationResponse : Internal.GatewayInformation
{
/// <summary>
/// The version of the host.
@@ -0,0 +1,38 @@
query ServerInformation {
swarm {
metadata {
apiVersion
dmApiVersion
updateInProgress
version
}
currentNode {
gateway {
information {
instanceLimit
minimumPasswordLength
userGroupLimit
userLimit
validInstancePaths
windowsHost
oAuthProviderInfos {
value {
clientId
redirectUri
serverUrl
}
key
}
}
}
}
nodes {
info {
address
controller
identifier
publicAddress
}
}
}
}
@@ -1,34 +0,0 @@
query ServerInformationQuery {
swarm {
metadata {
apiVersion
dmApiVersion
updateInProgress
version
}
localServer {
information {
instanceLimit
minimumPasswordLength
userGroupLimit
userLimit
validInstancePaths
windowsHost
oAuthProviderInfos {
key
value {
clientId
redirectUri
serverUrl
}
}
}
}
servers {
address
controller
identifier
publicAddress
}
}
}
@@ -16,7 +16,7 @@ namespace Tgstation.Server.Host.Configuration
/// <summary>
/// General configuration options.
/// </summary>
public sealed class GeneralConfiguration : ServerInformationBase
public sealed class GeneralConfiguration : GatewayInformationBase
{
/// <summary>
/// The key for the <see cref="Microsoft.Extensions.Configuration.IConfigurationSection"/> the <see cref="GeneralConfiguration"/> resides in.
@@ -29,22 +29,22 @@ namespace Tgstation.Server.Host.Configuration
public const ushort DefaultApiPort = 5000;
/// <summary>
/// The default value for <see cref="ServerInformationBase.MinimumPasswordLength"/>.
/// The default value for <see cref="GatewayInformationBase.MinimumPasswordLength"/>.
/// </summary>
const uint DefaultMinimumPasswordLength = 15;
/// <summary>
/// The default value for <see cref="ServerInformationBase.InstanceLimit"/>.
/// The default value for <see cref="GatewayInformationBase.InstanceLimit"/>.
/// </summary>
const uint DefaultInstanceLimit = 10;
/// <summary>
/// The default value for <see cref="ServerInformationBase.UserLimit"/>.
/// The default value for <see cref="GatewayInformationBase.UserLimit"/>.
/// </summary>
const uint DefaultUserLimit = 100;
/// <summary>
/// The default value for <see cref="ServerInformationBase.UserGroupLimit"/>.
/// The default value for <see cref="GatewayInformationBase.UserGroupLimit"/>.
/// </summary>
const uint DefaultUserGroupLimit = 25;
@@ -56,6 +56,7 @@ using Tgstation.Server.Host.Controllers.Results;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.GraphQL;
using Tgstation.Server.Host.GraphQL.Types;
using Tgstation.Server.Host.GraphQL.Types.Scalars;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
@@ -297,6 +298,8 @@ namespace Tgstation.Server.Host.Core
.AddAuthorization()
.AddMutationConventions()
.AddErrorFilter<ErrorMessageFilter>()
.AddType<LocalGateway>()
.AddType<RemoteGateway>()
.AddType<UnsignedIntType>()
.BindRuntimeType<Version, SemverType>()
.AddQueryType<Query>()
@@ -33,5 +33,14 @@ namespace Tgstation.Server.Host.GraphQL
ErrorCode = errorMessage.ErrorCode != default ? errorMessage.ErrorCode : null;
AdditionalData = errorMessage.AdditionalData;
}
/// <summary>
/// Initializes a new instance of the <see cref="ErrorMessageException"/> class.
/// </summary>
/// <param name="errorCode">The <see cref="ErrorCode"/>.</param>
public ErrorMessageException(ErrorCode errorCode)
: this(new ErrorMessageResponse(errorCode), String.Empty)
{
}
}
}
+3 -5
View File
@@ -1,7 +1,5 @@
#pragma warning disable CA1724
using Tgstation.Server.Host.GraphQL.Types;
namespace Tgstation.Server.Host.GraphQL
{
/// <summary>
@@ -10,9 +8,9 @@ namespace Tgstation.Server.Host.GraphQL
public sealed class Query
{
/// <summary>
/// Gets the <see cref="ServerSwarm"/>.
/// Gets the <see cref="Types.Swarm"/>.
/// </summary>
/// <returns>A new <see cref="ServerSwarm"/>.</returns>
public ServerSwarm Swarm() => new();
/// <returns>A new <see cref="Types.Swarm"/>.</returns>
public Types.Swarm Swarm() => new();
}
}
@@ -0,0 +1,31 @@
using HotChocolate;
using HotChocolate.Authorization;
using Microsoft.Extensions.Options;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Security.OAuth;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.GraphQL.Types
{
/// <summary>
/// Management interface for the parent <see cref="Node"/>.
/// </summary>
public interface IGateway
{
/// <summary>
/// Gets <see cref="GatewayInformation"/>.
/// </summary>
/// <param name="oAuthProviders">The <see cref="IOAuthProviders"/> to use.</param>
/// <param name="platformIdentifier">The <see cref="IPlatformIdentifier"/> to use.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptionsSnapshot{TOptions}"/> containing the <see cref="GeneralConfiguration"/> to use.</param>
/// <returns>A new <see cref="GatewayInformation"/>.</returns>
[AllowAnonymous]
GatewayInformation Information(
[Service] IOAuthProviders oAuthProviders,
[Service] IPlatformIdentifier platformIdentifier,
[Service] IOptionsSnapshot<GeneralConfiguration> generalConfigurationOptions);
}
}
@@ -1,9 +1,9 @@
using System;
using HotChocolate;
using HotChocolate.Authorization;
using Microsoft.Extensions.Options;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Security.OAuth;
@@ -12,19 +12,12 @@ using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.GraphQL.Types
{
/// <summary>
/// Represents the local tgstation-server.
/// <see cref="IGateway"/> for the <see cref="Node"/> this query is executing on.
/// </summary>
public sealed class LocalServer
public sealed class LocalGateway : IGateway
{
/// <summary>
/// Gets <see cref="LocalServerInformation"/>.
/// </summary>
/// <param name="oAuthProviders">The <see cref="IOAuthProviders"/> to use.</param>
/// <param name="platformIdentifier">The <see cref="IPlatformIdentifier"/> to use.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptionsSnapshot{TOptions}"/> containing the <see cref="GeneralConfiguration"/> to use.</param>
/// <returns>A new <see cref="LocalServerInformation"/>.</returns>
[AllowAnonymous]
public LocalServerInformation Information(
/// <inheritdoc />
public GatewayInformation Information(
[Service] IOAuthProviders oAuthProviders,
[Service] IPlatformIdentifier platformIdentifier,
[Service] IOptionsSnapshot<GeneralConfiguration> generalConfigurationOptions)
@@ -34,7 +27,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
ArgumentNullException.ThrowIfNull(generalConfigurationOptions);
var generalConfiguration = generalConfigurationOptions.Value;
return new LocalServerInformation
return new GatewayInformation
{
MinimumPasswordLength = generalConfiguration.MinimumPasswordLength,
InstanceLimit = generalConfiguration.InstanceLimit,
@@ -0,0 +1,47 @@
using System;
using HotChocolate;
using Microsoft.Extensions.Options;
using Tgstation.Server.Host.Configuration;
namespace Tgstation.Server.Host.GraphQL.Types
{
/// <summary>
/// Represents a node server in a swarm.
/// </summary>
public sealed class Node
{
/// <summary>
/// Gets the <see cref="NodeInformation"/>.
/// </summary>
public NodeInformation? Info { get; }
/// <summary>
/// Initializes a new instance of the <see cref="Node"/> class.
/// </summary>
/// <param name="info">The value of <see cref="Info"/>.</param>
public Node(NodeInformation? info)
{
Info = info;
}
/// <summary>
/// Gets the <see cref="Node"/>'s <see cref="IGateway"/>.
/// </summary>
/// <param name="swarmConfigurationOptions">The <see cref="IOptionsSnapshot{TOptions}"/> containing the current <see cref="SwarmConfiguration"/>.</param>
/// <returns>A new <see cref="IGateway"/>.</returns>
/// <remarks>The <see cref="Node"/>'s <see cref="IGateway"/>.</remarks>
public IGateway? Gateway([Service] IOptionsSnapshot<SwarmConfiguration> swarmConfigurationOptions)
{
ArgumentNullException.ThrowIfNull(swarmConfigurationOptions);
bool local = Info == null || Info.Identifier == swarmConfigurationOptions.Value.Identifier;
if (local)
return new LocalGateway();
return new RemoteGateway();
}
}
}
@@ -0,0 +1,47 @@
using System;
using HotChocolate.Types.Relay;
namespace Tgstation.Server.Host.GraphQL.Types
{
/// <summary>
/// Represent a server in the TGS server swarm.
/// </summary>
public sealed class NodeInformation
{
/// <summary>
/// The swarm server ID.
/// </summary>
[ID]
public string Identifier { get; }
/// <summary>
/// The swarm server's internal <see cref="Uri"/>.
/// </summary>
public Uri Address { get; }
/// <summary>
/// The swarm server's optional public address.
/// </summary>
public Uri? PublicAddress { get; }
/// <summary>
/// Whether or not the server is the swarm's controller.
/// </summary>
public bool Controller { get; }
/// <summary>
/// Initializes a new instance of the <see cref="NodeInformation"/> class.
/// </summary>
/// <param name="swarmServerInformation">The <see cref="Api.Models.Internal.SwarmServerInformation"/> to build from.</param>
public NodeInformation(Api.Models.Internal.SwarmServerInformation swarmServerInformation)
{
ArgumentNullException.ThrowIfNull(swarmServerInformation);
Identifier = swarmServerInformation.Identifier!;
Address = swarmServerInformation.Address!;
PublicAddress = swarmServerInformation.PublicAddress;
Controller = swarmServerInformation.Controller;
}
}
}
@@ -0,0 +1,34 @@
using System;
using HotChocolate;
using Microsoft.Extensions.Options;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Security.OAuth;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.GraphQL.Types
{
/// <summary>
/// <see cref="IGateway"/> for accessing remote <see cref="Node"/>s.
/// </summary>
/// <remarks>This is currently unimplemented.</remarks>
public sealed class RemoteGateway : IGateway
{
/// <inheritdoc />
public GatewayInformation Information(
[Service] IOAuthProviders oAuthProviders,
[Service] IPlatformIdentifier platformIdentifier,
[Service] IOptionsSnapshot<GeneralConfiguration> generalConfigurationOptions)
{
ArgumentNullException.ThrowIfNull(oAuthProviders);
ArgumentNullException.ThrowIfNull(platformIdentifier);
ArgumentNullException.ThrowIfNull(generalConfigurationOptions);
throw new ErrorMessageException(ErrorCode.RemoteGatewaysNotImplemented);
}
}
}
@@ -1,9 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using HotChocolate;
using Tgstation.Server.Api.Models.Internal;
using Microsoft.Extensions.Options;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Swarm;
using Tgstation.Server.Host.System;
@@ -13,7 +16,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
/// <summary>
/// Represents a tgstation-server swarm.
/// </summary>
public sealed class ServerSwarm
public sealed class Swarm
{
/// <summary>
/// Gets the <see cref="SwarmMetadata"/> for the swarm.
@@ -30,12 +33,6 @@ namespace Tgstation.Server.Host.GraphQL.Types
return new SwarmMetadata(assemblyInformationProvider, serverControl.UpdateInProgress);
}
/// <summary>
/// Gets the local <see cref="Types.LocalServer"/>.
/// </summary>
/// <returns>A new <see cref="Types.LocalServer"/>.</returns>
public LocalServer LocalServer() => new();
/// <summary>
/// Gets the swarm's <see cref="Types.Users"/>.
/// </summary>
@@ -43,15 +40,35 @@ namespace Tgstation.Server.Host.GraphQL.Types
public Users Users() => new();
/// <summary>
/// Gets the <see cref="SwarmServerInformation"/> for all servers in a swarm.
/// Gets the connected <see cref="Node"/> server.
/// </summary>
/// <param name="swarmService">The <see cref="ISwarmService"/> to use.</param>
/// <returns>A <see cref="List{T}"/> of <see cref="SwarmServerInformation"/>s if the local server is part of a swarm, <see langword="null"/> otherwise.</returns>
public List<SwarmServerInformation>? Servers(
/// <param name="swarmConfigurationOptions">The <see cref="IOptionsSnapshot{TOptions}"/> containing the current <see cref="SwarmConfiguration"/>.</param>
/// <returns>A new <see cref="Node"/>.</returns>
public Node CurrentNode(
[Service] ISwarmService swarmService,
[Service] IOptionsSnapshot<SwarmConfiguration> swarmConfigurationOptions)
{
ArgumentNullException.ThrowIfNull(swarmService);
ArgumentNullException.ThrowIfNull(swarmConfigurationOptions);
var nodeInfos = Nodes(swarmService);
if (nodeInfos != null)
return nodeInfos.First(x => x.Info!.Identifier == swarmConfigurationOptions.Value.Identifier);
return new Node(null);
}
/// <summary>
/// Gets all <see cref="Node"/> servers in the swarm.
/// </summary>
/// <param name="swarmService">The <see cref="ISwarmService"/> to use.</param>
/// <returns>A <see cref="List{T}"/> of <see cref="Node"/>s if the local server is part of a swarm, <see langword="null"/> otherwise.</returns>
public List<Node>? Nodes(
[Service] ISwarmService swarmService)
{
ArgumentNullException.ThrowIfNull(swarmService);
return swarmService.GetSwarmServers();
return swarmService.GetSwarmServers()?.Select(x => new Node(new NodeInformation(x))).ToList();
}
}
}
@@ -7,7 +7,7 @@ using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.GraphQL.Types
{
/// <summary>
/// Represents information that is constant across all servers in a <see cref="ServerSwarm"/>.
/// Represents information that is constant across all servers in a <see cref="Swarm"/>.
/// </summary>
public sealed class SwarmMetadata
{
@@ -23,6 +23,32 @@ namespace Tgstation.Server.Host.Security
/// </summary>
public RightsType? RightsType { get; }
/// <summary>
/// Implementation of <see cref="IAuthorizationFilter.OnAuthorization(AuthorizationFilterContext)"/>.
/// </summary>
/// <param name="context">The <see cref="AuthorizationFilterContext"/>.</param>
public static void OnAuthorizationHelper(AuthorizationFilterContext context)
{
ArgumentNullException.ThrowIfNull(context);
var services = context.HttpContext.RequestServices;
var authenticationContext = services.GetRequiredService<IAuthenticationContext>();
var logger = services.GetRequiredService<ILogger<TgsAuthorizeAttribute>>();
if (!authenticationContext.Valid)
{
logger.LogTrace("authenticationContext is invalid!");
context.Result = new UnauthorizedResult();
return;
}
if (authenticationContext.User.Require(x => x.Enabled))
return;
logger.LogTrace("authenticationContext is for a disabled user!");
context.Result = new ForbidResult();
}
/// <summary>
/// Initializes a new instance of the <see cref="TgsAuthorizeAttribute"/> class.
/// </summary>
@@ -122,23 +148,6 @@ namespace Tgstation.Server.Host.Security
/// <inheritdoc />
public void OnAuthorization(AuthorizationFilterContext context)
{
var services = context.HttpContext.RequestServices;
var authenticationContext = services.GetRequiredService<IAuthenticationContext>();
var logger = services.GetRequiredService<ILogger<TgsAuthorizeAttribute>>();
if (!authenticationContext.Valid)
{
logger.LogTrace("authenticationContext is invalid!");
context.Result = new UnauthorizedResult();
return;
}
if (authenticationContext.User.Require(x => x.Enabled))
return;
logger.LogTrace("authenticationContext is for a disabled user!");
context.Result = new ForbidResult();
}
=> OnAuthorizationHelper(context);
}
}
@@ -2,6 +2,7 @@
using System.Reflection;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
using Tgstation.Server.Host.Authority.Core;
@@ -12,7 +13,7 @@ namespace Tgstation.Server.Host.Security
/// </summary>
/// <typeparam name="TAuthority">The <see cref="IAuthority"/> being wrapped.</typeparam>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public sealed class TgsRestAuthorizeAttribute<TAuthority> : AuthorizeAttribute
public sealed class TgsRestAuthorizeAttribute<TAuthority> : AuthorizeAttribute, IAuthorizationFilter
where TAuthority : IAuthority
{
/// <summary>
@@ -40,5 +41,9 @@ namespace Tgstation.Server.Host.Security
MethodName = methodName;
Roles = authorizeAttribute.Roles;
}
/// <inheritdoc />
public void OnAuthorization(AuthorizationFilterContext context)
=> TgsAuthorizeAttribute.OnAuthorizationHelper(context);
}
}
@@ -1380,27 +1380,27 @@ namespace Tgstation.Server.Tests.Live
await multiClient.ExecuteReadOnlyConfirmEquivalence(
restClient => restClient.ServerInformation(cancellationToken),
async gqlClient => (await gqlClient.ServerInformationQuery.ExecuteAsync(cancellationToken)).Data,
async gqlClient => (await gqlClient.ServerInformation.ExecuteAsync(cancellationToken)).Data,
(restServerInfo, gqlServerInfo) => restServerInfo.UpdateInProgress == gqlServerInfo.Swarm.Metadata.UpdateInProgress
&& restServerInfo.Version == gqlServerInfo.Swarm.Metadata.Version
&& restServerInfo.DMApiVersion == gqlServerInfo.Swarm.Metadata.DmApiVersion
&& restServerInfo.InstanceLimit == gqlServerInfo.Swarm.LocalServer.Information.InstanceLimit
&& restServerInfo.UserGroupLimit == gqlServerInfo.Swarm.LocalServer.Information.UserGroupLimit
&& restServerInfo.ValidInstancePaths.SequenceEqual(gqlServerInfo.Swarm.LocalServer.Information.ValidInstancePaths)
&& restServerInfo.UserLimit == gqlServerInfo.Swarm.LocalServer.Information.UserLimit
&& restServerInfo.MinimumPasswordLength == gqlServerInfo.Swarm.LocalServer.Information.MinimumPasswordLength
&& (restServerInfo.SwarmServers == gqlServerInfo.Swarm.Servers
|| restServerInfo.SwarmServers.SequenceEqual(gqlServerInfo.Swarm.Servers.Select(x => new SwarmServerResponse(new Api.Models.Internal.SwarmServerInformation
&& restServerInfo.InstanceLimit == gqlServerInfo.Swarm.CurrentNode.Gateway.Information.InstanceLimit
&& restServerInfo.UserGroupLimit == gqlServerInfo.Swarm.CurrentNode.Gateway.Information.UserGroupLimit
&& restServerInfo.ValidInstancePaths.SequenceEqual(gqlServerInfo.Swarm.CurrentNode.Gateway.Information.ValidInstancePaths)
&& restServerInfo.UserLimit == gqlServerInfo.Swarm.CurrentNode.Gateway.Information.UserLimit
&& restServerInfo.MinimumPasswordLength == gqlServerInfo.Swarm.CurrentNode.Gateway.Information.MinimumPasswordLength
&& ((object)restServerInfo.SwarmServers == gqlServerInfo.Swarm.Nodes
|| restServerInfo.SwarmServers.SequenceEqual(gqlServerInfo.Swarm.Nodes.Select(x => new SwarmServerResponse(new Api.Models.Internal.SwarmServerInformation
{
Address = x.Address,
PublicAddress = x.PublicAddress,
Controller = x.Controller,
Identifier = x.Identifier,
Address = x.Info.Address,
PublicAddress = x.Info.PublicAddress,
Controller = x.Info.Controller,
Identifier = x.Info.Identifier,
}))))
&& (restServerInfo.OAuthProviderInfos == gqlServerInfo.Swarm.LocalServer.Information.OAuthProviderInfos
&& (restServerInfo.OAuthProviderInfos == gqlServerInfo.Swarm.CurrentNode.Gateway.Information.OAuthProviderInfos
|| restServerInfo.OAuthProviderInfos.All(kvp =>
{
var info = gqlServerInfo.Swarm.LocalServer.Information.OAuthProviderInfos.FirstOrDefault(x => (int)x.Key == (int)kvp.Key);
var info = gqlServerInfo.Swarm.CurrentNode.Gateway.Information.OAuthProviderInfos.FirstOrDefault(x => (int)x.Key == (int)kvp.Key);
return info != null
&& info.Value.ServerUrl == kvp.Value.ServerUrl
&& info.Value.ClientId == kvp.Value.ClientId