diff --git a/src/Tgstation.Server.Host/Authority/IUserAuthority.cs b/src/Tgstation.Server.Host/Authority/IUserAuthority.cs index 0279483f5c..71f807371a 100644 --- a/src/Tgstation.Server.Host/Authority/IUserAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/IUserAuthority.cs @@ -28,10 +28,11 @@ namespace Tgstation.Server.Host.Authority /// /// The of the . /// If related entities should be loaded. + /// If the may be returned. /// The for the operation. /// A resulting in a . [TgsAuthorize(AdministrationRights.ReadUsers)] - public ValueTask> GetId(long id, bool includeJoins, CancellationToken cancellationToken); + public ValueTask> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken); /// /// Gets all registered s. diff --git a/src/Tgstation.Server.Host/Authority/UserAuthority.cs b/src/Tgstation.Server.Host/Authority/UserAuthority.cs index 2d113a7abb..fe15593a1b 100644 --- a/src/Tgstation.Server.Host/Authority/UserAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/UserAuthority.cs @@ -47,7 +47,7 @@ namespace Tgstation.Server.Host.Authority => ValueTask.FromResult(new AuthorityResponse(authenticationContext.User)); /// - public async ValueTask> GetId(long id, bool includeJoins, CancellationToken cancellationToken) + public async ValueTask> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken) { var queryable = ListCore(includeJoins); @@ -57,7 +57,7 @@ namespace Tgstation.Server.Host.Authority if (user == default) return NotFound(); - if (user.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName)) + if (!allowSystemUser && user.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName)) return Forbid(); return new AuthorityResponse(user); diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index 507eb1978c..e59df1f889 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -414,7 +414,7 @@ namespace Tgstation.Server.Host.Controllers return await userAuthority.InvokeTransformable( this, - authority => authority.GetId(id, true, cancellationToken)); + authority => authority.GetId(id, true, false, cancellationToken)); } /// diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 2ef310428e..5da177e23e 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -300,6 +300,7 @@ namespace Tgstation.Server.Host.Core .AddErrorFilter() .AddType() .AddType() + .AddType() .AddType() .BindRuntimeType() .AddQueryType() diff --git a/src/Tgstation.Server.Host/GraphQL/Types/IGateway.cs b/src/Tgstation.Server.Host/GraphQL/Interfaces/IGateway.cs similarity index 92% rename from src/Tgstation.Server.Host/GraphQL/Types/IGateway.cs rename to src/Tgstation.Server.Host/GraphQL/Interfaces/IGateway.cs index 45b83f87c7..89acbc10cc 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/IGateway.cs +++ b/src/Tgstation.Server.Host/GraphQL/Interfaces/IGateway.cs @@ -2,13 +2,13 @@ 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; -namespace Tgstation.Server.Host.GraphQL.Types +namespace Tgstation.Server.Host.GraphQL.Interfaces { /// /// Management interface for the parent . diff --git a/src/Tgstation.Server.Host/GraphQL/Interfaces/IUserName.cs b/src/Tgstation.Server.Host/GraphQL/Interfaces/IUserName.cs new file mode 100644 index 0000000000..f2a57702cc --- /dev/null +++ b/src/Tgstation.Server.Host/GraphQL/Interfaces/IUserName.cs @@ -0,0 +1,21 @@ +using HotChocolate.Types.Relay; + +namespace Tgstation.Server.Host.GraphQL.Interfaces +{ + /// + /// A lightly scoped . + /// + public interface IUserName + { + /// + /// The ID of the user. + /// + [ID] + public long Id { get; } + + /// + /// The name of the user. + /// + public string Name { get; } + } +} diff --git a/src/Tgstation.Server.Host/GraphQL/Types/LocalGateway.cs b/src/Tgstation.Server.Host/GraphQL/Types/LocalGateway.cs index 5892a49f01..1774bc6596 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/LocalGateway.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/LocalGateway.cs @@ -6,6 +6,7 @@ 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; diff --git a/src/Tgstation.Server.Host/GraphQL/Types/NamedEntity.cs b/src/Tgstation.Server.Host/GraphQL/Types/NamedEntity.cs index e48c18ce14..a52677b834 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/NamedEntity.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/NamedEntity.cs @@ -1,5 +1,7 @@ using System; +using Tgstation.Server.Host.GraphQL.Interfaces; + namespace Tgstation.Server.Host.GraphQL.Types { /// @@ -12,6 +14,16 @@ namespace Tgstation.Server.Host.GraphQL.Types /// public string Name { get; } + /// + /// Initializes a new instance of the class. + /// + /// The to copy. + protected NamedEntity(NamedEntity copy) + : base(copy?.Id ?? throw new ArgumentNullException(nameof(copy))) + { + Name = copy.Name; + } + /// /// Initializes a new instance of the class. /// diff --git a/src/Tgstation.Server.Host/GraphQL/Types/Node.cs b/src/Tgstation.Server.Host/GraphQL/Types/Node.cs index dac5cd5a84..f42183950d 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/Node.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/Node.cs @@ -5,6 +5,7 @@ using HotChocolate; using Microsoft.Extensions.Options; using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.GraphQL.Interfaces; namespace Tgstation.Server.Host.GraphQL.Types { diff --git a/src/Tgstation.Server.Host/GraphQL/Types/RemoteGateway.cs b/src/Tgstation.Server.Host/GraphQL/Types/RemoteGateway.cs index ffe7731895..0a00849057 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/RemoteGateway.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/RemoteGateway.cs @@ -7,6 +7,7 @@ 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; diff --git a/src/Tgstation.Server.Host/GraphQL/Types/User.cs b/src/Tgstation.Server.Host/GraphQL/Types/User.cs index f32c6ff2cd..4ab07675df 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/User.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/User.cs @@ -1,13 +1,20 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; +using HotChocolate; + +using Tgstation.Server.Host.Authority; +using Tgstation.Server.Host.Authority.Core; +using Tgstation.Server.Host.GraphQL.Interfaces; + namespace Tgstation.Server.Host.GraphQL.Types { /// /// A user registered in the server. /// - public sealed class User : NamedEntity + public sealed class User : NamedEntity, IUserName { /// /// If the is enabled since users cannot be deleted. System users cannot be disabled. @@ -72,9 +79,23 @@ namespace Tgstation.Server.Host.GraphQL.Types /// /// The who created this . /// - /// A resulting in the who created this , if any. - public ValueTask CreatedBy() - => throw new NotImplementedException(); + /// The . + /// The for the operation. + /// The that created this , if any. + public async ValueTask CreatedBy( + [Service] IGraphQLAuthorityInvoker userAuthority, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(userAuthority); + if (!createdById.HasValue) + return null; + + var user = await userAuthority.InvokeTransformable(authority => authority.GetId(createdById.Value, false, true, cancellationToken)); + if (user.CanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) + return new UserName(user); + + return user; + } /// /// List of s associated with the user if OAuth is configured. diff --git a/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs b/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs new file mode 100644 index 0000000000..f16379debc --- /dev/null +++ b/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs @@ -0,0 +1,19 @@ +using Tgstation.Server.Host.GraphQL.Interfaces; + +namespace Tgstation.Server.Host.GraphQL.Types +{ + /// + /// A with limited fields. + /// + public sealed class UserName : NamedEntity, IUserName + { + /// + /// Initializes a new instance of the class. + /// + /// The to copy. + public UserName(NamedEntity copy) + : base(copy) + { + } + } +} diff --git a/src/Tgstation.Server.Host/GraphQL/Types/Users.cs b/src/Tgstation.Server.Host/GraphQL/Types/Users.cs index 9ff2e2b256..754771e917 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/Users.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/Users.cs @@ -51,7 +51,7 @@ namespace Tgstation.Server.Host.GraphQL.Types CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(userAuthority); - return await userAuthority.InvokeTransformable(authority => authority.GetId(id, false, cancellationToken)); + return await userAuthority.InvokeTransformable(authority => authority.GetId(id, false, false, cancellationToken)); } ///