mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-07-21 13:03:41 +01:00
Implement the RightsAuthorizationHandler
Note the workaround for https://github.com/dotnet/aspnetcore/issues/56272
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user