Exploratory architecture best architecture

This commit is contained in:
Cyberboss
2018-04-12 13:25:03 -04:00
parent a9cc9b96fe
commit 0d5c4c8e48
11 changed files with 176 additions and 30 deletions
+21 -3
View File
@@ -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());
+49 -3
View File
@@ -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();
}
}
}