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();
- }
}
}