diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index 7fe2f24992..37a61e55fb 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -461,7 +461,7 @@ namespace Tgstation.Server.Api.Models RepoTestMergeConflict, /// - /// Attempted to create an instance outside of the . + /// Attempted to create an instance outside of the . /// [Description("The new instance's path is not under a white-listed path.")] InstanceNotAtWhitelistedPath, diff --git a/src/Tgstation.Server.Api/Models/Internal/GatewayInformation.cs b/src/Tgstation.Server.Api/Models/Internal/GatewayInformation.cs deleted file mode 100644 index 48939aba6f..0000000000 --- a/src/Tgstation.Server.Api/Models/Internal/GatewayInformation.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; - -namespace Tgstation.Server.Api.Models.Internal -{ - /// - /// Information about the local tgstation-server. - /// - public class GatewayInformation : GatewayInformationBase - { - /// - /// If the server is running on a windows operating system. - /// - public bool WindowsHost { get; set; } - - /// - /// Map of to the for them. - /// - public Dictionary? OAuthProviderInfos { get; set; } - } -} diff --git a/src/Tgstation.Server.Api/Models/Internal/GatewayInformationBase.cs b/src/Tgstation.Server.Api/Models/Internal/ServerInformationBase.cs similarity index 88% rename from src/Tgstation.Server.Api/Models/Internal/GatewayInformationBase.cs rename to src/Tgstation.Server.Api/Models/Internal/ServerInformationBase.cs index f1f53b26bb..15fafe90b1 100644 --- a/src/Tgstation.Server.Api/Models/Internal/GatewayInformationBase.cs +++ b/src/Tgstation.Server.Api/Models/Internal/ServerInformationBase.cs @@ -5,7 +5,7 @@ namespace Tgstation.Server.Api.Models.Internal /// /// Base class for . /// - public abstract class GatewayInformationBase + public abstract class ServerInformationBase { /// /// 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. /// [ResponseOptions] - public ICollection? ValidInstancePaths { get; set; } + public List? ValidInstancePaths { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/Response/ServerInformationResponse.cs b/src/Tgstation.Server.Api/Models/Response/ServerInformationResponse.cs index a912fa127e..4e80e80564 100644 --- a/src/Tgstation.Server.Api/Models/Response/ServerInformationResponse.cs +++ b/src/Tgstation.Server.Api/Models/Response/ServerInformationResponse.cs @@ -6,7 +6,7 @@ namespace Tgstation.Server.Api.Models.Response /// /// Represents basic server information. /// - public sealed class ServerInformationResponse : Internal.GatewayInformation + public sealed class ServerInformationResponse : Internal.ServerInformationBase { /// /// The version of the host. @@ -23,6 +23,16 @@ namespace Tgstation.Server.Api.Models.Response /// public Version? DMApiVersion { get; set; } + /// + /// If the server is running on a windows operating system. + /// + public bool WindowsHost { get; set; } + + /// + /// Map of to the for them. + /// + public Dictionary? OAuthProviderInfos { get; set; } + /// /// If there is a server update in progress. /// diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Queries/ServerInformation.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/ServerInformation.graphql deleted file mode 100644 index 5e04dc4a66..0000000000 --- a/src/Tgstation.Server.Client.GraphQL/GQL/Queries/ServerInformation.graphql +++ /dev/null @@ -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 - } - } - } -} diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Queries/UnauthenticatedServerInformation.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/UnauthenticatedServerInformation.graphql new file mode 100644 index 0000000000..44bff278f0 --- /dev/null +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/UnauthenticatedServerInformation.graphql @@ -0,0 +1,19 @@ +query UnauthenticatedServerInformation { + swarm { + currentNode { + gateway { + information { + majorApiVersion + oAuthProviderInfos { + key + value { + clientId + redirectUri + serverUrl + } + } + } + } + } + } +} diff --git a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs index 3a5ccd8ca2..1790316e37 100644 --- a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs @@ -16,7 +16,7 @@ namespace Tgstation.Server.Host.Configuration /// /// General configuration options. /// - public sealed class GeneralConfiguration : GatewayInformationBase + public sealed class GeneralConfiguration : ServerInformationBase { /// /// The key for the the resides in. @@ -29,22 +29,22 @@ namespace Tgstation.Server.Host.Configuration public const ushort DefaultApiPort = 5000; /// - /// The default value for . + /// The default value for . /// const uint DefaultMinimumPasswordLength = 15; /// - /// The default value for . + /// The default value for . /// const uint DefaultInstanceLimit = 10; /// - /// The default value for . + /// The default value for . /// const uint DefaultUserLimit = 100; /// - /// The default value for . + /// The default value for . /// const uint DefaultUserGroupLimit = 25; diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 8c6d33e8b4..de1599b484 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -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() + .ModifyOptions(options => + { + options.EnsureAllNodesCanBeResolved = true; + }) .AddMutationConventions() .AddGlobalObjectIdentification() .AddQueryFieldToMutationPayloads() @@ -314,6 +316,7 @@ namespace Tgstation.Server.Host.Core .AddSorting() .AddHostTypes() .AddErrorFilter() + .AddType() .AddType() .AddType() .AddType() diff --git a/src/Tgstation.Server.Host/GraphQL/Interfaces/IGateway.cs b/src/Tgstation.Server.Host/GraphQL/Interfaces/IGateway.cs index e8846abbea..56722119d2 100644 --- a/src/Tgstation.Server.Host/GraphQL/Interfaces/IGateway.cs +++ b/src/Tgstation.Server.Host/GraphQL/Interfaces/IGateway.cs @@ -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 /// /// Gets . /// - /// The to use. - /// The to use. - /// The containing the to use. - /// A new . - [AllowAnonymous] - GatewayInformation Information( - [Service] IOAuthProviders oAuthProviders, - [Service] IPlatformIdentifier platformIdentifier, - [Service] IOptionsSnapshot generalConfigurationOptions); + /// The for the . + GatewayInformation Information(); } } diff --git a/src/Tgstation.Server.Host/GraphQL/Interfaces/IServerNode.cs b/src/Tgstation.Server.Host/GraphQL/Interfaces/IServerNode.cs new file mode 100644 index 0000000000..63ca2b3b31 --- /dev/null +++ b/src/Tgstation.Server.Host/GraphQL/Interfaces/IServerNode.cs @@ -0,0 +1,21 @@ +using HotChocolate; + +using Microsoft.Extensions.Options; + +using Tgstation.Server.Host.Configuration; + +namespace Tgstation.Server.Host.GraphQL.Interfaces +{ + /// + /// Represents a tgstation-server installation. + /// + public interface IServerNode + { + /// + /// Access the for the . + /// + /// The of . + /// The for the . + public IGateway Gateway([Service] IOptionsSnapshot swarmConfigurationOptions); + } +} diff --git a/src/Tgstation.Server.Host/GraphQL/Types/GatewayInformation.cs b/src/Tgstation.Server.Host/GraphQL/Types/GatewayInformation.cs new file mode 100644 index 0000000000..aefc2562bb --- /dev/null +++ b/src/Tgstation.Server.Host/GraphQL/Types/GatewayInformation.cs @@ -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 +{ + /// + /// Represents information about a retrieved via a . + /// + public sealed class GatewayInformation + { + /// + /// Gets the minimum valid password length for TGS users. + /// + /// The containing the . + /// A specifying the minimumn valid password length for TGS users. + [TgsGraphQLAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword)] + public uint? MinimumPasswordLength( + [Service] IOptionsSnapshot generalConfigurationOptions) + { + ArgumentNullException.ThrowIfNull(generalConfigurationOptions); + return generalConfigurationOptions.Value.MinimumPasswordLength; + } + + /// + /// Gets the maximum allowed attached instances for the . + /// + /// The containing the . + /// A specifying the maximum allowed attached instances for the . + [TgsGraphQLAuthorize(InstanceManagerRights.Create)] + public uint? InstanceLimit( + [Service] IOptionsSnapshot generalConfigurationOptions) + { + ArgumentNullException.ThrowIfNull(generalConfigurationOptions); + return generalConfigurationOptions.Value.InstanceLimit; + } + + /// + /// Gets the maximum allowed registered s for the . + /// + /// The containing the . + /// A specifying the maximum allowed registered users for the . + /// This limit only applies to user creation attempts made via the current . + [TgsGraphQLAuthorize(AdministrationRights.WriteUsers)] + public uint? UserLimit( + [Service] IOptionsSnapshot generalConfigurationOptions) + { + ArgumentNullException.ThrowIfNull(generalConfigurationOptions); + return generalConfigurationOptions.Value.UserLimit; + } + + /// + /// Gets the maximum allowed registered s for the . + /// + /// The containing the . + /// A specifying the maximum allowed registered s for the . + /// This limit only applies to creation attempts made via the current . + [TgsGraphQLAuthorize(AdministrationRights.WriteUsers)] + public uint? UserGroupLimit( + [Service] IOptionsSnapshot generalConfigurationOptions) + { + ArgumentNullException.ThrowIfNull(generalConfigurationOptions); + return generalConfigurationOptions.Value.UserGroupLimit; + } + + /// + /// Gets the locations s may be created or attached from if there are restrictions. + /// + /// The containing the . + /// The locations s may be created or attached from if there are restrictions, otherwise. + [TgsGraphQLAuthorize(InstanceManagerRights.Create | InstanceManagerRights.Relocate)] + public IReadOnlyCollection? ValidInstancePaths( + [Service] IOptionsSnapshot generalConfigurationOptions) + { + ArgumentNullException.ThrowIfNull(generalConfigurationOptions); + return generalConfigurationOptions.Value.ValidInstancePaths; + } + + /// + /// Gets a flag indicating whether or not the current runs on a Windows operating system. + /// + /// The to use. + /// if the runs on a Windows operating system, otherwise. + [TgsGraphQLAuthorize] + public bool? WindowsHost( + [Service] IPlatformIdentifier platformIdentifier) + { + ArgumentNullException.ThrowIfNull(platformIdentifier); + return platformIdentifier.IsWindows; + } + + /// + /// Gets the swarm protocol . + /// + [TgsGraphQLAuthorize] + public Version? SwarmProtocolVersion => global::System.Version.Parse(MasterVersionsAttribute.Instance.RawSwarmProtocolVersion); + + /// + /// Gets the of tgstation-server the is running. + /// + /// The to use. + /// The of tgstation-server the is running. + [TgsGraphQLAuthorize] + public Version? Version( + [Service] IAssemblyInformationProvider assemblyInformationProvider) + { + ArgumentNullException.ThrowIfNull(assemblyInformationProvider); + return assemblyInformationProvider.Version; + } + + /// + /// Gets the major HTTP API number of the . + /// + public int MajorApiVersion => ApiHeaders.Version.Major; + + /// + /// Gets the HTTP API of the . + /// + [TgsGraphQLAuthorize] + public Version? ApiVersion => ApiHeaders.Version; + + /// + /// Gets the DMAPI interop the uses. + /// + [TgsGraphQLAuthorize] + public Version? DMApiVersion => DMApiConstants.InteropVersion; + + /// + /// Gets the information needed to perform open authentication with the . + /// + /// The to use. + /// A map of enabled s to their . + public IReadOnlyDictionary OAuthProviderInfos( + [Service] IOAuthProviders oAuthProviders) + { + ArgumentNullException.ThrowIfNull(oAuthProviders); + return oAuthProviders.ProviderInfos(); + } + } +} diff --git a/src/Tgstation.Server.Host/GraphQL/Types/Interceptors/PublicAuthorizeDirectiveTypeInterceptor.cs b/src/Tgstation.Server.Host/GraphQL/Types/Interceptors/PublicAuthorizeDirectiveTypeInterceptor.cs deleted file mode 100644 index d4cb7407bd..0000000000 --- a/src/Tgstation.Server.Host/GraphQL/Types/Interceptors/PublicAuthorizeDirectiveTypeInterceptor.cs +++ /dev/null @@ -1,23 +0,0 @@ -using HotChocolate.Configuration; -using HotChocolate.Types.Descriptors.Definitions; - -namespace Tgstation.Server.Host.GraphQL.Types.Interceptors -{ - /// - /// Makes the @authorize directive public (It is internal by default). - /// - public sealed class PublicAuthorizeDirectiveTypeInterceptor : TypeInterceptor - { - /// - public override void OnBeforeRegisterDependencies(ITypeDiscoveryContext discoveryContext, DefinitionBase definition) - { - if (definition is DirectiveTypeDefinition dtd - && dtd.Name == "authorize") - { - dtd.IsPublic = true; - } - - base.OnBeforeRegisterDependencies(discoveryContext, definition); - } - } -} diff --git a/src/Tgstation.Server.Host/GraphQL/Types/LocalGateway.cs b/src/Tgstation.Server.Host/GraphQL/Types/LocalGateway.cs index 107df473e1..09d21f7f2d 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/LocalGateway.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/LocalGateway.cs @@ -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 { /// - public GatewayInformation Information( - [Service] IOAuthProviders oAuthProviders, - [Service] IPlatformIdentifier platformIdentifier, - [Service] IOptionsSnapshot 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(); } } diff --git a/src/Tgstation.Server.Host/GraphQL/Types/NodeInformation.cs b/src/Tgstation.Server.Host/GraphQL/Types/NodeInformation.cs deleted file mode 100644 index 42af3c8176..0000000000 --- a/src/Tgstation.Server.Host/GraphQL/Types/NodeInformation.cs +++ /dev/null @@ -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 -{ - /// - /// Represent a server in the TGS server swarm. - /// - [Node] - public sealed class NodeInformation - { - /// - /// Node resolver for s. - /// - /// The to lookup. - /// The to use. - /// The queried , if present. - 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); - } - - /// - /// The swarm server ID. - /// - [ID] - public string Identifier { get; } - - /// - /// The swarm server's internal . - /// - public Uri Address { get; } - - /// - /// The swarm server's optional public address. - /// - public Uri? PublicAddress { get; } - - /// - /// Whether or not the server is the swarm's controller. - /// - public bool Controller { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The to build from. - public NodeInformation(Api.Models.Internal.SwarmServerInformation swarmServerInformation) - { - ArgumentNullException.ThrowIfNull(swarmServerInformation); - - Identifier = swarmServerInformation.Identifier!; - Address = swarmServerInformation.Address!; - PublicAddress = swarmServerInformation.PublicAddress; - Controller = swarmServerInformation.Controller; - } - } -} diff --git a/src/Tgstation.Server.Host/GraphQL/Types/PermissionSet.cs b/src/Tgstation.Server.Host/GraphQL/Types/PermissionSet.cs index 9f0734f316..ba0c070582 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/PermissionSet.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/PermissionSet.cs @@ -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 /// /// Represents a set of permissions for the server. /// - [Node] public sealed class PermissionSet : Entity { /// diff --git a/src/Tgstation.Server.Host/GraphQL/Types/RemoteGateway.cs b/src/Tgstation.Server.Host/GraphQL/Types/RemoteGateway.cs index 5c3305e56d..1a17b636e9 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/RemoteGateway.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/RemoteGateway.cs @@ -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 { /// - public GatewayInformation Information( - [Service] IOAuthProviders oAuthProviders, - [Service] IPlatformIdentifier platformIdentifier, - [Service] IOptionsSnapshot generalConfigurationOptions) - { - ArgumentNullException.ThrowIfNull(oAuthProviders); - ArgumentNullException.ThrowIfNull(platformIdentifier); - ArgumentNullException.ThrowIfNull(generalConfigurationOptions); - - throw new ErrorMessageException(ErrorCode.RemoteGatewaysNotImplemented); - } + public GatewayInformation Information() => throw new NotImplementedException(); } } diff --git a/src/Tgstation.Server.Host/GraphQL/Types/ServerSwarm.cs b/src/Tgstation.Server.Host/GraphQL/Types/ServerSwarm.cs index 1b874c13b1..286aad2adf 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/ServerSwarm.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/ServerSwarm.cs @@ -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 { /// - /// Gets the for the swarm. + /// If there is a swarm update in progress. /// - /// The to use. /// The to use. - /// A new . - public SwarmMetadata Metadata( - [Service] IAssemblyInformationProvider assemblyInformationProvider, + /// if there is an update in progress, otherwise. + [TgsGraphQLAuthorize] + public bool UpdateInProgress( [Service] IServerControl serverControl) { - ArgumentNullException.ThrowIfNull(assemblyInformationProvider); ArgumentNullException.ThrowIfNull(serverControl); - return new SwarmMetadata(assemblyInformationProvider, serverControl.UpdateInProgress); + return serverControl.UpdateInProgress; } + /// + /// Gets the swarm protocol major version in use. + /// + [TgsGraphQLAuthorize] + public int ProtocolMajorVersion => Version.Parse(MasterVersionsAttribute.Instance.RawSwarmProtocolVersion).Major; + /// /// Gets the swarm's . /// /// A new . + [TgsGraphQLAuthorize] public Users Users() => new(); /// /// Gets the connected server. /// - /// The to use. /// The containing the current . - /// A new . - public SwarmNode CurrentNode( - [Service] ISwarmService swarmService, - [Service] IOptionsSnapshot swarmConfigurationOptions) + /// The to use. + /// A new for the local node if it is part of a swarm, otherwise. + public IServerNode CurrentNode( + [Service] IOptionsSnapshot 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(); } /// /// Gets all servers in the swarm. /// /// The to use. - /// A of s if the local server is part of a swarm, otherwise. + /// A of s if the local node is part of a swarm, otherwise. + [TgsGraphQLAuthorize] public List? 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(); } } } diff --git a/src/Tgstation.Server.Host/GraphQL/Types/StandaloneNode.cs b/src/Tgstation.Server.Host/GraphQL/Types/StandaloneNode.cs new file mode 100644 index 0000000000..335bee3543 --- /dev/null +++ b/src/Tgstation.Server.Host/GraphQL/Types/StandaloneNode.cs @@ -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 +{ + /// + /// A not running as part of a larger . + /// + public sealed class StandaloneNode : IServerNode + { + /// + public IGateway Gateway([Service] IOptionsSnapshot swarmConfigurationOptions) + => new LocalGateway(); + } +} diff --git a/src/Tgstation.Server.Host/GraphQL/Types/SwarmMetadata.cs b/src/Tgstation.Server.Host/GraphQL/Types/SwarmMetadata.cs deleted file mode 100644 index 19fe192b23..0000000000 --- a/src/Tgstation.Server.Host/GraphQL/Types/SwarmMetadata.cs +++ /dev/null @@ -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 -{ - /// - /// Represents information that is constant across all servers in a . - /// - public sealed class SwarmMetadata - { - /// - /// The version of the host. - /// - public Version Version { get; } - - /// - /// The version of the host. - /// - public Version ApiVersion => ApiHeaders.Version; - - /// - /// The DMAPI interop version the server uses. - /// - public Version DMApiVersion => DMApiConstants.InteropVersion; - - /// - /// If there is a server update in progress. - /// - public bool UpdateInProgress { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The used to derive the . - /// The value of . - public SwarmMetadata(IAssemblyInformationProvider assemblyInformationProvider, bool updateInProgress) - { - ArgumentNullException.ThrowIfNull(assemblyInformationProvider); - Version = assemblyInformationProvider.Version; - UpdateInProgress = updateInProgress; - } - } -} diff --git a/src/Tgstation.Server.Host/GraphQL/Types/SwarmNode.cs b/src/Tgstation.Server.Host/GraphQL/Types/SwarmNode.cs index f864287f3e..2701e828b5 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/SwarmNode.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/SwarmNode.cs @@ -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 { /// /// Represents a node server in a swarm. /// - public sealed class SwarmNode + [Node(IdField = nameof(Identifier))] + public sealed class SwarmNode : IServerNode { /// - /// Gets the . + /// The swarm server ID. /// - public NodeInformation? Info { get; } + [ID] + public string Identifier { get; } + + /// + /// The swarm server's internal . + /// + public Uri Address { get; } + + /// + /// The swarm server's optional public address. + /// + public Uri? PublicAddress { get; } + + /// + /// Whether or not the server is the swarm's controller. + /// + public bool Controller { get; } + + /// + /// Node resolver for s. + /// + /// The . + /// The to load from. + /// A new with the matching if found, otherwise. + 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); + } /// /// Initializes a new instance of the class. /// - /// The value of . - public SwarmNode(NodeInformation? info) + /// The to use for initialization. + public SwarmNode(SwarmServerInformation? nodeInformation) { - Info = info; + ArgumentNullException.ThrowIfNull(nodeInformation); + + Identifier = nodeInformation.Identifier!; + Address = nodeInformation.Address!; + PublicAddress = nodeInformation.PublicAddress; + Controller = nodeInformation.Controller; } /// @@ -35,11 +83,11 @@ namespace Tgstation.Server.Host.GraphQL.Types /// The containing the current . /// A new . /// The 's . - public IGateway? Gateway([Service] IOptionsSnapshot swarmConfigurationOptions) + public IGateway Gateway([Service] IOptionsSnapshot swarmConfigurationOptions) { ArgumentNullException.ThrowIfNull(swarmConfigurationOptions); - bool local = Info == null || Info.Identifier == swarmConfigurationOptions.Value.Identifier; + bool local = Identifier == swarmConfigurationOptions.Value.Identifier; if (local) return new LocalGateway(); diff --git a/src/Tgstation.Server.Host/Security/TgsGraphQLAuthorizeAttribute.cs b/src/Tgstation.Server.Host/Security/TgsGraphQLAuthorizeAttribute.cs new file mode 100644 index 0000000000..e064440e18 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/TgsGraphQLAuthorizeAttribute.cs @@ -0,0 +1,118 @@ +using System; + +using HotChocolate.Authorization; + +using Tgstation.Server.Api.Rights; + +namespace Tgstation.Server.Host.Security +{ + /// + /// Helper for using the with the system. + /// +#pragma warning disable CA1019 + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true, Inherited = true)] + sealed class TgsGraphQLAuthorizeAttribute : AuthorizeAttribute + { + /// + /// Gets the associated with the if any. + /// + public RightsType? RightsType { get; } + + /// + /// Initializes a new instance of the class. + /// + public TgsGraphQLAuthorizeAttribute() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The required. + public TgsGraphQLAuthorizeAttribute(AdministrationRights requiredRights) + { + Roles = RightsHelper.RoleNames(requiredRights).Split(','); + RightsType = Api.Rights.RightsType.Administration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The required. + public TgsGraphQLAuthorizeAttribute(InstanceManagerRights requiredRights) + { + Roles = RightsHelper.RoleNames(requiredRights).Split(','); + RightsType = Api.Rights.RightsType.InstanceManager; + } + + /// + /// Initializes a new instance of the class. + /// + /// The required. + public TgsGraphQLAuthorizeAttribute(RepositoryRights requiredRights) + { + Roles = RightsHelper.RoleNames(requiredRights).Split(','); + RightsType = Api.Rights.RightsType.Repository; + } + + /// + /// Initializes a new instance of the class. + /// + /// The required. + public TgsGraphQLAuthorizeAttribute(EngineRights requiredRights) + { + Roles = RightsHelper.RoleNames(requiredRights).Split(','); + RightsType = Api.Rights.RightsType.Engine; + } + + /// + /// Initializes a new instance of the class. + /// + /// The required. + public TgsGraphQLAuthorizeAttribute(DreamMakerRights requiredRights) + { + Roles = RightsHelper.RoleNames(requiredRights).Split(','); + RightsType = Api.Rights.RightsType.DreamMaker; + } + + /// + /// Initializes a new instance of the class. + /// + /// The required. + public TgsGraphQLAuthorizeAttribute(DreamDaemonRights requiredRights) + { + Roles = RightsHelper.RoleNames(requiredRights).Split(','); + RightsType = Api.Rights.RightsType.DreamDaemon; + } + + /// + /// Initializes a new instance of the class. + /// + /// The required. + public TgsGraphQLAuthorizeAttribute(ChatBotRights requiredRights) + { + Roles = RightsHelper.RoleNames(requiredRights).Split(','); + RightsType = Api.Rights.RightsType.ChatBots; + } + + /// + /// Initializes a new instance of the class. + /// + /// The required. + public TgsGraphQLAuthorizeAttribute(ConfigurationRights requiredRights) + { + Roles = RightsHelper.RoleNames(requiredRights).Split(','); + RightsType = Api.Rights.RightsType.Configuration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The required. + public TgsGraphQLAuthorizeAttribute(InstancePermissionSetRights requiredRights) + { + Roles = RightsHelper.RoleNames(requiredRights).Split(','); + RightsType = Api.Rights.RightsType.InstancePermissionSet; + } + } +} diff --git a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs index bc55b33077..1c17f587c0 100644 --- a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs +++ b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs @@ -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 => {