Implement the RightsAuthorizationHandler

Note the workaround for https://github.com/dotnet/aspnetcore/issues/56272
This commit is contained in:
Jordan Dominion
2025-04-18 15:54:55 -04:00
parent 4d53be23c6
commit b4e7c6f915
3 changed files with 137 additions and 0 deletions
@@ -33,6 +33,12 @@ namespace Tgstation.Server.Api.Rights
/// <returns>The <see cref="Enum"/> <see cref="Type"/> of the given <paramref name="rightsType"/>.</returns>
public static Type RightToType(RightsType rightsType) => TypeMap[rightsType];
/// <summary>
/// Iterate the <see cref="Type"/> of each right.
/// </summary>
/// <returns>An <see cref="IEnumerable{T}"/> of each <see cref="Type"/> of right.</returns>
public static IEnumerable<Type> AllRightTypes() => TypeMap.Values;
/// <summary>
/// Map a given <typeparamref name="TRight"/> to its respective <see cref="RightsType"/>.
/// </summary>
@@ -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<AuthenticationContextFactory>();
services.AddScoped<ITokenValidator>(provider => provider.GetRequiredService<AuthenticationContextFactory>());
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<AuthenticationContextFactory>().CurrentAuthenticationContext
@@ -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
{
/// <summary>
/// <see cref="AuthorizationHandler{TRequirement}"/> for <see cref="RightsConditional{TRights}"/>s.
/// </summary>
/// <typeparam name="TRights">The <typeparamref name="TRights"/> to evaluate.</typeparam>
public sealed class RightsAuthorizationHandler<TRights> : AuthorizationHandler<RightsConditional<TRights>>
where TRights : Enum
{
/// <summary>
/// The <see cref="IDatabaseContext"/> for the <see cref="RightsAuthorizationHandler{TRights}"/>.
/// </summary>
readonly IDatabaseContext databaseContext;
/// <summary>
/// The <see cref="IApiHeadersProvider"/> for the <see cref="RightsAuthorizationHandler{TRights}"/>.
/// </summary>
readonly IApiHeadersProvider apiHeadersProvider;
/// <summary>
/// Initializes a new instance of the <see cref="RightsAuthorizationHandler{TRights}"/> class.
/// </summary>
/// <param name="databaseContext">The value of <see cref="databaseContext"/>.</param>
/// <param name="apiHeadersProvider">The value of <see cref="apiHeadersProvider"/>.</param>
public RightsAuthorizationHandler(IDatabaseContext databaseContext, IApiHeadersProvider apiHeadersProvider)
{
this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
this.apiHeadersProvider = apiHeadersProvider ?? throw new ArgumentNullException(nameof(apiHeadersProvider));
}
/// <inheritdoc />
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, RightsConditional<TRights> requirement)
{
// https://github.com/dotnet/aspnetcore/issues/56272
CancellationToken cancellationToken = CancellationToken.None;
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(requirement);
var rightsType = RightsHelper.TypeToRight<TRights>();
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<object>())
?? throw new InvalidOperationException("A user right was null!");
if (requirement.Evaluate((TRights)right))
context.Succeed(requirement);
}
}
}