diff --git a/src/Tgstation.Server.Api/Rights/RightsHelper.cs b/src/Tgstation.Server.Api/Rights/RightsHelper.cs
index 424d4608d5..ec1d0fad08 100644
--- a/src/Tgstation.Server.Api/Rights/RightsHelper.cs
+++ b/src/Tgstation.Server.Api/Rights/RightsHelper.cs
@@ -33,6 +33,12 @@ namespace Tgstation.Server.Api.Rights
/// The of the given .
public static Type RightToType(RightsType rightsType) => TypeMap[rightsType];
+ ///
+ /// Iterate the of each right.
+ ///
+ /// An of each of right.
+ public static IEnumerable AllRightTypes() => TypeMap.Values;
+
///
/// Map a given to its respective .
///
diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs
index c4958767ee..9d764c9ce1 100644
--- a/src/Tgstation.Server.Host/Core/Application.cs
+++ b/src/Tgstation.Server.Host/Core/Application.cs
@@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Hosting;
@@ -45,6 +46,7 @@ using Serilog.Sinks.Elasticsearch;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Hubs;
using Tgstation.Server.Api.Models;
+using Tgstation.Server.Api.Rights;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Host.Authority;
using Tgstation.Server.Host.Authority.Core;
@@ -811,6 +813,10 @@ namespace Tgstation.Server.Host.Core
services.AddScoped();
services.AddScoped(provider => provider.GetRequiredService());
+ var genericRightsAuthHandler = typeof(RightsAuthorizationHandler<>);
+ foreach (var rightType in RightsHelper.AllRightTypes())
+ services.AddScoped(typeof(IAuthorizationHandler), genericRightsAuthHandler.MakeGenericType(rightType));
+
// what if you
// wanted to just do this:
// return provider.GetRequiredService().CurrentAuthenticationContext
diff --git a/src/Tgstation.Server.Host/Security/RightsAuthorizationHandler.cs b/src/Tgstation.Server.Host/Security/RightsAuthorizationHandler.cs
new file mode 100644
index 0000000000..995ce44e1d
--- /dev/null
+++ b/src/Tgstation.Server.Host/Security/RightsAuthorizationHandler.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.EntityFrameworkCore;
+
+using Tgstation.Server.Api.Models;
+using Tgstation.Server.Api.Models.Internal;
+using Tgstation.Server.Api.Rights;
+using Tgstation.Server.Host.Database;
+using Tgstation.Server.Host.Extensions;
+using Tgstation.Server.Host.Security.RightsEvaluation;
+using Tgstation.Server.Host.Utils;
+
+namespace Tgstation.Server.Host.Security
+{
+ ///
+ /// for s.
+ ///
+ /// The to evaluate.
+ public sealed class RightsAuthorizationHandler : AuthorizationHandler>
+ where TRights : Enum
+ {
+ ///
+ /// The for the .
+ ///
+ readonly IDatabaseContext databaseContext;
+
+ ///
+ /// The for the .
+ ///
+ readonly IApiHeadersProvider apiHeadersProvider;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The value of .
+ /// The value of .
+ public RightsAuthorizationHandler(IDatabaseContext databaseContext, IApiHeadersProvider apiHeadersProvider)
+ {
+ this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
+ this.apiHeadersProvider = apiHeadersProvider ?? throw new ArgumentNullException(nameof(apiHeadersProvider));
+ }
+
+ ///
+ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, RightsConditional requirement)
+ {
+ // https://github.com/dotnet/aspnetcore/issues/56272
+ CancellationToken cancellationToken = CancellationToken.None;
+
+ ArgumentNullException.ThrowIfNull(context);
+ ArgumentNullException.ThrowIfNull(requirement);
+
+ var rightsType = RightsHelper.TypeToRight();
+ var isInstance = RightsHelper.IsInstanceRight(rightsType);
+ var userId = context.User.GetTgsUserId();
+
+ object? permissionSet;
+ if (isInstance)
+ {
+ var apiHeaders = apiHeadersProvider.ApiHeaders;
+ if (apiHeaders == null)
+ throw new InvalidOperationException("API headers should have been validated at this point!");
+
+ if (!apiHeaders.InstanceId.HasValue)
+ throw new InvalidOperationException("Instance ID header should have been validated at this point!");
+
+ var queryableUsers = databaseContext
+ .Users
+ .AsQueryable();
+
+ var matchingUniquePermissionSetIds = queryableUsers
+ .Where(user => user.Id == userId && user.PermissionSet != null)
+ .Select(user => user.PermissionSet!.Id);
+
+ var matchingGroupPermissionSetIds = queryableUsers
+ .Where(user => user.Id == userId && user.Group != null)
+ .Select(user => user.Group!.PermissionSet!.Id);
+
+ permissionSet = await databaseContext
+ .InstancePermissionSets
+ .AsQueryable()
+ .Where(ips => ips.InstanceId == apiHeaders.InstanceId.Value
+ && (matchingUniquePermissionSetIds.Contains(ips.PermissionSetId) || matchingGroupPermissionSetIds.Contains(ips.PermissionSetId)))
+ .TagWith("rights_authorization_handler_instance_permission_set")
+ .FirstOrDefaultAsync(cancellationToken);
+ }
+ else
+ permissionSet = await databaseContext
+ .PermissionSets
+ .AsQueryable()
+ .Where(permissionSet => permissionSet.UserId == userId)
+ .TagWith("rights_authorization_handler_permission_set")
+ .FirstOrDefaultAsync(cancellationToken);
+
+ if (permissionSet == null)
+ return; // fail
+
+ // use the api versions because they're the ones that contain the actual properties
+ var requiredPermissionSetType = isInstance ? typeof(InstancePermissionSet) : typeof(PermissionSet);
+
+ var rightsClrType = typeof(TRights);
+ var nullableRightsType = typeof(Nullable<>).MakeGenericType(rightsClrType);
+
+ var rightPropertyInfo = requiredPermissionSetType
+ .GetProperties()
+ .Where(propertyInfo => propertyInfo.PropertyType == nullableRightsType && propertyInfo.CanRead)
+ .Single();
+
+ var rightPropertyGetMethod = rightPropertyInfo.GetMethod;
+ if (rightPropertyGetMethod == null)
+ throw new InvalidOperationException($"Rights property {rightPropertyInfo.Name} on {rightsClrType.FullName} has no getter!");
+
+ var right = rightPropertyGetMethod.Invoke(
+ permissionSet,
+ Array.Empty