From 4f99568d412d8b965fcfbce71fcb29b1d3a8a42f Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Tue, 18 Sep 2018 10:40:14 -0400 Subject: [PATCH 1/3] Replace IServiceProvider parameter in job execution with IDatabaseContext --- .../Components/IInstance.cs | 5 +- .../Components/Instance.cs | 14 ++- .../Components/Watchdog/Watchdog.cs | 2 +- .../Controllers/ByondController.cs | 2 +- .../Controllers/DreamDaemonController.cs | 4 +- .../Controllers/InstanceController.cs | 2 +- .../Controllers/RepositoryController.cs | 17 ++-- src/Tgstation.Server.Host/Core/IJobManager.cs | 4 +- src/Tgstation.Server.Host/Core/JobManager.cs | 86 ++++++++----------- 9 files changed, 59 insertions(+), 77 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/IInstance.cs b/src/Tgstation.Server.Host/Components/IInstance.cs index 921a408266..d90826f9b4 100644 --- a/src/Tgstation.Server.Host/Components/IInstance.cs +++ b/src/Tgstation.Server.Host/Components/IInstance.cs @@ -4,7 +4,6 @@ using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; -using Tgstation.Server.Host.Components.Compiler; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.StaticFiles; using Tgstation.Server.Host.Components.Watchdog; @@ -65,10 +64,10 @@ namespace Tgstation.Server.Host.Components /// Run the compile job and insert it into the database. Meant to be called by a /// /// The running - /// The for the operation + /// The for the operation /// The to report compilation progress /// The for the operation /// A representing the running operation - Task CompileProcess(Job job, IServiceProvider serviceProvider, Action progressReporter, CancellationToken cancellationToken); + Task CompileProcess(Job job, IDatabaseContext databaseContext, Action progressReporter, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index 79faad2b45..708725c349 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -1,5 +1,4 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; using System.Linq; @@ -117,18 +116,16 @@ namespace Tgstation.Server.Host.Components } /// - public async Task CompileProcess(Job job, IServiceProvider serviceProvider, Action progressReporter, CancellationToken cancellationToken) + public async Task CompileProcess(Job job, IDatabaseContext databaseContext, Action progressReporter, CancellationToken cancellationToken) { //DO NOT FOLLOW THE SUGGESTION FOR A THROW EXPRESSION HERE if (job == null) throw new ArgumentNullException(nameof(job)); - if (serviceProvider == null) - throw new ArgumentNullException(nameof(serviceProvider)); + if (databaseContext == null) + throw new ArgumentNullException(nameof(databaseContext)); if (progressReporter == null) throw new ArgumentNullException(nameof(progressReporter)); - var databaseContext = serviceProvider.GetRequiredService(); - var ddSettingsTask = databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == metadata.Id).Select(x => new DreamDaemonSettings { StartupTimeout = x.StartupTimeout, @@ -205,10 +202,9 @@ namespace Tgstation.Server.Host.Components }; var noRepo = false; - await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, serviceProvider, progressReporter, jobCancellationToken) => + await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, databaseContext, progressReporter, jobCancellationToken) => { - var db = serviceProvider.GetRequiredService(); - var repositorySettingsTask = db.RepositorySettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(jobCancellationToken); + var repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(jobCancellationToken); //assume 5 steps with synchronize const int ProgressSections = 5; diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs index 28a1eeb60c..412612e49c 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs @@ -836,7 +836,7 @@ namespace Tgstation.Server.Host.Components.Watchdog CancelRight = (ulong)DreamDaemonRights.Shutdown, CancelRightsType = RightsType.DreamDaemon }; - await jobManager.RegisterOperation(job, (j, serviceProvider, progressFunction, ct) => Launch(ct), cancellationToken).ConfigureAwait(false); + await jobManager.RegisterOperation(job, (j, databaseContext, progressFunction, ct) => Launch(ct), cancellationToken).ConfigureAwait(false); } /// diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs index 566aaae2b0..f8be5a0174 100644 --- a/src/Tgstation.Server.Host/Controllers/ByondController.cs +++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs @@ -95,7 +95,7 @@ namespace Tgstation.Server.Host.Controllers CancelRight = (ulong)ByondRights.CancelInstall, Instance = Instance }; - await jobManager.RegisterOperation(job, (paramJob, serviceProvicer, progressHandler, ct) => byondManager.ChangeVersion(installingVersion, ct), cancellationToken).ConfigureAwait(false); + await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, ct) => byondManager.ChangeVersion(installingVersion, ct), cancellationToken).ConfigureAwait(false); result.InstallJob = job.ToApi(); } result.Version = byondManager.ActiveVersion; diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index 85d7841d8c..c13ec7e820 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -67,7 +67,7 @@ namespace Tgstation.Server.Host.Controllers StartedBy = AuthenticationContext.User }; await jobManager.RegisterOperation(job, - async (paramJob, serviceProvider, progressHandler, innerCt) => + async (paramJob, databaseContext, progressHandler, innerCt) => { var result = await instance.Watchdog.Launch(innerCt).ConfigureAwait(false); if (result == null) @@ -231,7 +231,7 @@ namespace Tgstation.Server.Host.Controllers var watchdog = instanceManager.GetInstance(Instance).Watchdog; - await jobManager.RegisterOperation(job, (paramJob, serviceProvider, progressReporter, ct) => watchdog.Restart(false, ct), cancellationToken).ConfigureAwait(false); + await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressReporter, ct) => watchdog.Restart(false, ct), cancellationToken).ConfigureAwait(false); return Accepted(job.ToApi()); } } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index aafd0da61e..0c6801cbf2 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -352,7 +352,7 @@ namespace Tgstation.Server.Host.Controllers StartedBy = AuthenticationContext.User }; - await jobManager.RegisterOperation(job, (paramJob, serviceProvider, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(false); + await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(false); api.MoveJob = job.ToApi(); } diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index 20f87f6cd1..240227aaaf 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; @@ -186,20 +185,19 @@ namespace Tgstation.Server.Host.Controllers Instance = Instance }; var api = currentModel.ToApi(); - await jobManager.RegisterOperation(job, async (paramJob, serviceProvider, progressReporter, ct) => + await jobManager.RegisterOperation(job, async (paramJob, databaseContext, progressReporter, ct) => { using (var repos = await repoManager.CloneRepository(new Uri(origin), cloneBranch, currentModel.AccessUser, currentModel.AccessToken, progressReporter, ct).ConfigureAwait(false)) { if (repos == null) throw new JobException("Filesystem conflict while cloning repository!"); - var db = serviceProvider.GetRequiredService(); var instance = new Models.Instance { Id = Instance.Id }; - db.Instances.Attach(instance); - if (await PopulateApi(api, repos, db, instance, ct).ConfigureAwait(false)) - await db.Save(ct).ConfigureAwait(false); + databaseContext.Instances.Attach(instance); + if (await PopulateApi(api, repos, databaseContext, instance, ct).ConfigureAwait(false)) + await databaseContext.Save(ct).ConfigureAwait(false); } }, cancellationToken).ConfigureAwait(false); @@ -238,7 +236,7 @@ namespace Tgstation.Server.Host.Controllers Instance = Instance }; var api = currentModel.ToApi(); - await jobManager.RegisterOperation(job, (paramJob, serviceProvider, progressReporter, ct) => instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken), cancellationToken).ConfigureAwait(false); + await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressReporter, ct) => instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken), cancellationToken).ConfigureAwait(false); api.ActiveJob = job.ToApi(); return Accepted(api); } @@ -419,7 +417,7 @@ namespace Tgstation.Server.Host.Controllers CancelRight = (ulong)RepositoryRights.CancelPendingChanges, }; - await jobManager.RegisterOperation(job, async (paramJob, serviceProvider, progressReporter, ct) => + await jobManager.RegisterOperation(job, async (paramJob, databaseContext, progressReporter, ct) => { using (var repo = await repoManager.LoadRepository(ct).ConfigureAwait(false)) { @@ -443,8 +441,7 @@ namespace Tgstation.Server.Host.Controllers //get a base line for where we are Models.RevisionInformation lastRevisionInfo = null; - - var databaseContext = serviceProvider.GetRequiredService(); + var attachedInstance = new Models.Instance { Id = Instance.Id diff --git a/src/Tgstation.Server.Host/Core/IJobManager.cs b/src/Tgstation.Server.Host/Core/IJobManager.cs index 7389ce78d5..12ecf7666f 100644 --- a/src/Tgstation.Server.Host/Core/IJobManager.cs +++ b/src/Tgstation.Server.Host/Core/IJobManager.cs @@ -20,10 +20,10 @@ namespace Tgstation.Server.Host.Core /// Registers a given and begins running it /// /// The - /// The operation to run taking the started , a progress reporter and a + /// The operation to run taking the started , a , progress reporter and a /// The for the operation /// A representing a running operation - Task RegisterOperation(Job job, Func, CancellationToken, Task> operation, CancellationToken cancellationToken); + Task RegisterOperation(Job job, Func, CancellationToken, Task> operation, CancellationToken cancellationToken); /// /// Wait for a given to complete diff --git a/src/Tgstation.Server.Host/Core/JobManager.cs b/src/Tgstation.Server.Host/Core/JobManager.cs index 304f2d0acd..2900dc28f4 100644 --- a/src/Tgstation.Server.Host/Core/JobManager.cs +++ b/src/Tgstation.Server.Host/Core/JobManager.cs @@ -1,5 +1,4 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -16,7 +15,7 @@ namespace Tgstation.Server.Host.Core /// /// The for the /// - readonly IServiceProvider serviceProvider; + readonly IDatabaseContextFactory databaseContextFactory; /// /// The for the @@ -31,11 +30,11 @@ namespace Tgstation.Server.Host.Core /// /// Construct a /// - /// The value of + /// The value of /// The value of - public JobManager(IServiceProvider serviceProvider, ILogger logger) + public JobManager(IDatabaseContextFactory databaseContextFactory, ILogger logger) { - this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); jobs = new Dictionary(); } @@ -69,11 +68,11 @@ namespace Tgstation.Server.Host.Core /// The operation for the /// The for the operation /// A representing the running operation - async Task RunJob(Job job, Func operation, CancellationToken cancellationToken) + async Task RunJob(Job job, Func operation, CancellationToken cancellationToken) { try { - using (var scope = serviceProvider.CreateScope()) + await databaseContextFactory.UseContext(async databaseContext => { async Task HandleExceptions(Task task) { @@ -97,15 +96,13 @@ namespace Tgstation.Server.Host.Core } } - IDatabaseContext databaseContext = null; async Task RunJobInternal() { var oldJob = job; job = new Job { Id = oldJob.Id }; - databaseContext = scope.ServiceProvider.GetRequiredService(); databaseContext.Jobs.Attach(job); - await operation(job, scope.ServiceProvider, cancellationToken).ConfigureAwait(false); + await operation(job, databaseContext, cancellationToken).ConfigureAwait(false); logger.LogDebug("Job {0} completed!", job.Id); }; @@ -123,7 +120,7 @@ namespace Tgstation.Server.Host.Core if (JobErroredOrCancelled()) await databaseContext.Save(default).ConfigureAwait(false); } - } + }).ConfigureAwait(false); } finally { @@ -137,50 +134,44 @@ namespace Tgstation.Server.Host.Core } /// - public async Task RegisterOperation(Job job, Func, CancellationToken, Task> operation, CancellationToken cancellationToken) + public Task RegisterOperation(Job job, Func, CancellationToken, Task> operation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext => { - using (var scope = serviceProvider.CreateScope()) + job.StartedAt = DateTimeOffset.Now; + job.Cancelled = false; + job.Instance = new Instance { - var databaseContext = scope.ServiceProvider.GetRequiredService(); - job.StartedAt = DateTimeOffset.Now; - job.Cancelled = false; - job.Instance = new Instance + Id = job.Instance.Id + }; + databaseContext.Instances.Attach(job.Instance); + if (job.StartedBy != null) + { + job.StartedBy = new User { - Id = job.Instance.Id + Id = job.StartedBy.Id }; - databaseContext.Instances.Attach(job.Instance); - if (job.StartedBy != null) - { - job.StartedBy = new User - { - Id = job.StartedBy.Id - }; - databaseContext.Users.Attach(job.StartedBy); - } - databaseContext.Jobs.Add(job); - await databaseContext.Save(cancellationToken).ConfigureAwait(false); - logger.LogDebug("Starting job {0}: {1}...", job.Id, job.Description); - var jobHandler = JobHandler.Create(x => RunJob(job, (jobParam, serviceProvider, ct) => - operation(jobParam, serviceProvider, y => - { - lock (this) - if (jobs.TryGetValue(job.Id, out var handler)) - handler.Progress = y; - }, ct), - x)); - lock (this) - jobs.Add(job.Id, jobHandler); + databaseContext.Users.Attach(job.StartedBy); } - } + databaseContext.Jobs.Add(job); + await databaseContext.Save(cancellationToken).ConfigureAwait(false); + logger.LogDebug("Starting job {0}: {1}...", job.Id, job.Description); + var jobHandler = JobHandler.Create(x => RunJob(job, (jobParam, serviceProvider, ct) => + operation(jobParam, serviceProvider, y => + { + lock (this) + if (jobs.TryGetValue(job.Id, out var handler)) + handler.Progress = y; + }, ct), + x)); + lock (this) + jobs.Add(job.Id, jobHandler); + }); /// public async Task StartAsync(CancellationToken cancellationToken) { logger.LogTrace("Starting job manager..."); - using (var scope = serviceProvider.CreateScope()) + await databaseContextFactory.UseContext(async databaseContext => { - var databaseContext = scope.ServiceProvider.GetRequiredService(); - //mark all jobs as cancelled var badJobs = await databaseContext.Jobs.Where(y => !y.StoppedAt.HasValue).Select(y => y.Id).ToListAsync(cancellationToken).ConfigureAwait(false); if (badJobs.Count > 0) @@ -195,7 +186,7 @@ namespace Tgstation.Server.Host.Core } await databaseContext.Save(cancellationToken).ConfigureAwait(false); } - } + }).ConfigureAwait(false); logger.LogDebug("Job manager started!"); } @@ -228,9 +219,8 @@ namespace Tgstation.Server.Host.Core return false; } handler.Cancel(); //this will ensure the db update is only done once - using (var scope = serviceProvider.CreateScope()) + await databaseContextFactory.UseContext(async databaseContext => { - var databaseContext = scope.ServiceProvider.GetRequiredService(); job = new Job { Id = job.Id }; databaseContext.Jobs.Attach(job); user = new User { Id = user.Id }; @@ -238,7 +228,7 @@ namespace Tgstation.Server.Host.Core job.CancelledBy = user; //let either startup or cancellation set job.cancelled await databaseContext.Save(cancellationToken).ConfigureAwait(false); - } + }).ConfigureAwait(false); if (blocking) await handler.Wait(cancellationToken).ConfigureAwait(false); return true; From 16c6d41e09f33d833e6238066c3ab6f952215620 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Tue, 18 Sep 2018 11:09:23 -0400 Subject: [PATCH 2/3] Change DatabaseContextFactory to use IServiceScopeFactory. Perform some constructor time validation --- .../Core/DatabaseContextFactory.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Tgstation.Server.Host/Core/DatabaseContextFactory.cs b/src/Tgstation.Server.Host/Core/DatabaseContextFactory.cs index f3d64f355c..69c5287aa9 100644 --- a/src/Tgstation.Server.Host/Core/DatabaseContextFactory.cs +++ b/src/Tgstation.Server.Host/Core/DatabaseContextFactory.cs @@ -9,20 +9,26 @@ namespace Tgstation.Server.Host.Core sealed class DatabaseContextFactory : IDatabaseContextFactory { /// - /// The for the + /// The for the /// - readonly IServiceProvider serviceProvider; + readonly IServiceScopeFactory scopeFactory; /// /// Construct a /// - /// The value of - public DatabaseContextFactory(IServiceProvider serviceProvider) => this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + /// The value of . Created scopes must be able to provide instances of + public DatabaseContextFactory(IServiceScopeFactory scopeFactory) + { + this.scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); + + using (var scope = scopeFactory.CreateScope()) + scope.ServiceProvider.GetRequiredService(); + } /// public async Task UseContext(Func operation) { - using (var scope = serviceProvider.CreateScope()) + using (var scope = scopeFactory.CreateScope()) await operation(scope.ServiceProvider.GetRequiredService()).ConfigureAwait(false); } } From cd0df312cbc7c81af3b487ab6762c9b2ee8c11a2 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Tue, 18 Sep 2018 11:40:13 -0400 Subject: [PATCH 3/3] Create a custom injected class for claims injection --- .../Controllers/ApiController.cs | 66 +------------ src/Tgstation.Server.Host/Core/Application.cs | 6 +- .../Security/ClaimsInjector.cs | 95 +++++++++++++++++++ .../Security/IClaimsInjector.cs | 20 ++++ 4 files changed, 121 insertions(+), 66 deletions(-) create mode 100644 src/Tgstation.Server.Host/Security/ClaimsInjector.cs create mode 100644 src/Tgstation.Server.Host/Security/IClaimsInjector.cs diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index 16fa1704dd..0fc660be6c 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -1,20 +1,14 @@ -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.Globalization; -using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Net; -using System.Security.Claims; using System.Threading.Tasks; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; -using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; @@ -57,64 +51,6 @@ namespace Tgstation.Server.Host.Controllers /// readonly bool requireInstance; - /// - /// Runs after a has been validated. Creates the for the - /// - /// The for the operation - /// A representing the running operation - public static async Task OnTokenValidated(TokenValidatedContext context) - { - var databaseContext = context.HttpContext.RequestServices.GetRequiredService(); - var authenticationContextFactory = context.HttpContext.RequestServices.GetRequiredService(); - - var userIdClaim = context.Principal.FindFirst(JwtRegisteredClaimNames.Sub); - - if (userIdClaim == default(Claim)) - throw new InvalidOperationException("Missing required claim!"); - - long userId; - try - { - userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture); - } - catch (Exception e) - { - throw new InvalidOperationException("Failed to parse user ID!", e); - } - - ApiHeaders apiHeaders; - try - { - apiHeaders = new ApiHeaders(context.HttpContext.Request.GetTypedHeaders()); - } - catch - { - //let OnActionExecutionAsync handle the reponse - return; - } - - await authenticationContextFactory.CreateAuthenticationContext(userId, apiHeaders.InstanceId, context.SecurityToken.ValidFrom, context.HttpContext.RequestAborted).ConfigureAwait(false); - - var authenticationContext = authenticationContextFactory.CurrentAuthenticationContext; - - var enumerator = Enum.GetValues(typeof(RightsType)); - var claims = new List(); - foreach (RightsType I in enumerator) - { - //if there's no instance user, do a weird thing and add all the instance roles - //we need it so we can get to OnActionExecutionAsync where we can properly decide between BadRequest and Forbid - //if user is null that means they got the token with an expired password - var rightInt = authenticationContext.User == null || (RightsHelper.IsInstanceRight(I) && authenticationContext.InstanceUser == null) ? ~0U : authenticationContext.GetRight(I); - var rightEnum = RightsHelper.RightToType(I); - var right = (Enum)Enum.ToObject(rightEnum, rightInt); - foreach (Enum J in Enum.GetValues(rightEnum)) - if (right.HasFlag(J)) - claims.Add(new Claim(ClaimTypes.Role, RightsHelper.RoleName(I, J))); - } - - context.Principal.AddIdentity(new ClaimsIdentity(claims)); - } - /// /// Construct an /// diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 86475ae07c..6233129ca1 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -101,6 +101,8 @@ namespace Tgstation.Server.Host.Core services.AddOptions(); + services.AddScoped(); + const string scheme = "JwtBearer"; services.AddAuthentication((options) => { @@ -128,9 +130,11 @@ namespace Tgstation.Server.Host.Core }; jwtBearerOptions.Events = new JwtBearerEvents { - OnTokenValidated = ApiController.OnTokenValidated + //Application is our composition root so this monstrosity of a line is okay + OnTokenValidated = ctx => ctx.HttpContext.RequestServices.GetRequiredService().InjectClaimsIntoContext(ctx, ctx.HttpContext.RequestAborted) }; }); + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); //fucking converts 'sub' to M$ bs services.AddMvc().AddJsonOptions(options => diff --git a/src/Tgstation.Server.Host/Security/ClaimsInjector.cs b/src/Tgstation.Server.Host/Security/ClaimsInjector.cs new file mode 100644 index 0000000000..3e7f202b2e --- /dev/null +++ b/src/Tgstation.Server.Host/Security/ClaimsInjector.cs @@ -0,0 +1,95 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api; +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Security +{ + /// + sealed class ClaimsInjector : IClaimsInjector + { + /// + /// The for the + /// + readonly IDatabaseContext databaseContext; + + /// + /// The for the + /// + readonly IAuthenticationContextFactory authenticationContextFactory; + + /// + /// Construct a + /// + /// The value of + /// The value of + public ClaimsInjector(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory) + { + this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext)); + this.authenticationContextFactory = authenticationContextFactory ?? throw new ArgumentNullException(nameof(authenticationContextFactory)); + } + + /// + public async Task InjectClaimsIntoContext(TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken) + { + if (tokenValidatedContext == null) + throw new ArgumentNullException(nameof(tokenValidatedContext)); + + //Find the user id in the token + var userIdClaim = tokenValidatedContext.Principal.FindFirst(JwtRegisteredClaimNames.Sub); + if (userIdClaim == default) + throw new InvalidOperationException("Missing required claim!"); + + long userId; + try + { + userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture); + } + catch (Exception e) + { + throw new InvalidOperationException("Failed to parse user ID!", e); + } + + ApiHeaders apiHeaders; + try + { + apiHeaders = new ApiHeaders(tokenValidatedContext.HttpContext.Request.GetTypedHeaders()); + } + catch (InvalidOperationException) + { + //we are not responsible for handling header validation issues + return; + } + + //This populates the CurrentAuthenticationContext field for use by us and subsequent controllers + await authenticationContextFactory.CreateAuthenticationContext(userId, apiHeaders.InstanceId, tokenValidatedContext.SecurityToken.ValidFrom, cancellationToken).ConfigureAwait(false); + + var authenticationContext = authenticationContextFactory.CurrentAuthenticationContext; + + var enumerator = Enum.GetValues(typeof(RightsType)); + var claims = new List(); + foreach (RightsType I in enumerator) + { + //if there's no instance user, do a weird thing and add all the instance roles + //we need it so we can get to OnActionExecutionAsync where we can properly decide between BadRequest and Forbid + //if user is null that means they got the token with an expired password + var rightInt = authenticationContext.User == null || (RightsHelper.IsInstanceRight(I) && authenticationContext.InstanceUser == null) ? ~0U : authenticationContext.GetRight(I); + var rightEnum = RightsHelper.RightToType(I); + var right = (Enum)Enum.ToObject(rightEnum, rightInt); + foreach (Enum J in Enum.GetValues(rightEnum)) + if (right.HasFlag(J)) + claims.Add(new Claim(ClaimTypes.Role, RightsHelper.RoleName(I, J))); + } + + tokenValidatedContext.Principal.AddIdentity(new ClaimsIdentity(claims)); + } + } +} diff --git a/src/Tgstation.Server.Host/Security/IClaimsInjector.cs b/src/Tgstation.Server.Host/Security/IClaimsInjector.cs new file mode 100644 index 0000000000..216b03b0f0 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/IClaimsInjector.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Security +{ + /// + /// For injecting s that can look for + /// + interface IClaimsInjector + { + /// + /// Setup the s for a given + /// + /// The containing the and of the request and the to add s to + /// The for the operation + /// A representing the running operation + Task InjectClaimsIntoContext(TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken); + } +} \ No newline at end of file