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()) + ?? throw new InvalidOperationException("A user right was null!"); + + if (requirement.Evaluate((TRights)right)) + context.Succeed(requirement); + } + } +}