Fixups tested users controller

This commit is contained in:
Jordan Brown
2018-07-26 21:25:38 -04:00
parent f97056bb82
commit f299eba469
22 changed files with 695 additions and 147 deletions
@@ -71,7 +71,7 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
/// <param name="logger">The value of <see cref="logger"/></param>
/// <param name="updatesConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing value of <see cref="updatesConfiguration"/></param>
public AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClient gitHubClient, IServerUpdater serverUpdater, IApplication application, IIOManager ioManager, ILogger<AdministrationController> logger, IOptions<UpdatesConfiguration> updatesConfigurationOptions) : base(databaseContext, authenticationContextFactory)
public AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClient gitHubClient, IServerUpdater serverUpdater, IApplication application, IIOManager ioManager, ILogger<AdministrationController> logger, IOptions<UpdatesConfiguration> updatesConfigurationOptions) : base(databaseContext, authenticationContextFactory, false)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.gitHubClient = gitHubClient ?? throw new ArgumentNullException(nameof(gitHubClient));
@@ -43,6 +43,11 @@ namespace Tgstation.Server.Host.Controllers
/// </summary>
protected Instance Instance { get; }
/// <summary>
/// If <see cref="IAuthenticationContext.InstanceUser"/> permissions are required to access the <see cref="ApiController"/>
/// </summary>
readonly bool requireInstance;
/// <summary>
/// Runs after a <see cref="Api.Models.Token"/> has been validated. Creates the <see cref="IAuthenticationContext"/> for the <see cref="ControllerBase.Request"/>
/// </summary>
@@ -81,6 +86,7 @@ namespace Tgstation.Server.Host.Controllers
await authenticationContextFactory.CreateAuthenticationContext(userId, apiHeaders.InstanceId, context.HttpContext.RequestAborted).ConfigureAwait(false);
var authenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
var enumerator = Enum.GetValues(typeof(RightsType));
@@ -103,19 +109,21 @@ namespace Tgstation.Server.Host.Controllers
/// </summary>
/// <param name="databaseContext">The value of <see cref="DatabaseContext"/></param>
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory)
/// <param name="requireInstance">The value of <see cref="requireInstance"/></param>
public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, bool requireInstance)
{
DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
if (authenticationContextFactory == null)
throw new ArgumentNullException(nameof(authenticationContextFactory));
AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
Instance = AuthenticationContext?.InstanceUser?.Instance;
this.requireInstance = requireInstance;
}
/// <inheritdoc />
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (AuthenticationContext == null)
if (requireInstance && AuthenticationContext.InstanceUser == null)
{
//accessing an instance they don't have access to
await Forbid().ExecuteResultAsync(context).ConfigureAwait(false);
@@ -34,7 +34,7 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/></param>
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
public ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager) : base(databaseContext, authenticationContextFactory)
public ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager) : base(databaseContext, authenticationContextFactory, true)
{
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
}
@@ -35,7 +35,7 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
/// <param name="logger">The value of <see cref="logger"/></param>
public ConfigurationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger<ConfigurationController> logger) : base(databaseContext, authenticationContextFactory)
public ConfigurationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger<ConfigurationController> logger) : base(databaseContext, authenticationContextFactory, true)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
@@ -39,7 +39,7 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
public DreamDaemonController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager) : base(databaseContext, authenticationContextFactory)
public DreamDaemonController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager) : base(databaseContext, authenticationContextFactory, true)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
@@ -36,7 +36,7 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
public DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager) : base(databaseContext, authenticationContextFactory)
public DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager) : base(databaseContext, authenticationContextFactory, true)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
@@ -35,9 +35,9 @@ namespace Tgstation.Server.Host.Controllers
/// </summary>
readonly IApplication application;
/// <summary>
/// The <see cref="IAuthenticationContextFactory"/> for the <see cref="HomeController"/>
/// The <see cref="IIdentityCache"/> for the <see cref="HomeController"/>
/// </summary>
readonly IAuthenticationContextFactory authenticationContextFactory;
readonly IIdentityCache identityCache;
/// <summary>
/// Construct a <see cref="HomeController"/>
@@ -48,14 +48,14 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="systemIdentityFactory">The value of <see cref="systemIdentityFactory"/></param>
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/></param>
/// <param name="application">The value of <see cref="application"/></param>
public HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, IApplication application) : base(databaseContext, authenticationContextFactory)
/// <param name="identityCache">The value of <see cref="identityCache"/</param>
public HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, IApplication application, IIdentityCache identityCache) : base(databaseContext, authenticationContextFactory, false)
{
this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
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;
this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
}
/// <summary>
@@ -109,7 +109,7 @@ namespace Tgstation.Server.Host.Controllers
else
try
{
identity = await systemIdentityFactory.CreateSystemIdentity(user.Name, ApiHeaders.Password, cancellationToken).ConfigureAwait(false);
identity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken).ConfigureAwait(false);
if (identity == null || identity.Uid != user.SystemIdentifier)
return Unauthorized();
}
@@ -123,7 +123,7 @@ namespace Tgstation.Server.Host.Controllers
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
identityCache.CacheSystemIdentity(user, identity, expiry.AddSeconds(10)); //expire the identity slightly after the auth token in case of lag
return Json(token);
}
}
@@ -42,7 +42,7 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
public InstanceController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, IIOManager ioManager) : base(databaseContext, authenticationContextFactory)
public InstanceController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, IIOManager ioManager) : base(databaseContext, authenticationContextFactory, false)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
@@ -31,7 +31,7 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/></param>
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
/// <param name="logger">The value of <see cref="logger"/></param>
public InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger<InstanceUserController> logger) : base(databaseContext, authenticationContextFactory)
public InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger<InstanceUserController> logger) : base(databaseContext, authenticationContextFactory, false)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -24,7 +24,7 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/></param>
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
public InteropController(IInstanceManager instanceManager, IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory) : base(databaseContext, authenticationContextFactory)
public InteropController(IInstanceManager instanceManager, IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory) : base(databaseContext, authenticationContextFactory, false)
{
this.instanceManager = instanceManager;
}
@@ -28,7 +28,7 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/></param>
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
public JobController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager) : base(databaseContext, authenticationContextFactory)
public JobController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager) : base(databaseContext, authenticationContextFactory, false)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
}
@@ -24,7 +24,8 @@ namespace Tgstation.Server.Host.Controllers
/// </summary>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/></param>
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
public ModelController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory) : base(databaseContext, authenticationContextFactory) { }
/// <param name="requireInstance">If the <see cref="ModelController{TModel}"/> requires an <see cref="IAuthenticationContext.InstanceUser"/></param>
public ModelController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, bool requireInstance) : base(databaseContext, authenticationContextFactory, requireInstance) { }
/// <summary>
/// Attempt to create a <paramref name="model"/>
@@ -49,7 +50,7 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="id">The ID of the model to get</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation</returns>
[HttpGet("/{id}")]
[HttpGet("{id}")]
public virtual Task<IActionResult> GetId(long id, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
/// <summary>
@@ -75,7 +76,7 @@ namespace Tgstation.Server.Host.Controllers
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation</returns>
[HttpGet("/List")]
[HttpGet("List")]
public virtual Task<IActionResult> List(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
}
}
@@ -43,7 +43,7 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="systemIdentityFactory">The value of <see cref="systemIdentityFactory"/></param>
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/></param>
/// <param name="logger">The value of <see cref="logger"/></param>
public UserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, ILogger<UserController> logger) : base(databaseContext, authenticationContextFactory)
public UserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, ILogger<UserController> logger) : base(databaseContext, authenticationContextFactory, false)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
@@ -148,9 +148,9 @@ namespace Tgstation.Server.Host.Core
default:
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}!", nameof(DatabaseType)));
}
//very important this remains a singleton
services.AddSingleton<IAuthenticationContextFactory, AuthenticationContextFactory>();
services.AddScoped<IAuthenticationContextFactory, AuthenticationContextFactory>();
services.AddSingleton<IIdentityCache, IdentityCache>();
services.AddSingleton<ICryptographySuite, CryptographySuite>();
services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
@@ -10,7 +10,7 @@ using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Security
{
/// <inheritdoc />
sealed class AuthenticationContextFactory : IAuthenticationContextFactory, IDisposable
sealed class AuthenticationContextFactory : IAuthenticationContextFactory
{
/// <inheritdoc />
public IAuthenticationContext CurrentAuthenticationContext { get; private set; }
@@ -21,53 +21,22 @@ namespace Tgstation.Server.Host.Security
readonly ISystemIdentityFactory systemIdentityFactory;
/// <summary>
/// The <see cref="IDatabaseContextFactory"/> for the <see cref="AuthenticationContextFactory"/>
/// The <see cref="IDatabaseContext"/> for the <see cref="AuthenticationContextFactory"/>
/// </summary>
readonly IDatabaseContextFactory databaseContextFactory;
readonly IDatabaseContext databaseContext;
/// <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;
readonly IIdentityCache identityCache;
/// <summary>
/// Construct an <see cref="AuthenticationContextFactory"/>
/// </summary>
/// <param name="systemIdentityFactory">The value of <see cref="systemIdentityFactory"/></param>
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/></param>
public AuthenticationContextFactory(ISystemIdentityFactory systemIdentityFactory, IDatabaseContextFactory databaseContextFactory)
public AuthenticationContextFactory(ISystemIdentityFactory systemIdentityFactory, IDatabaseContext databaseContext, IIdentityCache identityCache)
{
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
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);
}
this.databaseContext = databaseContext?? throw new ArgumentNullException(nameof(databaseContext));
this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
}
/// <inheritdoc />
@@ -76,32 +45,24 @@ namespace Tgstation.Server.Host.Security
if (CurrentAuthenticationContext != null)
throw new InvalidOperationException("Authentication context has already been loaded");
User user = null;
await databaseContextFactory.UseContext(async db =>
{
var userQuery = db.Users.Where(x => x.Id == userId);
var userQuery = databaseContext.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));
user = await userQuery.Include(x => x.InstanceUsers).FirstAsync(cancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
var user = await userQuery.Include(x => x.InstanceUsers).FirstAsync(cancellationToken).ConfigureAwait(false);
InstanceUser instanceUser = null;
if (instanceId.HasValue)
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();
}
{
systemIdentity = identityCache.LoadCachedIdentity(user);
if (systemIdentity == null)
throw new InvalidOperationException("Cached system identity has expired!");
}
else
systemIdentity = null;
@@ -15,14 +15,6 @@ 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>
@@ -0,0 +1,23 @@
using System;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Security
{
public interface IIdentityCache
{
/// <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>
/// Attempt to load a cached <see cref="ISystemIdentity"/>
/// </summary>
/// <param name="user">The <see cref="User"/> the <see cref="ISystemIdentity"/> belongs to</param>
/// <returns>The cached <see cref="ISystemIdentity"/> or <see langword="null"/> if it doesn't exist or expired</returns>
ISystemIdentity LoadCachedIdentity(User user);
}
}
@@ -1,63 +1,57 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Security
{
/// <summary>
/// For keeping a specific <see cref="ISystemIdentity"/> alive for a period of time
/// </summary>
sealed class IdentityCache : IDisposable
/// <inheritdoc />
sealed class IdentityCache : IIdentityCache, IDisposable
{
/// <summary>
/// The <see cref="ISystemIdentity"/> the <see cref="IdentityCache"/> manages
/// </summary>
public ISystemIdentity SystemIdentity { get; }
readonly Dictionary<long, IdentityCacheObject> cachedIdentities;
/// <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)
public IdentityCache()
{
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);
cachedIdentities = new Dictionary<long, IdentityCacheObject>();
}
/// <inheritdoc />
public void Dispose()
{
cancellationTokenSource.Cancel();
task.Wait();
cancellationTokenSource.Dispose();
foreach (var I in cachedIdentities)
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 (cachedIdentities)
{
if (cachedIdentities.TryGetValue(user.Id, out var identCache))
identCache.Dispose(); //also clears it out
identCache = new IdentityCacheObject(systemIdentity.Clone(), () =>
{
lock (cachedIdentities)
cachedIdentities.Remove(user.Id);
}, expiry);
cachedIdentities.Add(user.Id, identCache);
}
}
/// <inheritdoc />
public ISystemIdentity LoadCachedIdentity(User user)
{
if (user == null)
throw new ArgumentNullException(nameof(user));
lock (cachedIdentities)
{
if (cachedIdentities.TryGetValue(user.Id, out var identity))
return identity.SystemIdentity;
return null;
}
}
}
}
@@ -0,0 +1,68 @@
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 IdentityCacheObject : 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="onExpiry">The <see cref="Action"/> to take on expiry</param>
/// <param name="expiry">The <see cref="DateTimeOffset"/></param>
public IdentityCacheObject(ISystemIdentity systemIdentity, Action onExpiry, DateTimeOffset expiry)
{
SystemIdentity = systemIdentity ?? throw new ArgumentNullException(nameof(systemIdentity));
if (onExpiry == null)
throw new ArgumentNullException(nameof(onExpiry));
var now = DateTimeOffset.Now;
if (expiry < now)
throw new ArgumentOutOfRangeException(nameof(expiry), expiry, "expiry must be greater than DateTimeOffset.Now!");
cancellationTokenSource = new CancellationTokenSource();
async Task DisposeOnExipiry(CancellationToken cancellationToken)
{
using (SystemIdentity)
try
{
await Task.Delay(expiry - now, cancellationToken).ConfigureAwait(false);
}
finally
{
onExpiry();
}
}
task = DisposeOnExipiry(cancellationTokenSource.Token);
}
/// <inheritdoc />
public void Dispose()
{
cancellationTokenSource.Cancel();
task.Wait();
cancellationTokenSource.Dispose();
}
}
}
@@ -40,7 +40,13 @@ namespace Tgstation.Server.Host.Security
}
/// <inheritdoc />
public void Dispose() => identity.Dispose();
public void Dispose()
{
if (identity != null)
identity.Dispose();
else
userPrincipal.Dispose();
}
/// <inheritdoc />
public string Uid => (userPrincipal?.Sid ?? identity.User).ToString();
@@ -24,14 +24,28 @@ namespace Tgstation.Server.Host.Security
if (user.SystemIdentifier == null)
throw new InvalidOperationException("User's SystemIdentifier must not be null!");
PrincipalContext pc = null;
UserPrincipal principal = null;
//machine logon first cause it's faster
using (var pc = new PrincipalContext(ContextType.Machine, Environment.UserDomainName))
try
{
pc = new PrincipalContext(ContextType.Machine, Environment.UserDomainName);
principal = UserPrincipal.FindByIdentity(pc, user.SystemIdentifier);
if(principal == null)
using (var pc = new PrincipalContext(ContextType.Domain, Environment.UserDomainName))
if (principal == null)
{
pc.Dispose();
pc = new PrincipalContext(ContextType.Domain, Environment.UserDomainName);
principal = UserPrincipal.FindByIdentity(pc, user.SystemIdentifier);
if (principal == null)
pc.Dispose();
}
}
catch
{
pc?.Dispose();
throw;
}
if (principal == null)
return null;
@@ -42,6 +56,10 @@ namespace Tgstation.Server.Host.Security
/// <inheritdoc />
public Task<ISystemIdentity> CreateSystemIdentity(string username, string password, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
{
if (username == null)
throw new ArgumentNullException(nameof(username));
if (password == null)
throw new ArgumentNullException(nameof(password));
var splits = username.Split('\\');
var res = NativeMethods.LogonUser(splits.Length > 1 ? splits[1] : splits[0], splits.Length > 1 ? splits[0] : null, password, 3 /*LOGON32_LOGON_NETWORK*/, 0 /*LOGON32_PROVIDER_DEFAULT*/, out var token);
+484 -7
View File
@@ -1,13 +1,13 @@
{
"info": {
"_postman_id": "f0940866-29fd-4ea3-8682-7e30df5b08c9",
"_postman_id": "e62d1230-5b76-45e2-9fb6-002e260daa2a",
"name": "TGS",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Administration",
"description": "",
"description": null,
"item": [
{
"name": "Get Update Versions",
@@ -27,7 +27,10 @@
"value": "Tgstation.Server.Api/4.0.0.0"
}
],
"body": {},
"body": {
"mode": "raw",
"raw": ""
},
"url": {
"raw": "localhost:5000/Administration",
"host": [
@@ -98,7 +101,10 @@
"value": "Tgstation.Server.Api/4.0.0.0"
}
],
"body": {},
"body": {
"mode": "raw",
"raw": ""
},
"url": {
"raw": "localhost:5000/Administration",
"host": [
@@ -136,6 +142,475 @@
}
]
},
{
"name": "Users",
"description": "",
"item": [
{
"name": "Read Current User",
"request": {
"method": "GET",
"header": [
{
"key": "Accept",
"value": "application/json"
},
{
"key": "User-Agent",
"value": "Postman/1.0"
},
{
"key": "User-Agent",
"value": "Tgstation.Server.Api/4.0.0.0"
}
],
"body": {},
"url": {
"raw": "localhost:5000/User",
"host": [
"localhost"
],
"port": "5000",
"path": [
"User"
]
}
},
"response": []
},
{
"name": "Read All Users",
"request": {
"method": "GET",
"header": [
{
"key": "Accept",
"value": "application/json"
},
{
"key": "User-Agent",
"value": "Postman/1.0"
},
{
"key": "User-Agent",
"value": "Tgstation.Server.Api/4.0.0.0"
}
],
"body": {},
"url": {
"raw": "localhost:5000/User/List",
"host": [
"localhost"
],
"port": "5000",
"path": [
"User",
"List"
]
}
},
"response": []
},
{
"name": "Create User Bob Pass (asdf)",
"request": {
"method": "PUT",
"header": [
{
"key": "Accept",
"value": "application/json"
},
{
"key": "User-Agent",
"value": "Postman/1.0"
},
{
"key": "User-Agent",
"value": "Tgstation.Server.Api/4.0.0.0"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"id\": 1,\n \"enabled\": true,\n \"password\": \"asdf\",\n \"name\": \"Bob\"\n}"
},
"url": {
"raw": "localhost:5000/User",
"host": [
"localhost"
],
"port": "5000",
"path": [
"User"
]
}
},
"response": []
},
{
"name": "Try To Add A SysId To User ID 2",
"request": {
"method": "POST",
"header": [
{
"key": "Accept",
"value": "application/json"
},
{
"key": "User-Agent",
"value": "Postman/1.0"
},
{
"key": "User-Agent",
"value": "Tgstation.Server.Api/4.0.0.0"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"id\": 2,\n \"systemIdentifier\": \"fake\"\n}"
},
"url": {
"raw": "localhost:5000/User",
"host": [
"localhost"
],
"port": "5000",
"path": [
"User"
]
}
},
"response": []
},
{
"name": "Enable User ID 3",
"request": {
"method": "POST",
"header": [
{
"key": "Accept",
"value": "application/json"
},
{
"key": "User-Agent",
"value": "Postman/1.0"
},
{
"key": "User-Agent",
"value": "Tgstation.Server.Api/4.0.0.0"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"id\": 3,\n \"enabled\": true\n}"
},
"url": {
"raw": "localhost:5000/User",
"host": [
"localhost"
],
"port": "5000",
"path": [
"User"
]
}
},
"response": []
},
{
"name": "Try To Add A Password To User ID",
"request": {
"method": "POST",
"header": [
{
"key": "Accept",
"value": "application/json"
},
{
"key": "User-Agent",
"value": "Postman/1.0"
},
{
"key": "User-Agent",
"value": "Tgstation.Server.Api/4.0.0.0"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"id\": 3,\n \"password\": \"fake\"\n}"
},
"url": {
"raw": "localhost:5000/User",
"host": [
"localhost"
],
"port": "5000",
"path": [
"User"
]
}
},
"response": []
},
{
"name": "Change User ID 2's password and name to bOb and fdsa",
"request": {
"method": "POST",
"header": [
{
"key": "Accept",
"value": "application/json"
},
{
"key": "User-Agent",
"value": "Postman/1.0"
},
{
"key": "User-Agent",
"value": "Tgstation.Server.Api/4.0.0.0"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"id\": 2,\n \"password\": \"fdsa\",\n \"name\": \"bOb\"\n}"
},
"url": {
"raw": "localhost:5000/User",
"host": [
"localhost"
],
"port": "5000",
"path": [
"User"
]
}
},
"response": []
},
{
"name": "Change User ID 2's name to Bob2",
"request": {
"method": "POST",
"header": [
{
"key": "Accept",
"value": "application/json"
},
{
"key": "User-Agent",
"value": "Postman/1.0"
},
{
"key": "User-Agent",
"value": "Tgstation.Server.Api/4.0.0.0"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"id\": 2,\n \"name\": \"Bob2\"\n}"
},
"url": {
"raw": "localhost:5000/User",
"host": [
"localhost"
],
"port": "5000",
"path": [
"User"
]
}
},
"response": []
},
{
"name": "Give User ID 3 rights",
"request": {
"method": "POST",
"header": [
{
"key": "Accept",
"value": "application/json"
},
{
"key": "User-Agent",
"value": "Postman/1.0"
},
{
"key": "User-Agent",
"value": "Tgstation.Server.Api/4.0.0.0"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"id\": 3,\n \"administrationRights\": -1,\n\t\"instanceManagerRights\": -1,\n}"
},
"url": {
"raw": "localhost:5000/User",
"host": [
"localhost"
],
"port": "5000",
"path": [
"User"
]
}
},
"response": []
},
{
"name": "Create system user Cyberboss",
"request": {
"method": "PUT",
"header": [
{
"key": "Accept",
"value": "application/json"
},
{
"key": "User-Agent",
"value": "Postman/1.0"
},
{
"key": "User-Agent",
"value": "Tgstation.Server.Api/4.0.0.0"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"systemIdentifier\": \"Cyberboss\"\n}"
},
"url": {
"raw": "localhost:5000/User",
"host": [
"localhost"
],
"port": "5000",
"path": [
"User"
]
}
},
"response": []
},
{
"name": "Login bob (asdf)",
"request": {
"auth": {
"type": "noauth"
},
"method": "POST",
"header": [
{
"key": "Accept",
"value": "application/json"
},
{
"key": "User-Agent",
"value": "Postman/1.0"
},
{
"key": "User-Agent",
"value": "Tgstation.Server.Api/4.0.0.0"
},
{
"key": "Authorization",
"value": "Password asdf"
},
{
"key": "Username",
"value": "bob"
}
],
"body": {
"mode": "raw",
"raw": ""
},
"url": {
"raw": "localhost:5000",
"host": [
"localhost"
],
"port": "5000"
}
},
"response": []
},
{
"name": "Login cyberboss",
"request": {
"auth": {
"type": "noauth"
},
"method": "POST",
"header": [
{
"key": "Accept",
"value": "application/json"
},
{
"key": "User-Agent",
"value": "Postman/1.0"
},
{
"key": "User-Agent",
"value": "Tgstation.Server.Api/4.0.0.0"
},
{
"key": "Authorization",
"value": "Password no"
},
{
"key": "Username",
"value": "cyberboss"
}
],
"body": {
"mode": "raw",
"raw": ""
},
"url": {
"raw": "localhost:5000",
"host": [
"localhost"
],
"port": "5000"
}
},
"response": []
}
]
},
{
"name": "Login Admin Default",
"request": {
@@ -165,7 +640,10 @@
"value": "admin"
}
],
"body": {},
"body": {
"mode": "raw",
"raw": ""
},
"url": {
"raw": "localhost:5000",
"host": [
@@ -200,7 +678,6 @@
{
"key": "name",
"value": "asdfasdf",
"description": "",
"type": "text"
}
]
@@ -221,7 +698,7 @@
"bearer": [
{
"key": "token",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoiMTUzMjU0MjgwMyIsIm5iZiI6IjE1MzI1MzkyMDMiLCJpc3MiOiJUZ3N0YXRpb24uU2VydmVyLkhvc3QiLCJhdWQiOiJUZ3N0YXRpb24uU2VydmVyLkFwaSJ9.f-y5xQ-JbZklD7iMeztZhLGfXBWnSxxlYtzl3TXDt5g",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoiMTUzMjY1ODI0OSIsIm5iZiI6IjE1MzI2NTQ2NDkiLCJpc3MiOiJUZ3N0YXRpb24uU2VydmVyLkhvc3QiLCJhdWQiOiJUZ3N0YXRpb24uU2VydmVyLkFwaSJ9.nfVqGC501-l1hkXCn6NsLj-rLFBVU65feHKLxXV3X8c",
"type": "string"
}
]