mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-26 22:48:20 +01:00
Exploratory architecture best architecture
This commit is contained in:
@@ -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";
|
||||
|
||||
/// <summary>
|
||||
/// The username header key
|
||||
/// The <see cref="Username"/> header key
|
||||
/// </summary>
|
||||
const string usernameHeader = "Username";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="InstanceId"/> header key
|
||||
/// </summary>
|
||||
const string instanceIdHeader = "InstanceId";
|
||||
|
||||
/// <summary>
|
||||
/// The JWT authentication header scheme
|
||||
/// </summary>
|
||||
@@ -40,6 +44,11 @@ namespace Tgstation.Server.Api
|
||||
/// </summary>
|
||||
static readonly AssemblyName assemblyName = Assembly.GetExecutingAssembly().GetName();
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Models.Instance.Id"/> being accessed
|
||||
/// </summary>
|
||||
public long? InstanceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The client's user agent
|
||||
/// </summary>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TModel> : 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<IActionResult> Create([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
|
||||
|
||||
|
||||
@@ -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<IDatabaseContext>();
|
||||
var authenticationContextFactory = context.HttpContext.RequestServices.GetRequiredService<IAuthenticationContextFactory>();
|
||||
|
||||
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<Claim>
|
||||
{
|
||||
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<DatabaseConfiguration>();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Tgstation.Server.Host.Models
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public sealed class InstanceUser : Api.Models.InstanceUser
|
||||
@@ -7,5 +9,11 @@
|
||||
/// The row Id
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Models.Instance"/> the <see cref="InstanceUser"/> belongs to
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Instance Instance { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAuthenticationContext Clone() => new AuthenticationContext(SystemIdentity.Clone(), User, InstanceUser);
|
||||
|
||||
/// <inheritdoc />
|
||||
public long GetRight(RightsType rightsType)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class AuthenticationContextFactory : IAuthenticationContextFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IAuthenticationContext CurrentAuthenticationContext { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ISystemIdentityFactory"/> for the <see cref="AuthenticationContextFactory"/>
|
||||
/// </summary>
|
||||
readonly ISystemIdentityFactory systemIdentityFactory;
|
||||
/// <summary>
|
||||
/// The <see cref="IDatabaseContext"/> for the <see cref="AuthenticationContextFactory"/>
|
||||
/// </summary>
|
||||
readonly IDatabaseContext databaseContext;
|
||||
|
||||
/// <summary>
|
||||
/// Construct an <see cref="AuthenticationContextFactory"/>
|
||||
/// </summary>
|
||||
/// <param name="systemIdentityFactory">The value of <see cref="systemIdentityFactory"/></param>
|
||||
/// <param name="databaseContext">The value of <see cref="databaseContext"/></param>
|
||||
public AuthenticationContextFactory(ISystemIdentityFactory systemIdentityFactory, IDatabaseContext databaseContext)
|
||||
{
|
||||
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
|
||||
this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// </summary>
|
||||
InstanceUser InstanceUser { get; }
|
||||
|
||||
long GetRight(RightsType rightsType);
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ISystemIdentity"/> of <see cref="User"/> if applicable
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// For creating and accessing authentication contexts
|
||||
/// </summary>
|
||||
public interface IAuthenticationContextFactory
|
||||
{
|
||||
IAuthenticationContext CurrentAuthenticationContext { get; }
|
||||
|
||||
Task CreateAuthenticationContext(long userId, long? instanceId, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ namespace Tgstation.Server.Host.Security
|
||||
/// Create a <see cref="ISystemIdentity"/> for a given <paramref name="user"/>
|
||||
/// </summary>
|
||||
/// <param name="user">The user to create a <see cref="ISystemIdentity"/> for</param>
|
||||
/// <returns>A new <see cref="ISystemIdentity"/></returns>
|
||||
/// <returns>A new <see cref="ISystemIdentity"/> or <see langword="null"/> if the <paramref name="user"/> has no <see cref="ISystemIdentity"/></returns>
|
||||
ISystemIdentity CreateSystemIdentity(User user);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -15,7 +15,5 @@ namespace Tgstation.Server.Host.Security
|
||||
/// <param name="user">The <see cref="Models.User"/> to create the token for. Must have the <see cref="Api.Models.Internal.User.Id"/> and <see cref="Models.User.TokenSecret"/> fields available</param>
|
||||
/// <returns>A new <see cref="Token"/></returns>
|
||||
Token CreateToken(Models.User user);
|
||||
|
||||
Task<User> GetUser(Token token, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<User> GetUser(Token token, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user