diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs
index 4c6bcd541f..30d503f871 100644
--- a/src/Tgstation.Server.Host/Controllers/ApiController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs
@@ -115,6 +115,13 @@ namespace Tgstation.Server.Host.Controllers
///
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
+ if (AuthenticationContext == null)
+ {
+ //accessing an instance they don't have access to
+ await Forbid().ExecuteResultAsync(context).ConfigureAwait(false);
+ return;
+ }
+
//validate the headers
try
{
diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs
index a1849bfc86..414593875e 100644
--- a/src/Tgstation.Server.Host/Controllers/HomeController.cs
+++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs
@@ -2,6 +2,7 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
+using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
@@ -33,6 +34,10 @@ namespace Tgstation.Server.Host.Controllers
/// The for the
///
readonly IApplication application;
+ ///
+ /// The for the
+ ///
+ readonly IAuthenticationContextFactory authenticationContextFactory;
///
/// Construct a
@@ -49,6 +54,8 @@ namespace Tgstation.Server.Host.Controllers
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
this.application = application ?? throw new ArgumentNullException(nameof(application));
+ //base checks not null
+ this.authenticationContextFactory = authenticationContextFactory;
}
///
@@ -81,6 +88,7 @@ namespace Tgstation.Server.Host.Controllers
if (user == null)
return Unauthorized();
+ ISystemIdentity identity = null;
if (user.PasswordHash != null)
{
var originalHash = user.PasswordHash;
@@ -101,17 +109,23 @@ namespace Tgstation.Server.Host.Controllers
else
try
{
- using (await systemIdentityFactory.CreateSystemIdentity(user.Name, ApiHeaders.Password, cancellationToken).ConfigureAwait(false)) { }
+ identity = await systemIdentityFactory.CreateSystemIdentity(user.Name, ApiHeaders.Password, cancellationToken).ConfigureAwait(false);
+ if (identity == null || identity.Uid != user.SystemIdentifier)
+ return Unauthorized();
}
- catch
+ catch (NotImplementedException)
{
- return Unauthorized();
+ return StatusCode((int)HttpStatusCode.NotImplemented);
}
+ using (identity) {
+ if (!user.Enabled.Value)
+ return Forbid();
- if (!user.Enabled.Value)
- return Forbid();
-
- return Json(tokenFactory.CreateToken(user));
+ var token = tokenFactory.CreateToken(user, out var expiry);
+ if (identity != null)
+ authenticationContextFactory.CacheSystemIdentity(user, identity, expiry.AddSeconds(10)); //expire the identity slightly after the auth token in case of lag
+ return Json(token);
+ }
}
}
}
diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs
index 59638e2026..376ccd3b26 100644
--- a/src/Tgstation.Server.Host/Controllers/UserController.cs
+++ b/src/Tgstation.Server.Host/Controllers/UserController.cs
@@ -82,6 +82,8 @@ namespace Tgstation.Server.Host.Controllers
{
using (var sysIdentity = await systemIdentityFactory.CreateSystemIdentity(dbUser, cancellationToken).ConfigureAwait(false))
{
+ if (sysIdentity == null)
+ return StatusCode((int)HttpStatusCode.Gone);
dbUser.Name = sysIdentity.Username;
dbUser.SystemIdentifier = sysIdentity.Uid;
}
@@ -90,11 +92,6 @@ namespace Tgstation.Server.Host.Controllers
{
return StatusCode((int)HttpStatusCode.NotImplemented);
}
- catch(Exception e)
- {
- logger.LogInformation("System identifier user creation failure for {0}. Exception: {1}", model.SystemIdentifier, e);
- return Forbid();
- }
else
cryptographySuite.SetUserPassword(dbUser, model.Password);
diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs
index 839951c716..c4f285701f 100644
--- a/src/Tgstation.Server.Host/Core/Application.cs
+++ b/src/Tgstation.Server.Host/Core/Application.cs
@@ -149,7 +149,8 @@ namespace Tgstation.Server.Host.Core
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}!", nameof(DatabaseType)));
}
- services.AddScoped();
+ //very important this remains a singleton
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs
index 1b73c7afc5..3bddfce971 100644
--- a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs
+++ b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs
@@ -1,14 +1,16 @@
using Microsoft.EntityFrameworkCore;
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Security
{
///
- sealed class AuthenticationContextFactory : IAuthenticationContextFactory
+ sealed class AuthenticationContextFactory : IAuthenticationContextFactory, IDisposable
{
///
public IAuthenticationContext CurrentAuthenticationContext { get; private set; }
@@ -19,19 +21,53 @@ namespace Tgstation.Server.Host.Security
readonly ISystemIdentityFactory systemIdentityFactory;
///
- /// The for the
+ /// The for the
///
- readonly IDatabaseContext databaseContext;
+ readonly IDatabaseContextFactory databaseContextFactory;
+
+ ///
+ /// Map of s to s for that user
+ ///
+ readonly Dictionary identityCache;
///
/// Construct an
///
/// The value of
- /// The value of
- public AuthenticationContextFactory(ISystemIdentityFactory systemIdentityFactory, IDatabaseContext databaseContext)
+ /// The value of
+ public AuthenticationContextFactory(ISystemIdentityFactory systemIdentityFactory, IDatabaseContextFactory databaseContextFactory)
{
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
- this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
+ this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
+
+ identityCache = new Dictionary();
+ }
+
+ ///
+ public void Dispose()
+ {
+ foreach (var I in identityCache)
+ I.Value.Dispose();
+ }
+
+ ///
+ public void CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateTimeOffset expiry)
+ {
+ if (user == null)
+ throw new ArgumentNullException(nameof(user));
+ if (systemIdentity == null)
+ throw new ArgumentNullException(nameof(systemIdentity));
+ lock (identityCache)
+ {
+ if (identityCache.TryGetValue(user.Id, out var identCache))
+ identCache.Dispose(); //also clears it out
+ identCache = new IdentityCache(systemIdentity.Clone(), expiry, () =>
+ {
+ lock (identityCache)
+ identityCache.Remove(user.Id);
+ });
+ identityCache.Add(user.Id, identCache);
+ }
}
///
@@ -40,18 +76,35 @@ namespace Tgstation.Server.Host.Security
if (CurrentAuthenticationContext != null)
throw new InvalidOperationException("Authentication context has already been loaded");
- var userQuery = databaseContext.Users.Where(x => x.Id == userId);
+ User user = null;
+ await databaseContextFactory.UseContext(async db =>
+ {
+ var userQuery = db.Users.Where(x => x.Id == userId);
- if (instanceId.HasValue)
- userQuery = userQuery.Include(x => x.InstanceUsers.Where(y => y.Id == instanceId));
+ if (instanceId.HasValue)
+ userQuery = userQuery.Include(x => x.InstanceUsers.Where(y => y.Id == instanceId));
- var user = await userQuery.Include(x => x.InstanceUsers).FirstAsync(cancellationToken).ConfigureAwait(false);
+ user = await userQuery.Include(x => x.InstanceUsers).FirstAsync(cancellationToken).ConfigureAwait(false);
+ }).ConfigureAwait(false);
InstanceUser instanceUser = null;
if (instanceId.HasValue)
- instanceUser = user.InstanceUsers.Where(x => x.InstanceId == instanceId).First();
+ instanceUser = user.InstanceUsers.Where(x => x.InstanceId == instanceId).FirstOrDefault();
+
+ if (instanceUser == default)
+ return;
+
+ ISystemIdentity systemIdentity;
+ if (user.SystemIdentifier != null)
+ lock (identityCache)
+ {
+ if (!identityCache.TryGetValue(userId, out var identCache))
+ throw new InvalidOperationException("Cached system identity has expired!");
+ systemIdentity = identCache.SystemIdentity.Clone();
+ }
+ else
+ systemIdentity = null;
- var systemIdentity = user.SystemIdentifier != null ? await systemIdentityFactory.CreateSystemIdentity(user, cancellationToken).ConfigureAwait(false) : null;
CurrentAuthenticationContext = new AuthenticationContext(systemIdentity, user, instanceUser);
}
}
diff --git a/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs
index e0f3a225f2..2e8552c037 100644
--- a/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs
+++ b/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs
@@ -1,5 +1,7 @@
-using System.Threading;
+using System;
+using System.Threading;
using System.Threading.Tasks;
+using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Security
{
@@ -13,6 +15,14 @@ namespace Tgstation.Server.Host.Security
///
IAuthenticationContext CurrentAuthenticationContext { get; }
+ ///
+ /// Keep a 's alive until an time
+ ///
+ /// The the belongs to
+ /// The to cache
+ /// When the should expire
+ void CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateTimeOffset expiry);
+
///
/// Create an to populate
///
diff --git a/src/Tgstation.Server.Host/Security/ITokenFactory.cs b/src/Tgstation.Server.Host/Security/ITokenFactory.cs
index 77f8a1518c..7f40b2112d 100644
--- a/src/Tgstation.Server.Host/Security/ITokenFactory.cs
+++ b/src/Tgstation.Server.Host/Security/ITokenFactory.cs
@@ -1,5 +1,4 @@
-using System.Threading;
-using System.Threading.Tasks;
+using System;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Host.Security
@@ -13,7 +12,8 @@ namespace Tgstation.Server.Host.Security
/// Create a for a given
///
/// The to create the token for. Must have the field available
+ /// The representing the time the token expires
/// A new
- Token CreateToken(Models.User user);
+ Token CreateToken(Models.User user, out DateTimeOffset expiry);
}
}
diff --git a/src/Tgstation.Server.Host/Security/IdentityCache.cs b/src/Tgstation.Server.Host/Security/IdentityCache.cs
new file mode 100644
index 0000000000..931f8f171c
--- /dev/null
+++ b/src/Tgstation.Server.Host/Security/IdentityCache.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Tgstation.Server.Host.Security
+{
+ ///
+ /// For keeping a specific alive for a period of time
+ ///
+ sealed class IdentityCache : IDisposable
+ {
+ ///
+ /// The the manages
+ ///
+ public ISystemIdentity SystemIdentity { get; }
+
+ ///
+ /// The for the
+ ///
+ readonly CancellationTokenSource cancellationTokenSource;
+
+ ///
+ /// The to clean up
+ ///
+ readonly Task task;
+
+ ///
+ /// Construct an
+ ///
+ /// The value of
+ /// The
+ /// An optional to take on expiry
+ public IdentityCache(ISystemIdentity systemIdentity, DateTimeOffset expiry, Action onExpiry)
+ {
+ SystemIdentity = systemIdentity ?? throw new ArgumentNullException(nameof(systemIdentity));
+
+ cancellationTokenSource = new CancellationTokenSource();
+
+ async Task DisposeOnExipiry(CancellationToken cancellationToken)
+ {
+ using (SystemIdentity)
+ try
+ {
+ await Task.Delay(expiry - DateTimeOffset.Now, cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ onExpiry?.Invoke();
+ }
+ }
+
+ task = DisposeOnExipiry(cancellationTokenSource.Token);
+ }
+
+ ///
+ public void Dispose()
+ {
+ cancellationTokenSource.Cancel();
+ task.Wait();
+ cancellationTokenSource.Dispose();
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Security/TokenFactory.cs b/src/Tgstation.Server.Host/Security/TokenFactory.cs
index e0e4c9af7b..47b3809afe 100644
--- a/src/Tgstation.Server.Host/Security/TokenFactory.cs
+++ b/src/Tgstation.Server.Host/Security/TokenFactory.cs
@@ -4,7 +4,6 @@ using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Reflection;
using System.Security.Claims;
-using System.Text;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Host.Security
@@ -12,20 +11,26 @@ namespace Tgstation.Server.Host.Security
///
sealed class TokenFactory : ITokenFactory
{
+ ///
+ /// Amount of hours until generated s expire
+ ///
+ const int TokenExpiryHours = 1;
+
public static readonly string TokenAudience = typeof(Token).Assembly.GetName().Name;
public static readonly string TokenIssuer = Assembly.GetExecutingAssembly().GetName().Name;
public static readonly byte[] TokenSigningKey = CryptographySuite.GetSecureBytes(256);
///
- public Token CreateToken(Models.User user)
+ public Token CreateToken(Models.User user, out DateTimeOffset expiry)
{
if (user == null)
throw new ArgumentNullException(nameof(user));
+ expiry = DateTimeOffset.Now.AddHours(TokenExpiryHours);
var claims = new Claim[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString(CultureInfo.InvariantCulture)),
- new Claim(JwtRegisteredClaimNames.Exp, $"{DateTimeOffset.Now.AddHours(1).ToUnixTimeSeconds()}"),
+ new Claim(JwtRegisteredClaimNames.Exp, $"{expiry.ToUnixTimeSeconds()}"),
new Claim(JwtRegisteredClaimNames.Nbf, $"{DateTimeOffset.Now.ToUnixTimeSeconds()}"),
new Claim(JwtRegisteredClaimNames.Iss, TokenIssuer),
new Claim(JwtRegisteredClaimNames.Aud, TokenAudience)
diff --git a/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs b/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs
index 626d699ac7..1e0dc1afcf 100644
--- a/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs
+++ b/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs
@@ -1,4 +1,5 @@
using System;
+using System.DirectoryServices.AccountManagement;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
@@ -16,7 +17,12 @@ namespace Tgstation.Server.Host.Security
readonly WindowsIdentity identity;
///
- /// Construct a
+ /// The for the
+ ///
+ readonly UserPrincipal userPrincipal;
+
+ ///
+ /// Construct a using a
///
/// The value of
public WindowsSystemIdentity(WindowsIdentity identity)
@@ -24,23 +30,40 @@ namespace Tgstation.Server.Host.Security
this.identity = identity ?? throw new ArgumentNullException(nameof(identity));
}
+ ///
+ /// Construct a using a
+ ///
+ /// The value of
+ public WindowsSystemIdentity(UserPrincipal userPrincipal)
+ {
+ this.userPrincipal = userPrincipal ?? throw new ArgumentNullException(nameof(userPrincipal));
+ }
+
///
public void Dispose() => identity.Dispose();
///
- public string Uid => identity.User.ToString();
+ public string Uid => (userPrincipal?.Sid ?? identity.User).ToString();
///
- public string Username => identity.Name;
+ public string Username => userPrincipal?.Name ?? identity.Name;
///
- public ISystemIdentity Clone() => new WindowsSystemIdentity((WindowsIdentity)identity.Clone());
+ public ISystemIdentity Clone()
+ {
+ if (identity != null)
+ return new WindowsSystemIdentity((WindowsIdentity)identity.Clone());
+ //can't clone a UP, shouldn't be trying to anyway, cloning is for impersonation
+ throw new InvalidOperationException("Cannot clone a UserPrincipal based WindowsSystemIdentity!");
+ }
///
public Task RunImpersonated(Action action, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
{
if (action == null)
throw new ArgumentNullException(nameof(action));
+ if (identity == null)
+ throw new InvalidOperationException("Impersonate using a UserPrincipal based WindowsSystemIdentity!");
WindowsIdentity.RunImpersonated(identity.AccessToken, action);
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
}
diff --git a/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs b/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs
index 286d2daaec..781f0b381e 100644
--- a/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs
+++ b/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs
@@ -1,7 +1,7 @@
using Microsoft.Win32.SafeHandles;
using System;
using System.ComponentModel;
-using System.Globalization;
+using System.DirectoryServices.AccountManagement;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Threading;
@@ -24,15 +24,19 @@ namespace Tgstation.Server.Host.Security
if (user.SystemIdentifier == null)
throw new InvalidOperationException("User's SystemIdentifier must not be null!");
- //System identity at this point will always be in the form DOMAIN\\USER or USER
- var splits = user.SystemIdentifier.Split('\\');
- string identity;
- if (splits.Length > 1)
- identity = String.Format(CultureInfo.InvariantCulture, "{0}@{1}", splits[0], splits[1]);
- else
- identity = user.SystemIdentifier;
+ UserPrincipal principal = null;
+ //machine logon first cause it's faster
+ using (var pc = new PrincipalContext(ContextType.Machine, Environment.UserDomainName))
+ principal = UserPrincipal.FindByIdentity(pc, user.SystemIdentifier);
- return (ISystemIdentity)new WindowsSystemIdentity(new WindowsIdentity(identity));
+ if(principal == null)
+ using (var pc = new PrincipalContext(ContextType.Domain, Environment.UserDomainName))
+ principal = UserPrincipal.FindByIdentity(pc, user.SystemIdentifier);
+
+ if (principal == null)
+ return null;
+
+ return (ISystemIdentity)new WindowsSystemIdentity(principal);
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
///
diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs
index ab2cb8874e..c49fbbad8b 100644
--- a/src/Tgstation.Server.Host/Server.cs
+++ b/src/Tgstation.Server.Host/Server.cs
@@ -87,9 +87,19 @@ namespace Tgstation.Server.Host
}
///
- public void RegisterForUpdate(Action action) => cancellationTokenSource.Token.Register(action);
+ public void RegisterForUpdate(Action action)
+ {
+ if (cancellationTokenSource == null)
+ throw new InvalidOperationException("Tried to register an update action on a non-running Server!");
+ cancellationTokenSource.Token.Register(action);
+ }
///
- public void Restart() => cancellationTokenSource.Cancel();
+ public void Restart()
+ {
+ if (cancellationTokenSource == null)
+ throw new InvalidOperationException("Tried to restart a non-running Server!");
+ cancellationTokenSource.Cancel();
+ }
}
}