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.