diff --git a/src/Tgstation.Server.Host/Authority/Core/AuthorityBase.cs b/src/Tgstation.Server.Host/Authority/Core/AuthorityBase.cs index 45121dd8ff..eff31f3c41 100644 --- a/src/Tgstation.Server.Host/Authority/Core/AuthorityBase.cs +++ b/src/Tgstation.Server.Host/Authority/Core/AuthorityBase.cs @@ -7,6 +7,7 @@ using Octokit; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Response; +using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Authority.Core { @@ -15,6 +16,11 @@ namespace Tgstation.Server.Host.Authority.Core /// abstract class AuthorityBase : IAuthority { + /// + /// Gets the for the . + /// + protected IAuthenticationContext AuthenticationContext { get; } + /// /// Gets the for the . /// @@ -64,9 +70,13 @@ namespace Tgstation.Server.Host.Authority.Core /// /// Initializes a new instance of the class. /// + /// The value of . /// The value of . - protected AuthorityBase(ILogger logger) + protected AuthorityBase( + IAuthenticationContext authenticationContext, + ILogger logger) { + AuthenticationContext = authenticationContext ?? throw new ArgumentNullException(nameof(authenticationContext)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); } diff --git a/src/Tgstation.Server.Host/Authority/IUserGroupAuthority.cs b/src/Tgstation.Server.Host/Authority/IUserGroupAuthority.cs new file mode 100644 index 0000000000..3afd0c5777 --- /dev/null +++ b/src/Tgstation.Server.Host/Authority/IUserGroupAuthority.cs @@ -0,0 +1,25 @@ +using System.Threading; +using System.Threading.Tasks; + +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Authority.Core; +using Tgstation.Server.Host.Models; +using Tgstation.Server.Host.Security; + +namespace Tgstation.Server.Host.Authority +{ + /// + /// for managing s. + /// + public interface IUserGroupAuthority : IAuthority + { + /// + /// Gets the with a given . + /// + /// The of the . + /// The for the operation. + /// A resulting in a . + [TgsAuthorize(AdministrationRights.ReadUsers)] + public ValueTask> GetId(long id, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Authority/LoginAuthority.cs b/src/Tgstation.Server.Host/Authority/LoginAuthority.cs index 57f331a21c..d32e066478 100644 --- a/src/Tgstation.Server.Host/Authority/LoginAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/LoginAuthority.cs @@ -18,7 +18,7 @@ using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Authority { - /// + /// sealed class LoginAuthority : AuthorityBase, ILoginAuthority { /// @@ -101,6 +101,7 @@ namespace Tgstation.Server.Host.Authority /// /// Initializes a new instance of the class. /// + /// The to use. /// The to use. /// The value of . /// The value of . @@ -110,6 +111,7 @@ namespace Tgstation.Server.Host.Authority /// The value of . /// The value of . public LoginAuthority( + IAuthenticationContext authenticationContext, ILogger logger, IApiHeadersProvider apiHeadersProvider, ISystemIdentityFactory systemIdentityFactory, @@ -118,7 +120,7 @@ namespace Tgstation.Server.Host.Authority ITokenFactory tokenFactory, ICryptographySuite cryptographySuite, IIdentityCache identityCache) - : base(logger) + : base(authenticationContext, logger) { this.apiHeadersProvider = apiHeadersProvider ?? throw new ArgumentNullException(nameof(apiHeadersProvider)); this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory)); diff --git a/src/Tgstation.Server.Host/Authority/UserAuthority.cs b/src/Tgstation.Server.Host/Authority/UserAuthority.cs index 5a6094b0ec..a46e244ce2 100644 --- a/src/Tgstation.Server.Host/Authority/UserAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/UserAuthority.cs @@ -9,6 +9,7 @@ using GreenDonut; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Authority.Core; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Models; @@ -16,7 +17,7 @@ using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Authority { - /// + /// sealed class UserAuthority : AuthorityBase, IUserAuthority { /// @@ -34,11 +35,6 @@ namespace Tgstation.Server.Host.Authority /// readonly IOAuthConnectionsDataLoader oAuthConnectionsDataLoader; - /// - /// The for the . - /// - readonly IAuthenticationContext authenticationContext; - /// /// Implements the . /// @@ -47,7 +43,7 @@ namespace Tgstation.Server.Host.Authority /// The for the operation. /// A resulting in a of the requested s. [DataLoader] - public static async ValueTask> GetUsers( + public static Task> GetUsers( IReadOnlyList ids, IDatabaseContext databaseContext, CancellationToken cancellationToken) @@ -55,7 +51,7 @@ namespace Tgstation.Server.Host.Authority ArgumentNullException.ThrowIfNull(ids); ArgumentNullException.ThrowIfNull(databaseContext); - return await databaseContext + return databaseContext .Users .AsQueryable() .Where(x => ids.Contains(x.Id!.Value)) @@ -96,28 +92,30 @@ namespace Tgstation.Server.Host.Authority /// The value of . /// The value of . /// The value of . - /// The value of . + /// The value of . public UserAuthority( + IAuthenticationContext authenticationContext, ILogger logger, IDatabaseContext databaseContext, IUsersDataLoader usersDataLoader, - IOAuthConnectionsDataLoader oAuthConnectionsDataLoader, - IAuthenticationContext authenticationContext) - : base(logger) + IOAuthConnectionsDataLoader oAuthConnectionsDataLoader) + : base(authenticationContext, logger) { this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext)); this.usersDataLoader = usersDataLoader ?? throw new ArgumentNullException(nameof(usersDataLoader)); this.oAuthConnectionsDataLoader = oAuthConnectionsDataLoader ?? throw new ArgumentNullException(nameof(oAuthConnectionsDataLoader)); - this.authenticationContext = authenticationContext ?? throw new ArgumentNullException(nameof(authenticationContext)); } /// public ValueTask> Read(CancellationToken cancellationToken) - => ValueTask.FromResult(new AuthorityResponse(authenticationContext.User)); + => ValueTask.FromResult(new AuthorityResponse(AuthenticationContext.User)); /// public async ValueTask> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken) { + if (id != AuthenticationContext.User.Id && !((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers)) + return Forbid(); + User? user; if (includeJoins) { diff --git a/src/Tgstation.Server.Host/Authority/UserGroupAuthority.cs b/src/Tgstation.Server.Host/Authority/UserGroupAuthority.cs new file mode 100644 index 0000000000..c15b7c4c2e --- /dev/null +++ b/src/Tgstation.Server.Host/Authority/UserGroupAuthority.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using GreenDonut; + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Authority.Core; +using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Models; +using Tgstation.Server.Host.Security; + +namespace Tgstation.Server.Host.Authority +{ + /// + sealed class UserGroupAuthority : AuthorityBase, IUserGroupAuthority + { + /// + /// The for the . + /// + readonly IUserGroupsDataLoader userGroupsDataLoader; + + /// + /// Implements the . + /// + /// The of s to load. + /// The to load from. + /// The for the operation. + /// A resulting in a of the requested s. + [DataLoader] + public static Task> GetUserGroups( + IReadOnlyList ids, + IDatabaseContext databaseContext, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(ids); + ArgumentNullException.ThrowIfNull(databaseContext); + + return databaseContext + .Groups + .Where(group => ids.Contains(group.Id!.Value)) + .ToDictionaryAsync(userGroup => userGroup.Id!.Value, cancellationToken); + } + + /// + /// Initializes a new instance of the class. + /// + /// The to use. + /// The to use. + /// The value of . + public UserGroupAuthority( + IAuthenticationContext authenticationContext, + ILogger logger, + IUserGroupsDataLoader userGroupsDataLoader) + : base(authenticationContext, logger) + { + this.userGroupsDataLoader = userGroupsDataLoader ?? throw new ArgumentNullException(nameof(userGroupsDataLoader)); + } + + /// + public async ValueTask> GetId(long id, CancellationToken cancellationToken) + { + if (id != AuthenticationContext.User.GroupId && !((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers)) + return Forbid(); + + var userGroup = await userGroupsDataLoader.LoadAsync(id, cancellationToken); + if (userGroup == null) + return NotFound(); + + return new AuthorityResponse(userGroup); + } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs index fce1c12e1d..ae3dc2b0a8 100644 --- a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs @@ -13,6 +13,7 @@ using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Request; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Authority; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Controllers.Results; using Tgstation.Server.Host.Database; @@ -29,6 +30,11 @@ namespace Tgstation.Server.Host.Controllers [Route(Routes.UserGroup)] public class UserGroupController : ApiController { + /// + /// The for the . + /// + readonly IRestAuthorityInvoker userGroupAuthority; + /// /// The for the . /// @@ -39,15 +45,17 @@ namespace Tgstation.Server.Host.Controllers /// /// The for the . /// The for the . - /// The containing the value of . - /// The for the . /// The for the . + /// The for the . + /// The value of . + /// The containing the value of . public UserGroupController( IDatabaseContext databaseContext, IAuthenticationContext authenticationContext, - IOptions generalConfigurationOptions, + IApiHeadersProvider apiHeaders, ILogger logger, - IApiHeadersProvider apiHeaders) + IRestAuthorityInvoker userGroupAuthority, + IOptions generalConfigurationOptions) : base( databaseContext, authenticationContext, @@ -55,6 +63,7 @@ namespace Tgstation.Server.Host.Controllers logger, true) { + this.userGroupAuthority = userGroupAuthority ?? throw new ArgumentNullException(nameof(userGroupAuthority)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); } @@ -155,7 +164,7 @@ namespace Tgstation.Server.Host.Controllers /// Retrieve successfully. /// The requested does not currently exist. [HttpGet("{id}")] - [TgsAuthorize(AdministrationRights.ReadUsers)] + [TgsRestAuthorize(nameof(IUserGroupAuthority.GetId))] [ProducesResponseType(typeof(UserGroupResponse), 200)] [ProducesResponseType(typeof(ErrorMessageResponse), 410)] public async ValueTask GetId(long id, CancellationToken cancellationToken) diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index de1599b484..cb5b2bec4a 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -465,6 +465,7 @@ namespace Tgstation.Server.Host.Core services.AddScoped(typeof(IGraphQLAuthorityInvoker<>), typeof(GraphQLAuthorityInvoker<>)); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // configure misc services services.AddSingleton(); diff --git a/src/Tgstation.Server.Host/GraphQL/Types/SwarmNode.cs b/src/Tgstation.Server.Host/GraphQL/Types/SwarmNode.cs index 21979f2b36..520f8bd6c7 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/SwarmNode.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/SwarmNode.cs @@ -10,6 +10,7 @@ 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; using Tgstation.Server.Host.Swarm; namespace Tgstation.Server.Host.GraphQL.Types @@ -52,6 +53,7 @@ namespace Tgstation.Server.Host.GraphQL.Types /// The . /// The to load from. /// A new with the matching if found, otherwise. + [TgsGraphQLAuthorize] public static SwarmNode? GetSwarmNode( string identifier, [Service] ISwarmService swarmService) diff --git a/src/Tgstation.Server.Host/GraphQL/Types/User.cs b/src/Tgstation.Server.Host/GraphQL/Types/User.cs index baedcfaa52..0a9aa5c692 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/User.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/User.cs @@ -8,6 +8,7 @@ using HotChocolate.Types.Relay; using Tgstation.Server.Host.Authority; using Tgstation.Server.Host.GraphQL.Interfaces; using Tgstation.Server.Host.Models.Transformers; +using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.GraphQL.Types { @@ -56,6 +57,7 @@ namespace Tgstation.Server.Host.GraphQL.Types /// The . /// The for the operation. /// A resulting in the queried , if present. + [TgsGraphQLAuthorize] public static ValueTask GetUser( long id, [Service] IGraphQLAuthorityInvoker userAuthority, @@ -115,8 +117,19 @@ namespace Tgstation.Server.Host.GraphQL.Types /// /// The asociated with the user, if any. /// + /// The . + /// The for the operation. /// A resulting in the associated with the , if any. - public ValueTask Group() - => throw new NotImplementedException(); + public async ValueTask Group( + [Service] IGraphQLAuthorityInvoker userGroupAuthority, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(userGroupAuthority); + if (!GroupId.HasValue) + return null; + + return await userGroupAuthority.InvokeTransformable( + authority => authority.GetId(GroupId.Value, cancellationToken)); + } } } diff --git a/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs b/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs index 7a647e0b88..1d457be2a8 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs @@ -1,36 +1,40 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.Threading; using System.Threading.Tasks; +using HotChocolate; using HotChocolate.Types; +using HotChocolate.Types.Relay; +using Tgstation.Server.Host.Authority; + +using Tgstation.Server.Host.Models.Transformers; +using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.GraphQL.Types { /// /// Represents a group of s. /// + [Node] public sealed class UserGroup : NamedEntity { /// - /// The of the . + /// Node resolver for s. /// - readonly long permissionSetId; - - /// - /// Initializes a new instance of the class. - /// - /// The . - /// The . - /// The value of . - [SetsRequiredMembers] - public UserGroup( + /// The to lookup. + /// The . + /// The for the operation. + /// A resulting in the queried , if present. + [TgsGraphQLAuthorize] + public static ValueTask GetUserGroup( long id, - string name, - long permissionSetId) - : base(id, name) + [Service] IGraphQLAuthorityInvoker userGroupAuthority, + CancellationToken cancellationToken) { - this.permissionSetId = permissionSetId; + ArgumentNullException.ThrowIfNull(userGroupAuthority); + return userGroupAuthority.InvokeTransformable( + authority => authority.GetId(id, cancellationToken)); } /// diff --git a/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs b/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs index 3d02b0f42f..832aaa1798 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs @@ -9,6 +9,7 @@ using HotChocolate.Types.Relay; using Tgstation.Server.Host.Authority; using Tgstation.Server.Host.GraphQL.Interfaces; using Tgstation.Server.Host.Models.Transformers; +using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.GraphQL.Types { @@ -25,6 +26,7 @@ namespace Tgstation.Server.Host.GraphQL.Types /// The . /// The for the operation. /// A resulting in the queried , if present. + [TgsGraphQLAuthorize] public static async ValueTask GetUserName( long id, [Service] IGraphQLAuthorityInvoker userAuthority, diff --git a/src/Tgstation.Server.Host/Models/Transformers/UserGroupGraphQLTransformer.cs b/src/Tgstation.Server.Host/Models/Transformers/UserGroupGraphQLTransformer.cs new file mode 100644 index 0000000000..26ce4752e0 --- /dev/null +++ b/src/Tgstation.Server.Host/Models/Transformers/UserGroupGraphQLTransformer.cs @@ -0,0 +1,20 @@ +namespace Tgstation.Server.Host.Models.Transformers +{ + /// + /// for s. + /// + sealed class UserGroupGraphQLTransformer : TransformerBase + { + /// + /// Initializes a new instance of the class. + /// + public UserGroupGraphQLTransformer() + : base(model => new GraphQL.Types.UserGroup + { + Id = model.Id!.Value, + Name = model.Name!, + }) + { + } + } +} diff --git a/src/Tgstation.Server.Host/Models/UserGroup.cs b/src/Tgstation.Server.Host/Models/UserGroup.cs index c8fa3efa79..6a8e21c18c 100644 --- a/src/Tgstation.Server.Host/Models/UserGroup.cs +++ b/src/Tgstation.Server.Host/Models/UserGroup.cs @@ -5,13 +5,14 @@ using System.Linq; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Response; +using Tgstation.Server.Host.Models.Transformers; namespace Tgstation.Server.Host.Models { /// /// Represents a group of s. /// - public sealed class UserGroup : NamedEntity, ILegacyApiTransformable + public sealed class UserGroup : NamedEntity, ILegacyApiTransformable, IApiTransformable { /// /// The the has.