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