diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs index 436ae6c412..aedfa82fe2 100644 --- a/src/Tgstation.Server.Api/ApiHeaders.cs +++ b/src/Tgstation.Server.Api/ApiHeaders.cs @@ -1,5 +1,4 @@ -using Microsoft.AspNetCore.Http; -using System; +using System; using System.Globalization; using System.Linq; using System.Net.Http.Headers; @@ -21,10 +20,15 @@ namespace Tgstation.Server.Api public const string ApplicationJson = "application/json"; /// - /// The username header key + /// The header key /// const string usernameHeader = "Username"; + /// + /// The header key + /// + const string instanceIdHeader = "InstanceId"; + /// /// The JWT authentication header scheme /// @@ -40,6 +44,11 @@ namespace Tgstation.Server.Api /// static readonly AssemblyName assemblyName = Assembly.GetExecutingAssembly().GetName(); + /// + /// The being accessed + /// + public long? InstanceId { get; set; } + /// /// The client's user agent /// @@ -139,6 +148,13 @@ namespace Tgstation.Server.Api if (String.IsNullOrEmpty(parameter)) throw new InvalidOperationException("Missing authentication parameter!"); + if(requestHeaders.Headers.TryGetValue(instanceIdHeader, out StringValues instanceIdValues)) + { + var instanceIdString = instanceIdValues.FirstOrDefault(); + if (instanceIdString != default && Int64.TryParse(instanceIdString, out long instanceId)) + InstanceId = instanceId; + } + switch (scheme) { case jwtAuthenticationScheme: @@ -195,6 +211,8 @@ namespace Tgstation.Server.Api } headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent)); headers.UserAgent.Add(new ProductInfoHeaderValue(new ProductHeaderValue(assemblyName.Name, assemblyName.Version.ToString()))); + if(InstanceId.HasValue) + headers.Add(instanceIdHeader, InstanceId.ToString()); } } } diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index b21f1edad4..eccde8cde1 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -1,6 +1,5 @@ -using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; using System; using System.Linq; using System.Threading; @@ -12,6 +11,7 @@ using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Controllers { [Authorize] + [Route("[controller]")] [Produces(ApiHeaders.ApplicationJson)] [Consumes(ApiHeaders.ApplicationJson)] public abstract class ApiController : Controller @@ -22,23 +22,22 @@ namespace Tgstation.Server.Host.Controllers protected IDatabaseContext DatabaseContext { get; } - protected IAuthenticationContext AuthenticationContext { get; private set; } + protected IAuthenticationContext AuthenticationContext { get; } - readonly ITokenFactory tokenManager; + protected Instance Instance { get; } - public ApiController(IDatabaseContext databaseContext, ITokenFactory tokenManager) + public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory) { DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext)); - this.tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager)); - } + if (authenticationContextFactory == null) + throw new ArgumentNullException(nameof(authenticationContextFactory)); - public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) - { - User.Claims. - - await base.OnActionExecutionAsync(context, next).ConfigureAwait(false); - } + AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext; + if (AuthenticationContext.InstanceUser != null) + Instance = AuthenticationContext.InstanceUser.Instance; + } + [HttpPut] public virtual Task Create([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound()); diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 7da33a207b..b73932a36b 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -1,15 +1,23 @@ using Cyberboss.AspNetCore.AsyncInitializer; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using System; +using System.Collections.Generic; using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; using System.Reflection; +using System.Security.Claims; using System.Text; +using Tgstation.Server.Api; +using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; @@ -85,11 +93,49 @@ namespace Tgstation.Server.Host.Core RequireSignedTokens = true, - RequireExpirationTime = true, - + RequireExpirationTime = true }; + jwtBearerOptions.Events = new JwtBearerEvents + { + OnTokenValidated = async context => + { + var databaseContext = context.HttpContext.RequestServices.GetRequiredService(); + var authenticationContextFactory = context.HttpContext.RequestServices.GetRequiredService(); - jwtBearerOptions.SaveToken = true; + var userIdClaim = context.Principal.Claims.Where(x => x.Properties.ContainsKey(JwtRegisteredClaimNames.NameId)).FirstOrDefault(); + + 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); + } + + var requestHeaders = context.HttpContext.Request.GetTypedHeaders(); + + var apiHeaders = new ApiHeaders(requestHeaders); + + await authenticationContextFactory.CreateAuthenticationContext(userId, apiHeaders.InstanceId, context.HttpContext.RequestAborted).ConfigureAwait(false); + + var authenticationContext = authenticationContextFactory.CurrentAuthenticationContext; + + var enumerator = Enum.GetValues(typeof(RightsType)); + var claims = new List + { + Capacity = enumerator.Length + }; + foreach (RightsType I in enumerator) + claims.Add(new Claim(I.ToString(), authenticationContext.GetRight(I).ToString(CultureInfo.InvariantCulture))); + + context.Principal.AddIdentity(new ClaimsIdentity(claims)); + } + }; }); var databaseConfiguration = databaseConfigurationSection.Get(); diff --git a/src/Tgstation.Server.Host/Models/InstanceUser.cs b/src/Tgstation.Server.Host/Models/InstanceUser.cs index 7ccfc17ea0..6566b2c891 100644 --- a/src/Tgstation.Server.Host/Models/InstanceUser.cs +++ b/src/Tgstation.Server.Host/Models/InstanceUser.cs @@ -1,4 +1,6 @@ -namespace Tgstation.Server.Host.Models +using System.ComponentModel.DataAnnotations; + +namespace Tgstation.Server.Host.Models { /// public sealed class InstanceUser : Api.Models.InstanceUser @@ -7,5 +9,11 @@ /// The row Id /// public long Id { get; set; } + + /// + /// The the belongs to + /// + [Required] + public Instance Instance { get; set; } } } diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContext.cs b/src/Tgstation.Server.Host/Security/AuthenticationContext.cs index d121e81bab..f11783dba1 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContext.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContext.cs @@ -1,4 +1,6 @@ -using System; +using Microsoft.AspNetCore.Http; +using System; +using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Security @@ -37,5 +39,11 @@ namespace Tgstation.Server.Host.Security /// public IAuthenticationContext Clone() => new AuthenticationContext(SystemIdentity.Clone(), User, InstanceUser); + + /// + public long GetRight(RightsType rightsType) + { + throw new NotImplementedException(); + } } } diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs new file mode 100644 index 0000000000..f20835b583 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Security +{ + /// + sealed class AuthenticationContextFactory : IAuthenticationContextFactory + { + /// + public IAuthenticationContext CurrentAuthenticationContext { get; private set; } + + /// + /// The for the + /// + readonly ISystemIdentityFactory systemIdentityFactory; + /// + /// The for the + /// + readonly IDatabaseContext databaseContext; + + /// + /// Construct an + /// + /// The value of + /// The value of + public AuthenticationContextFactory(ISystemIdentityFactory systemIdentityFactory, IDatabaseContext databaseContext) + { + this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory)); + this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext)); + } + + /// + public async Task CreateAuthenticationContext(long userId, long? instanceId, CancellationToken cancellationToken) + { + if (CurrentAuthenticationContext != null) + throw new InvalidOperationException("Authentication context has already been loaded"); + + var userQuery = databaseContext.Users.Where(x => x.Id == userId); + + if (instanceId.HasValue) + userQuery = userQuery.Include(x => x.InstanceUsers.Where(y => y.Id == instanceId)); + + var user = await userQuery.FirstAsync(cancellationToken).ConfigureAwait(false); + + InstanceUser instanceUser = null; + if (instanceId.HasValue) + instanceUser = user.InstanceUsers.First(); + + CurrentAuthenticationContext = new AuthenticationContext(systemIdentityFactory.CreateSystemIdentity(user), user, instanceUser); + } + } +} diff --git a/src/Tgstation.Server.Host/Security/IAuthenticationContext.cs b/src/Tgstation.Server.Host/Security/IAuthenticationContext.cs index e50a3c5467..724cdf9921 100644 --- a/src/Tgstation.Server.Host/Security/IAuthenticationContext.cs +++ b/src/Tgstation.Server.Host/Security/IAuthenticationContext.cs @@ -1,4 +1,5 @@ using System; +using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Security @@ -18,6 +19,8 @@ namespace Tgstation.Server.Host.Security /// InstanceUser InstanceUser { get; } + long GetRight(RightsType rightsType); + /// /// The of if applicable /// diff --git a/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs new file mode 100644 index 0000000000..4051035c0c --- /dev/null +++ b/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs @@ -0,0 +1,15 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Security +{ + /// + /// For creating and accessing authentication contexts + /// + public interface IAuthenticationContextFactory + { + IAuthenticationContext CurrentAuthenticationContext { get; } + + Task CreateAuthenticationContext(long userId, long? instanceId, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Security/ISystemIdentityFactory.cs b/src/Tgstation.Server.Host/Security/ISystemIdentityFactory.cs index 1d5a4a53f5..d04c8d6d28 100644 --- a/src/Tgstation.Server.Host/Security/ISystemIdentityFactory.cs +++ b/src/Tgstation.Server.Host/Security/ISystemIdentityFactory.cs @@ -11,7 +11,7 @@ namespace Tgstation.Server.Host.Security /// Create a for a given /// /// The user to create a for - /// A new + /// A new or if the has no ISystemIdentity CreateSystemIdentity(User user); /// diff --git a/src/Tgstation.Server.Host/Security/ITokenFactory.cs b/src/Tgstation.Server.Host/Security/ITokenFactory.cs index fdd8949ef6..0a272438ea 100644 --- a/src/Tgstation.Server.Host/Security/ITokenFactory.cs +++ b/src/Tgstation.Server.Host/Security/ITokenFactory.cs @@ -15,7 +15,5 @@ namespace Tgstation.Server.Host.Security /// The to create the token for. Must have the and fields available /// A new Token CreateToken(Models.User user); - - Task GetUser(Token token, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Security/TokenFactory.cs b/src/Tgstation.Server.Host/Security/TokenFactory.cs index 3db336e859..628c78cb5f 100644 --- a/src/Tgstation.Server.Host/Security/TokenFactory.cs +++ b/src/Tgstation.Server.Host/Security/TokenFactory.cs @@ -46,11 +46,5 @@ namespace Tgstation.Server.Host.Security var token = new JwtSecurityToken(new JwtHeader(new SigningCredentials(key, SecurityAlgorithms.HmacSha256)), new JwtPayload(claims)); return new Token { Value = new JwtSecurityTokenHandler().WriteToken(token) }; } - - /// - public Task GetUser(Token token, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } } }