API surface restructuring for GraphQL. Lockdown some API information that wasn't present previously. Validate nodes can be resolved

This commit is contained in:
Jordan Dominion
2024-09-13 23:57:36 -04:00
parent d1434112f6
commit 30f3bd58f2
22 changed files with 447 additions and 330 deletions
+1 -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.GatewayInformationBase.ValidInstancePaths"/>.
/// Attempted to create an instance outside of the <see cref="Internal.ServerInformationBase.ValidInstancePaths"/>.
/// </summary>
[Description("The new instance's path is not under a white-listed path.")]
InstanceNotAtWhitelistedPath,
@@ -1,20 +0,0 @@
using System.Collections.Generic;
namespace Tgstation.Server.Api.Models.Internal
{
/// <summary>
/// Information about the local tgstation-server.
/// </summary>
public class GatewayInformation : GatewayInformationBase
{
/// <summary>
/// If the server is running on a windows operating system.
/// </summary>
public bool WindowsHost { get; set; }
/// <summary>
/// Map of <see cref="OAuthProvider"/> to the <see cref="OAuthProviderInfo"/> for them.
/// </summary>
public Dictionary<OAuthProvider, OAuthProviderInfo>? OAuthProviderInfos { get; set; }
}
}
@@ -5,7 +5,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// Base class for <see cref="Response.ServerInformationResponse"/>.
/// </summary>
public abstract class GatewayInformationBase
public abstract class ServerInformationBase
{
/// <summary>
/// Minimum length of database user passwords.
@@ -31,6 +31,6 @@ namespace Tgstation.Server.Api.Models.Internal
/// Limits the locations instances may be created or attached from.
/// </summary>
[ResponseOptions]
public ICollection<string>? ValidInstancePaths { get; set; }
public List<string>? ValidInstancePaths { get; set; }
}
}
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Api.Models.Response
/// <summary>
/// Represents basic server information.
/// </summary>
public sealed class ServerInformationResponse : Internal.GatewayInformation
public sealed class ServerInformationResponse : Internal.ServerInformationBase
{
/// <summary>
/// The version of the host.
@@ -23,6 +23,16 @@ namespace Tgstation.Server.Api.Models.Response
/// </summary>
public Version? DMApiVersion { get; set; }
/// <summary>
/// If the server is running on a windows operating system.
/// </summary>
public bool WindowsHost { get; set; }
/// <summary>
/// Map of <see cref="OAuthProvider"/> to the <see cref="OAuthProviderInfo"/> for them.
/// </summary>
public Dictionary<OAuthProvider, OAuthProviderInfo>? OAuthProviderInfos { get; set; }
/// <summary>
/// If there is a server update in progress.
/// </summary>
@@ -1,38 +0,0 @@
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
}
}
}
}
@@ -0,0 +1,19 @@
query UnauthenticatedServerInformation {
swarm {
currentNode {
gateway {
information {
majorApiVersion
oAuthProviderInfos {
key
value {
clientId
redirectUri
serverUrl
}
}
}
}
}
}
}
@@ -16,7 +16,7 @@ namespace Tgstation.Server.Host.Configuration
/// <summary>
/// General configuration options.
/// </summary>
public sealed class GeneralConfiguration : GatewayInformationBase
public sealed class GeneralConfiguration : ServerInformationBase
{
/// <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="GatewayInformationBase.MinimumPasswordLength"/>.
/// The default value for <see cref="ServerInformationBase.MinimumPasswordLength"/>.
/// </summary>
const uint DefaultMinimumPasswordLength = 15;
/// <summary>
/// The default value for <see cref="GatewayInformationBase.InstanceLimit"/>.
/// The default value for <see cref="ServerInformationBase.InstanceLimit"/>.
/// </summary>
const uint DefaultInstanceLimit = 10;
/// <summary>
/// The default value for <see cref="GatewayInformationBase.UserLimit"/>.
/// The default value for <see cref="ServerInformationBase.UserLimit"/>.
/// </summary>
const uint DefaultUserLimit = 100;
/// <summary>
/// The default value for <see cref="GatewayInformationBase.UserGroupLimit"/>.
/// The default value for <see cref="ServerInformationBase.UserGroupLimit"/>.
/// </summary>
const uint DefaultUserGroupLimit = 25;
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Globalization;
@@ -57,7 +57,6 @@ 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.Interceptors;
using Tgstation.Server.Host.GraphQL.Types.Scalars;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
@@ -297,7 +296,10 @@ namespace Tgstation.Server.Host.Core
services
.AddGraphQLServer(disableCostAnalyzer: true)
.AddAuthorization()
.TryAddTypeInterceptor<PublicAuthorizeDirectiveTypeInterceptor>()
.ModifyOptions(options =>
{
options.EnsureAllNodesCanBeResolved = true;
})
.AddMutationConventions()
.AddGlobalObjectIdentification()
.AddQueryFieldToMutationPayloads()
@@ -314,6 +316,7 @@ namespace Tgstation.Server.Host.Core
.AddSorting()
.AddHostTypes()
.AddErrorFilter<ErrorMessageFilter>()
.AddType<StandaloneNode>()
.AddType<LocalGateway>()
.AddType<RemoteGateway>()
.AddType<GraphQL.Types.UserName>()
@@ -1,12 +1,4 @@
using HotChocolate;
using HotChocolate.Authorization;
using Microsoft.Extensions.Options;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.GraphQL.Types;
using Tgstation.Server.Host.Security.OAuth;
using Tgstation.Server.Host.System;
using Tgstation.Server.Host.GraphQL.Types;
namespace Tgstation.Server.Host.GraphQL.Interfaces
{
@@ -18,14 +10,7 @@ namespace Tgstation.Server.Host.GraphQL.Interfaces
/// <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);
/// <returns>The <see cref="GatewayInformation"/> for the <see cref="IGateway"/>.</returns>
GatewayInformation Information();
}
}
@@ -0,0 +1,21 @@
using HotChocolate;
using Microsoft.Extensions.Options;
using Tgstation.Server.Host.Configuration;
namespace Tgstation.Server.Host.GraphQL.Interfaces
{
/// <summary>
/// Represents a tgstation-server installation.
/// </summary>
public interface IServerNode
{
/// <summary>
/// Access the <see cref="IGateway"/> for the <see cref="IServerNode"/>.
/// </summary>
/// <param name="swarmConfigurationOptions">The <see cref="IOptionsSnapshot{TOptions}"/> of <see cref="SwarmConfiguration"/>.</param>
/// <returns>The <see cref="IGateway"/> for the <see cref="IServerNode"/>.</returns>
public IGateway Gateway([Service] IOptionsSnapshot<SwarmConfiguration> swarmConfigurationOptions);
}
}
@@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using HotChocolate;
using Microsoft.Extensions.Options;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Components.Interop;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Properties;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.Security.OAuth;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.GraphQL.Types
{
/// <summary>
/// Represents information about a <see cref="SwarmNode"/> retrieved via a <see cref="Interfaces.IGateway"/>.
/// </summary>
public sealed class GatewayInformation
{
/// <summary>
/// Gets the minimum valid password length for TGS users.
/// </summary>
/// <param name="generalConfigurationOptions">The <see cref="IOptionsSnapshot{TOptions}"/> containing the <see cref="GeneralConfiguration"/>.</param>
/// <returns>A <see cref="uint"/> specifying the minimumn valid password length for TGS users.</returns>
[TgsGraphQLAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword)]
public uint? MinimumPasswordLength(
[Service] IOptionsSnapshot<GeneralConfiguration> generalConfigurationOptions)
{
ArgumentNullException.ThrowIfNull(generalConfigurationOptions);
return generalConfigurationOptions.Value.MinimumPasswordLength;
}
/// <summary>
/// Gets the maximum allowed attached instances for the <see cref="SwarmNode"/>.
/// </summary>
/// <param name="generalConfigurationOptions">The <see cref="IOptionsSnapshot{TOptions}"/> containing the <see cref="GeneralConfiguration"/>.</param>
/// <returns>A <see cref="uint"/> specifying the maximum allowed attached instances for the <see cref="SwarmNode"/>.</returns>
[TgsGraphQLAuthorize(InstanceManagerRights.Create)]
public uint? InstanceLimit(
[Service] IOptionsSnapshot<GeneralConfiguration> generalConfigurationOptions)
{
ArgumentNullException.ThrowIfNull(generalConfigurationOptions);
return generalConfigurationOptions.Value.InstanceLimit;
}
/// <summary>
/// Gets the maximum allowed registered <see cref="User"/>s for the <see cref="ServerSwarm"/>.
/// </summary>
/// <param name="generalConfigurationOptions">The <see cref="IOptionsSnapshot{TOptions}"/> containing the <see cref="GeneralConfiguration"/>.</param>
/// <returns>A <see cref="uint"/> specifying the maximum allowed registered users for the <see cref="ServerSwarm"/>.</returns>
/// <remarks>This limit only applies to user creation attempts made via the current <see cref="SwarmNode"/>.</remarks>
[TgsGraphQLAuthorize(AdministrationRights.WriteUsers)]
public uint? UserLimit(
[Service] IOptionsSnapshot<GeneralConfiguration> generalConfigurationOptions)
{
ArgumentNullException.ThrowIfNull(generalConfigurationOptions);
return generalConfigurationOptions.Value.UserLimit;
}
/// <summary>
/// Gets the maximum allowed registered <see cref="UserGroup"/>s for the <see cref="ServerSwarm"/>.
/// </summary>
/// <param name="generalConfigurationOptions">The <see cref="IOptionsSnapshot{TOptions}"/> containing the <see cref="GeneralConfiguration"/>.</param>
/// <returns>A <see cref="uint"/> specifying the maximum allowed registered <see cref="UserGroup"/>s for the <see cref="ServerSwarm"/>.</returns>
/// <remarks>This limit only applies to <see cref="UserGroup"/> creation attempts made via the current <see cref="SwarmNode"/>.</remarks>
[TgsGraphQLAuthorize(AdministrationRights.WriteUsers)]
public uint? UserGroupLimit(
[Service] IOptionsSnapshot<GeneralConfiguration> generalConfigurationOptions)
{
ArgumentNullException.ThrowIfNull(generalConfigurationOptions);
return generalConfigurationOptions.Value.UserGroupLimit;
}
/// <summary>
/// Gets the locations <see cref="Instance"/>s may be created or attached from if there are restrictions.
/// </summary>
/// <param name="generalConfigurationOptions">The <see cref="IOptionsSnapshot{TOptions}"/> containing the <see cref="GeneralConfiguration"/>.</param>
/// <returns>The locations <see cref="Instance"/>s may be created or attached from if there are restrictions, <see langword="null"/> otherwise.</returns>
[TgsGraphQLAuthorize(InstanceManagerRights.Create | InstanceManagerRights.Relocate)]
public IReadOnlyCollection<string>? ValidInstancePaths(
[Service] IOptionsSnapshot<GeneralConfiguration> generalConfigurationOptions)
{
ArgumentNullException.ThrowIfNull(generalConfigurationOptions);
return generalConfigurationOptions.Value.ValidInstancePaths;
}
/// <summary>
/// Gets a flag indicating whether or not the current <see cref="SwarmNode"/> runs on a Windows operating system.
/// </summary>
/// <param name="platformIdentifier">The <see cref="IPlatformIdentifier"/> to use.</param>
/// <returns><see langword="true"/> if the <see cref="SwarmNode"/> runs on a Windows operating system, <see langword="false"/> otherwise.</returns>
[TgsGraphQLAuthorize]
public bool? WindowsHost(
[Service] IPlatformIdentifier platformIdentifier)
{
ArgumentNullException.ThrowIfNull(platformIdentifier);
return platformIdentifier.IsWindows;
}
/// <summary>
/// Gets the swarm protocol <see cref="Version"/>.
/// </summary>
[TgsGraphQLAuthorize]
public Version? SwarmProtocolVersion => global::System.Version.Parse(MasterVersionsAttribute.Instance.RawSwarmProtocolVersion);
/// <summary>
/// Gets the <see cref="global::System.Version"/> of tgstation-server the <see cref="SwarmNode"/> is running.
/// </summary>
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> to use.</param>
/// <returns>The <see cref="global::System.Version"/> of tgstation-server the <see cref="SwarmNode"/> is running.</returns>
[TgsGraphQLAuthorize]
public Version? Version(
[Service] IAssemblyInformationProvider assemblyInformationProvider)
{
ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
return assemblyInformationProvider.Version;
}
/// <summary>
/// Gets the major HTTP API <see cref="global::System.Version"/> number of the <see cref="SwarmNode"/>.
/// </summary>
public int MajorApiVersion => ApiHeaders.Version.Major;
/// <summary>
/// Gets the HTTP API <see cref="global::System.Version"/> of the <see cref="SwarmNode"/>.
/// </summary>
[TgsGraphQLAuthorize]
public Version? ApiVersion => ApiHeaders.Version;
/// <summary>
/// Gets the DMAPI interop <see cref="global::System.Version"/> the <see cref="SwarmNode"/> uses.
/// </summary>
[TgsGraphQLAuthorize]
public Version? DMApiVersion => DMApiConstants.InteropVersion;
/// <summary>
/// Gets the information needed to perform open authentication with the <see cref="SwarmNode"/>.
/// </summary>
/// <param name="oAuthProviders">The <see cref="IOAuthProviders"/> to use.</param>
/// <returns>A map of enabled <see cref="OAuthProvider"/>s to their <see cref="OAuthProviderInfo"/>.</returns>
public IReadOnlyDictionary<OAuthProvider, OAuthProviderInfo> OAuthProviderInfos(
[Service] IOAuthProviders oAuthProviders)
{
ArgumentNullException.ThrowIfNull(oAuthProviders);
return oAuthProviders.ProviderInfos();
}
}
}
@@ -1,23 +0,0 @@
using HotChocolate.Configuration;
using HotChocolate.Types.Descriptors.Definitions;
namespace Tgstation.Server.Host.GraphQL.Types.Interceptors
{
/// <summary>
/// Makes the @authorize directive public (It is internal by default).
/// </summary>
public sealed class PublicAuthorizeDirectiveTypeInterceptor : TypeInterceptor
{
/// <inheritdoc />
public override void OnBeforeRegisterDependencies(ITypeDiscoveryContext discoveryContext, DefinitionBase definition)
{
if (definition is DirectiveTypeDefinition dtd
&& dtd.Name == "authorize")
{
dtd.IsPublic = true;
}
base.OnBeforeRegisterDependencies(discoveryContext, definition);
}
}
}
@@ -1,14 +1,4 @@
using System;
using HotChocolate;
using Microsoft.Extensions.Options;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.GraphQL.Interfaces;
using Tgstation.Server.Host.Security.OAuth;
using Tgstation.Server.Host.System;
using Tgstation.Server.Host.GraphQL.Interfaces;
namespace Tgstation.Server.Host.GraphQL.Types
{
@@ -18,26 +8,6 @@ namespace Tgstation.Server.Host.GraphQL.Types
public sealed class LocalGateway : IGateway
{
/// <inheritdoc />
public GatewayInformation Information(
[Service] IOAuthProviders oAuthProviders,
[Service] IPlatformIdentifier platformIdentifier,
[Service] IOptionsSnapshot<GeneralConfiguration> generalConfigurationOptions)
{
ArgumentNullException.ThrowIfNull(oAuthProviders);
ArgumentNullException.ThrowIfNull(platformIdentifier);
ArgumentNullException.ThrowIfNull(generalConfigurationOptions);
var generalConfiguration = generalConfigurationOptions.Value;
return new GatewayInformation
{
MinimumPasswordLength = generalConfiguration.MinimumPasswordLength,
InstanceLimit = generalConfiguration.InstanceLimit,
UserLimit = generalConfiguration.UserLimit,
UserGroupLimit = generalConfiguration.UserGroupLimit,
ValidInstancePaths = generalConfiguration.ValidInstancePaths,
WindowsHost = platformIdentifier.IsWindows,
OAuthProviderInfos = oAuthProviders.ProviderInfos(),
};
}
public GatewayInformation Information() => new();
}
}
@@ -1,73 +0,0 @@
using System;
using System.Linq;
using HotChocolate;
using HotChocolate.Types.Relay;
using Tgstation.Server.Host.Swarm;
namespace Tgstation.Server.Host.GraphQL.Types
{
/// <summary>
/// Represent a server in the TGS server swarm.
/// </summary>
[Node]
public sealed class NodeInformation
{
/// <summary>
/// Node resolver for <see cref="NodeInformation"/>s.
/// </summary>
/// <param name="identifier">The <see cref="Identifier"/> to lookup.</param>
/// <param name="swarmService">The <see cref="ISwarmService"/> to use.</param>
/// <returns>The queried <see cref="NodeInformation"/>, if present.</returns>
public NodeInformation? GetNodeInformation(
string identifier,
[Service] ISwarmService swarmService)
{
ArgumentNullException.ThrowIfNull(identifier);
ArgumentNullException.ThrowIfNull(swarmService);
var node = swarmService.GetSwarmServers()
?.FirstOrDefault(node => node.Identifier == identifier);
if (node == null)
return null;
return new NodeInformation(node);
}
/// <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;
}
}
}
@@ -1,7 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using HotChocolate.Types.Relay;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Host.GraphQL.Types
@@ -9,7 +7,6 @@ namespace Tgstation.Server.Host.GraphQL.Types
/// <summary>
/// Represents a set of permissions for the server.
/// </summary>
[Node]
public sealed class PermissionSet : Entity
{
/// <summary>
@@ -1,15 +1,6 @@
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.GraphQL.Interfaces;
using Tgstation.Server.Host.Security.OAuth;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.GraphQL.Types
{
@@ -20,16 +11,6 @@ namespace Tgstation.Server.Host.GraphQL.Types
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);
}
public GatewayInformation Information() => throw new NotImplementedException();
}
}
@@ -8,8 +8,10 @@ using Microsoft.Extensions.Options;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.GraphQL.Interfaces;
using Tgstation.Server.Host.Properties;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.Swarm;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.GraphQL.Types
{
@@ -19,56 +21,62 @@ namespace Tgstation.Server.Host.GraphQL.Types
public sealed class ServerSwarm
{
/// <summary>
/// Gets the <see cref="SwarmMetadata"/> for the swarm.
/// If there is a swarm update in progress.
/// </summary>
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> to use.</param>
/// <param name="serverControl">The <see cref="IServerControl"/> to use.</param>
/// <returns>A new <see cref="SwarmMetadata"/>.</returns>
public SwarmMetadata Metadata(
[Service] IAssemblyInformationProvider assemblyInformationProvider,
/// <returns><see langword="true"/> if there is an update in progress, <see langword="false"/> otherwise.</returns>
[TgsGraphQLAuthorize]
public bool UpdateInProgress(
[Service] IServerControl serverControl)
{
ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
ArgumentNullException.ThrowIfNull(serverControl);
return new SwarmMetadata(assemblyInformationProvider, serverControl.UpdateInProgress);
return serverControl.UpdateInProgress;
}
/// <summary>
/// Gets the swarm protocol major version in use.
/// </summary>
[TgsGraphQLAuthorize]
public int ProtocolMajorVersion => Version.Parse(MasterVersionsAttribute.Instance.RawSwarmProtocolVersion).Major;
/// <summary>
/// Gets the swarm's <see cref="Types.Users"/>.
/// </summary>
/// <returns>A new <see cref="Types.Users"/>.</returns>
[TgsGraphQLAuthorize]
public Users Users() => new();
/// <summary>
/// Gets the connected <see cref="SwarmNode"/> server.
/// </summary>
/// <param name="swarmService">The <see cref="ISwarmService"/> to use.</param>
/// <param name="swarmConfigurationOptions">The <see cref="IOptionsSnapshot{TOptions}"/> containing the current <see cref="SwarmConfiguration"/>.</param>
/// <returns>A new <see cref="SwarmNode"/>.</returns>
public SwarmNode CurrentNode(
[Service] ISwarmService swarmService,
[Service] IOptionsSnapshot<SwarmConfiguration> swarmConfigurationOptions)
/// <param name="swarmService">The <see cref="ISwarmService"/> to use.</param>
/// <returns>A new <see cref="SwarmNode"/> for the local node if it is part of a swarm, <see langword="null"/> otherwise.</returns>
public IServerNode CurrentNode(
[Service] IOptionsSnapshot<SwarmConfiguration> swarmConfigurationOptions,
[Service] ISwarmService swarmService)
{
ArgumentNullException.ThrowIfNull(swarmService);
ArgumentNullException.ThrowIfNull(swarmConfigurationOptions);
ArgumentNullException.ThrowIfNull(swarmService);
var nodeInfos = Nodes(swarmService);
if (nodeInfos != null)
return nodeInfos.First(x => x.Info!.Identifier == swarmConfigurationOptions.Value.Identifier);
var ourIdentifier = swarmConfigurationOptions.Value.Identifier;
if (ourIdentifier == null)
return new StandaloneNode();
return new SwarmNode(null);
return (IServerNode?)SwarmNode.GetSwarmNode(ourIdentifier, swarmService) ?? new StandaloneNode();
}
/// <summary>
/// Gets all <see cref="SwarmNode"/> servers in the swarm.
/// </summary>
/// <param name="swarmService">The <see cref="ISwarmService"/> to use.</param>
/// <returns>A <see cref="List{T}"/> of <see cref="SwarmNode"/>s if the local server is part of a swarm, <see langword="null"/> otherwise.</returns>
/// <returns>A <see cref="List{T}"/> of <see cref="SwarmNode"/>s if the local node is part of a swarm, <see langword="null"/> otherwise.</returns>
[TgsGraphQLAuthorize]
public List<SwarmNode>? Nodes(
[Service] ISwarmService swarmService)
{
ArgumentNullException.ThrowIfNull(swarmService);
return swarmService.GetSwarmServers()?.Select(x => new SwarmNode(new NodeInformation(x))).ToList();
return swarmService.GetSwarmServers()?.Select(x => new SwarmNode(x)).ToList();
}
}
}
@@ -0,0 +1,19 @@
using HotChocolate;
using Microsoft.Extensions.Options;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.GraphQL.Interfaces;
namespace Tgstation.Server.Host.GraphQL.Types
{
/// <summary>
/// A <see cref="IServerNode"/> not running as part of a larger <see cref="ServerSwarm"/>.
/// </summary>
public sealed class StandaloneNode : IServerNode
{
/// <inheritdoc />
public IGateway Gateway([Service] IOptionsSnapshot<SwarmConfiguration> swarmConfigurationOptions)
=> new LocalGateway();
}
}
@@ -1,46 +0,0 @@
using System;
using Tgstation.Server.Api;
using Tgstation.Server.Host.Components.Interop;
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"/>.
/// </summary>
public sealed class SwarmMetadata
{
/// <summary>
/// The version of the host.
/// </summary>
public Version Version { get; }
/// <summary>
/// The <see cref="Api"/> version of the host.
/// </summary>
public Version ApiVersion => ApiHeaders.Version;
/// <summary>
/// The DMAPI interop version the server uses.
/// </summary>
public Version DMApiVersion => DMApiConstants.InteropVersion;
/// <summary>
/// If there is a server update in progress.
/// </summary>
public bool UpdateInProgress { get; }
/// <summary>
/// Initializes a new instance of the <see cref="SwarmMetadata"/> class.
/// </summary>
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> used to derive the <see cref="Version"/>.</param>
/// <param name="updateInProgress">The value of <see cref="UpdateInProgress"/>.</param>
public SwarmMetadata(IAssemblyInformationProvider assemblyInformationProvider, bool updateInProgress)
{
ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
Version = assemblyInformationProvider.Version;
UpdateInProgress = updateInProgress;
}
}
}
@@ -1,32 +1,80 @@
using System;
using System.Linq;
using HotChocolate;
using HotChocolate.Types.Relay;
using Microsoft.Extensions.Options;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.GraphQL.Interfaces;
using Tgstation.Server.Host.Swarm;
namespace Tgstation.Server.Host.GraphQL.Types
{
/// <summary>
/// Represents a node server in a swarm.
/// </summary>
public sealed class SwarmNode
[Node(IdField = nameof(Identifier))]
public sealed class SwarmNode : IServerNode
{
/// <summary>
/// Gets the <see cref="NodeInformation"/>.
/// The swarm server ID.
/// </summary>
public NodeInformation? Info { get; }
[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>
/// Node resolver for <see cref="SwarmNode"/>s.
/// </summary>
/// <param name="identifier">The <see cref="Identifier"/>.</param>
/// <param name="swarmService">The <see cref="ISwarmService"/> to load from.</param>
/// <returns>A new <see cref="SwarmNode"/> with the matching <paramref name="identifier"/> if found, <see langword="null"/> otherwise.</returns>
public static SwarmNode? GetSwarmNode(
string identifier,
[Service] ISwarmService swarmService)
{
ArgumentNullException.ThrowIfNull(identifier);
ArgumentNullException.ThrowIfNull(swarmService);
var info = swarmService
.GetSwarmServers()
?.FirstOrDefault(x => x.Identifier == identifier);
if (info == null)
return null;
return new SwarmNode(info);
}
/// <summary>
/// Initializes a new instance of the <see cref="SwarmNode"/> class.
/// </summary>
/// <param name="info">The value of <see cref="Info"/>.</param>
public SwarmNode(NodeInformation? info)
/// <param name="nodeInformation">The <see cref="SwarmServerInformation"/> to use for initialization.</param>
public SwarmNode(SwarmServerInformation? nodeInformation)
{
Info = info;
ArgumentNullException.ThrowIfNull(nodeInformation);
Identifier = nodeInformation.Identifier!;
Address = nodeInformation.Address!;
PublicAddress = nodeInformation.PublicAddress;
Controller = nodeInformation.Controller;
}
/// <summary>
@@ -35,11 +83,11 @@ namespace Tgstation.Server.Host.GraphQL.Types
/// <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="SwarmNode"/>'s <see cref="IGateway"/>.</remarks>
public IGateway? Gateway([Service] IOptionsSnapshot<SwarmConfiguration> swarmConfigurationOptions)
public IGateway Gateway([Service] IOptionsSnapshot<SwarmConfiguration> swarmConfigurationOptions)
{
ArgumentNullException.ThrowIfNull(swarmConfigurationOptions);
bool local = Info == null || Info.Identifier == swarmConfigurationOptions.Value.Identifier;
bool local = Identifier == swarmConfigurationOptions.Value.Identifier;
if (local)
return new LocalGateway();
@@ -0,0 +1,118 @@
using System;
using HotChocolate.Authorization;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Host.Security
{
/// <summary>
/// Helper for using the <see cref="AuthorizeAttribute"/> with the <see cref="Api.Rights"/> system.
/// </summary>
#pragma warning disable CA1019
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
sealed class TgsGraphQLAuthorizeAttribute : AuthorizeAttribute
{
/// <summary>
/// Gets the <see cref="Api.Rights.RightsType"/> associated with the <see cref="TgsAuthorizeAttribute"/> if any.
/// </summary>
public RightsType? RightsType { get; }
/// <summary>
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
/// </summary>
public TgsGraphQLAuthorizeAttribute()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
/// </summary>
/// <param name="requiredRights">The <see cref="AdministrationRights"/> required.</param>
public TgsGraphQLAuthorizeAttribute(AdministrationRights requiredRights)
{
Roles = RightsHelper.RoleNames(requiredRights).Split(',');
RightsType = Api.Rights.RightsType.Administration;
}
/// <summary>
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
/// </summary>
/// <param name="requiredRights">The <see cref="InstanceManagerRights"/> required.</param>
public TgsGraphQLAuthorizeAttribute(InstanceManagerRights requiredRights)
{
Roles = RightsHelper.RoleNames(requiredRights).Split(',');
RightsType = Api.Rights.RightsType.InstanceManager;
}
/// <summary>
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
/// </summary>
/// <param name="requiredRights">The <see cref="RepositoryRights"/> required.</param>
public TgsGraphQLAuthorizeAttribute(RepositoryRights requiredRights)
{
Roles = RightsHelper.RoleNames(requiredRights).Split(',');
RightsType = Api.Rights.RightsType.Repository;
}
/// <summary>
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
/// </summary>
/// <param name="requiredRights">The <see cref="EngineRights"/> required.</param>
public TgsGraphQLAuthorizeAttribute(EngineRights requiredRights)
{
Roles = RightsHelper.RoleNames(requiredRights).Split(',');
RightsType = Api.Rights.RightsType.Engine;
}
/// <summary>
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
/// </summary>
/// <param name="requiredRights">The <see cref="DreamMakerRights"/> required.</param>
public TgsGraphQLAuthorizeAttribute(DreamMakerRights requiredRights)
{
Roles = RightsHelper.RoleNames(requiredRights).Split(',');
RightsType = Api.Rights.RightsType.DreamMaker;
}
/// <summary>
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
/// </summary>
/// <param name="requiredRights">The <see cref="DreamDaemonRights"/> required.</param>
public TgsGraphQLAuthorizeAttribute(DreamDaemonRights requiredRights)
{
Roles = RightsHelper.RoleNames(requiredRights).Split(',');
RightsType = Api.Rights.RightsType.DreamDaemon;
}
/// <summary>
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
/// </summary>
/// <param name="requiredRights">The <see cref="ChatBotRights"/> required.</param>
public TgsGraphQLAuthorizeAttribute(ChatBotRights requiredRights)
{
Roles = RightsHelper.RoleNames(requiredRights).Split(',');
RightsType = Api.Rights.RightsType.ChatBots;
}
/// <summary>
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
/// </summary>
/// <param name="requiredRights">The <see cref="ConfigurationRights"/> required.</param>
public TgsGraphQLAuthorizeAttribute(ConfigurationRights requiredRights)
{
Roles = RightsHelper.RoleNames(requiredRights).Split(',');
RightsType = Api.Rights.RightsType.Configuration;
}
/// <summary>
/// Initializes a new instance of the <see cref="TgsGraphQLAuthorizeAttribute"/> class.
/// </summary>
/// <param name="requiredRights">The <see cref="InstancePermissionSetRights"/> required.</param>
public TgsGraphQLAuthorizeAttribute(InstancePermissionSetRights requiredRights)
{
Roles = RightsHelper.RoleNames(requiredRights).Split(',');
RightsType = Api.Rights.RightsType.InstancePermissionSet;
}
}
}
@@ -1380,23 +1380,8 @@ namespace Tgstation.Server.Tests.Live
await multiClient.ExecuteReadOnlyConfirmEquivalence(
restClient => restClient.ServerInformation(cancellationToken),
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.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.Info.Address,
PublicAddress = x.Info.PublicAddress,
Controller = x.Info.Controller,
Identifier = x.Info.Identifier,
}))))
async gqlClient => (await gqlClient.UnauthenticatedServerInformation.ExecuteAsync(cancellationToken)).Data,
(restServerInfo, gqlServerInfo) => restServerInfo.ApiVersion.Major == gqlServerInfo.Swarm.CurrentNode.Gateway.Information.MajorApiVersion
&& (restServerInfo.OAuthProviderInfos == gqlServerInfo.Swarm.CurrentNode.Gateway.Information.OAuthProviderInfos
|| restServerInfo.OAuthProviderInfos.All(kvp =>
{