Fix windows system identities

This commit is contained in:
Jordan Brown
2018-07-26 17:35:42 -04:00
parent 571792ed6d
commit 5490c1e1ef
12 changed files with 234 additions and 47 deletions
@@ -115,6 +115,13 @@ namespace Tgstation.Server.Host.Controllers
/// <inheritdoc />
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
{
@@ -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 <see cref="IApplication"/> for the <see cref="HomeController"/>
/// </summary>
readonly IApplication application;
/// <summary>
/// The <see cref="IAuthenticationContextFactory"/> for the <see cref="HomeController"/>
/// </summary>
readonly IAuthenticationContextFactory authenticationContextFactory;
/// <summary>
/// Construct a <see cref="HomeController"/>
@@ -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;
}
/// <summary>
@@ -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);
}
}
}
}
@@ -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);
@@ -149,7 +149,8 @@ namespace Tgstation.Server.Host.Core
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}!", nameof(DatabaseType)));
}
services.AddScoped<IAuthenticationContextFactory, AuthenticationContextFactory>();
//very important this remains a singleton
services.AddSingleton<IAuthenticationContextFactory, AuthenticationContextFactory>();
services.AddSingleton<ICryptographySuite, CryptographySuite>();
services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
@@ -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
{
/// <inheritdoc />
sealed class AuthenticationContextFactory : IAuthenticationContextFactory
sealed class AuthenticationContextFactory : IAuthenticationContextFactory, IDisposable
{
/// <inheritdoc />
public IAuthenticationContext CurrentAuthenticationContext { get; private set; }
@@ -19,19 +21,53 @@ namespace Tgstation.Server.Host.Security
readonly ISystemIdentityFactory systemIdentityFactory;
/// <summary>
/// The <see cref="IDatabaseContext"/> for the <see cref="AuthenticationContextFactory"/>
/// The <see cref="IDatabaseContextFactory"/> for the <see cref="AuthenticationContextFactory"/>
/// </summary>
readonly IDatabaseContext databaseContext;
readonly IDatabaseContextFactory databaseContextFactory;
/// <summary>
/// Map of <see cref="Api.Models.Internal.User.Id"/>s to <see cref="IdentityCache"/>s for that user
/// </summary>
readonly Dictionary<long, IdentityCache> identityCache;
/// <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)
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/></param>
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<long, IdentityCache>();
}
/// <inheritdoc />
public void Dispose()
{
foreach (var I in identityCache)
I.Value.Dispose();
}
/// <inheritdoc />
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);
}
}
/// <inheritdoc />
@@ -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);
}
}
@@ -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
/// </summary>
IAuthenticationContext CurrentAuthenticationContext { get; }
/// <summary>
/// Keep a <paramref name="user"/>'s <paramref name="systemIdentity"/> alive until an <paramref name="expiry"/> time
/// </summary>
/// <param name="user">The <see cref="User"/> the <paramref name="systemIdentity"/> belongs to</param>
/// <param name="systemIdentity">The <see cref="ISystemIdentity"/> to cache</param>
/// <param name="expiry">When the <paramref name="systemIdentity"/> should expire</param>
void CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateTimeOffset expiry);
/// <summary>
/// Create an <see cref="IAuthenticationContext"/> to populate <see cref="CurrentAuthenticationContext"/>
/// </summary>
@@ -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 <see cref="Token"/> for a given <paramref name="user"/>
/// </summary>
/// <param name="user">The <see cref="Models.User"/> to create the token for. Must have the <see cref="Api.Models.Internal.User.Id"/> field available</param>
/// <param name="expiry">The <see cref="DateTimeOffset"/> representing the time the token expires</param>
/// <returns>A new <see cref="Token"/></returns>
Token CreateToken(Models.User user);
Token CreateToken(Models.User user, out DateTimeOffset expiry);
}
}
@@ -0,0 +1,63 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Security
{
/// <summary>
/// For keeping a specific <see cref="ISystemIdentity"/> alive for a period of time
/// </summary>
sealed class IdentityCache : IDisposable
{
/// <summary>
/// The <see cref="ISystemIdentity"/> the <see cref="IdentityCache"/> manages
/// </summary>
public ISystemIdentity SystemIdentity { get; }
/// <summary>
/// The <see cref="cancellationTokenSource"/> for the <see cref="IdentityCache"/>
/// </summary>
readonly CancellationTokenSource cancellationTokenSource;
/// <summary>
/// The <see cref="Task"/> to clean up <see cref="SystemIdentity"/>
/// </summary>
readonly Task task;
/// <summary>
/// Construct an <see cref="IdentityCache"/>
/// </summary>
/// <param name="systemIdentity">The value of <see cref="SystemIdentity"/></param>
/// <param name="expiry">The <see cref="DateTimeOffset"/></param>
/// <param name="onExpiry">An optional <see cref="Action"/> to take on expiry</param>
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);
}
/// <inheritdoc />
public void Dispose()
{
cancellationTokenSource.Cancel();
task.Wait();
cancellationTokenSource.Dispose();
}
}
}
@@ -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
/// <inheritdoc />
sealed class TokenFactory : ITokenFactory
{
/// <summary>
/// Amount of hours until generated <see cref="Token"/>s expire
/// </summary>
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);
/// <inheritdoc />
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)
@@ -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;
/// <summary>
/// Construct a <see cref="WindowsSystemIdentity"/>
/// The <see cref="UserPrincipal"/> for the <see cref="WindowsSystemIdentity"/>
/// </summary>
readonly UserPrincipal userPrincipal;
/// <summary>
/// Construct a <see cref="WindowsSystemIdentity"/> using a <see cref="WindowsIdentity"/>
/// </summary>
/// <param name="identity">The value of <see cref="identity"/></param>
public WindowsSystemIdentity(WindowsIdentity identity)
@@ -24,23 +30,40 @@ namespace Tgstation.Server.Host.Security
this.identity = identity ?? throw new ArgumentNullException(nameof(identity));
}
/// <summary>
/// Construct a <see cref="WindowsSystemIdentity"/> using a <see cref="UserPrincipal"/>
/// </summary>
/// <param name="userPrincipal">The value of <see cref="userPrincipal"/></param>
public WindowsSystemIdentity(UserPrincipal userPrincipal)
{
this.userPrincipal = userPrincipal ?? throw new ArgumentNullException(nameof(userPrincipal));
}
/// <inheritdoc />
public void Dispose() => identity.Dispose();
/// <inheritdoc />
public string Uid => identity.User.ToString();
public string Uid => (userPrincipal?.Sid ?? identity.User).ToString();
/// <inheritdoc />
public string Username => identity.Name;
public string Username => userPrincipal?.Name ?? identity.Name;
/// <inheritdoc />
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!");
}
/// <inheritdoc />
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);
}
@@ -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);
/// <inheritdoc />
+12 -2
View File
@@ -87,9 +87,19 @@ namespace Tgstation.Server.Host
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public void Restart() => cancellationTokenSource.Cancel();
public void Restart()
{
if (cancellationTokenSource == null)
throw new InvalidOperationException("Tried to restart a non-running Server!");
cancellationTokenSource.Cancel();
}
}
}