From f299eba4699ee498ffb52db6fa804bcb433fd7fe Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 26 Jul 2018 21:25:38 -0400 Subject: [PATCH] Fixups tested users controller --- .../Controllers/AdministrationController.cs | 2 +- .../Controllers/ApiController.cs | 12 +- .../Controllers/ChatController.cs | 2 +- .../Controllers/ConfigurationController.cs | 2 +- .../Controllers/DreamDaemonController.cs | 2 +- .../Controllers/DreamMakerController.cs | 2 +- .../Controllers/HomeController.cs | 14 +- .../Controllers/InstanceController.cs | 2 +- .../Controllers/InstanceUserController.cs | 2 +- .../Controllers/InteropController.cs | 2 +- .../Controllers/JobController.cs | 2 +- .../Controllers/ModelController.cs | 7 +- .../Controllers/UserController.cs | 2 +- src/Tgstation.Server.Host/Core/Application.cs | 6 +- .../Security/AuthenticationContextFactory.cs | 71 +-- .../Security/IAuthenticationContextFactory.cs | 8 - .../Security/IIdentityCache.cs | 23 + .../Security/IdentityCache.cs | 90 ++-- .../Security/IdentityCacheObject.cs | 68 +++ .../Security/WindowsSystemIdentity.cs | 8 +- .../Security/WindowsSystemIdentityFactory.cs | 24 +- tools/TGS.postman_collection.json | 491 +++++++++++++++++- 22 files changed, 695 insertions(+), 147 deletions(-) create mode 100644 src/Tgstation.Server.Host/Security/IIdentityCache.cs create mode 100644 src/Tgstation.Server.Host/Security/IdentityCacheObject.cs diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index cd966e7b6d..e8d8ca75dc 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -71,7 +71,7 @@ namespace Tgstation.Server.Host.Controllers /// The value of /// The value of /// The containing value of - public AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClient gitHubClient, IServerUpdater serverUpdater, IApplication application, IIOManager ioManager, ILogger logger, IOptions updatesConfigurationOptions) : base(databaseContext, authenticationContextFactory) + public AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClient gitHubClient, IServerUpdater serverUpdater, IApplication application, IIOManager ioManager, ILogger logger, IOptions updatesConfigurationOptions) : base(databaseContext, authenticationContextFactory, false) { this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.gitHubClient = gitHubClient ?? throw new ArgumentNullException(nameof(gitHubClient)); diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index 30d503f871..f835f6d004 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -43,6 +43,11 @@ namespace Tgstation.Server.Host.Controllers /// protected Instance Instance { get; } + /// + /// If permissions are required to access the + /// + readonly bool requireInstance; + /// /// Runs after a has been validated. Creates the for the /// @@ -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 /// /// The value of /// The for the - public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory) + /// The value of + 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; } /// 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); diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index 6cda73d094..195745ea11 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -34,7 +34,7 @@ namespace Tgstation.Server.Host.Controllers /// The for the /// The for the /// The value of - 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)); } diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs index fb3abd7ba4..903d08eee9 100644 --- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs +++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs @@ -35,7 +35,7 @@ namespace Tgstation.Server.Host.Controllers /// The for the /// The value of /// The value of - public ConfigurationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger logger) : base(databaseContext, authenticationContextFactory) + public ConfigurationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger logger) : base(databaseContext, authenticationContextFactory, true) { this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index b5574d49c7..0167d49112 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -39,7 +39,7 @@ namespace Tgstation.Server.Host.Controllers /// The for the /// The value of /// The value of - 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)); diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index 3437886044..f07b232017 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -36,7 +36,7 @@ namespace Tgstation.Server.Host.Controllers /// The for the /// The value of /// The value of - 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)); diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs index 414593875e..9ce8efffae 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs @@ -35,9 +35,9 @@ namespace Tgstation.Server.Host.Controllers /// readonly IApplication application; /// - /// The for the + /// The for the /// - readonly IAuthenticationContextFactory authenticationContextFactory; + readonly IIdentityCache identityCache; /// /// Construct a @@ -48,14 +48,14 @@ namespace Tgstation.Server.Host.Controllers /// The value of /// The value of /// The value of - public HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, IApplication application) : base(databaseContext, authenticationContextFactory) + /// The value of + 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)); } /// @@ -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); } } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index e7f38e8a01..aa4a656911 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -42,7 +42,7 @@ namespace Tgstation.Server.Host.Controllers /// The value of /// The value of /// The value of - 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)); diff --git a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs index 9ddc1d43cc..ceb71bd530 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs @@ -31,7 +31,7 @@ namespace Tgstation.Server.Host.Controllers /// The for the /// The for the /// The value of - public InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger) : base(databaseContext, authenticationContextFactory) + public InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger) : base(databaseContext, authenticationContextFactory, false) { this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } diff --git a/src/Tgstation.Server.Host/Controllers/InteropController.cs b/src/Tgstation.Server.Host/Controllers/InteropController.cs index 2f1690884d..d145f99632 100644 --- a/src/Tgstation.Server.Host/Controllers/InteropController.cs +++ b/src/Tgstation.Server.Host/Controllers/InteropController.cs @@ -24,7 +24,7 @@ namespace Tgstation.Server.Host.Controllers /// The for the /// The for the /// The value of - 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; } diff --git a/src/Tgstation.Server.Host/Controllers/JobController.cs b/src/Tgstation.Server.Host/Controllers/JobController.cs index 8219125d29..c7bab7aee3 100644 --- a/src/Tgstation.Server.Host/Controllers/JobController.cs +++ b/src/Tgstation.Server.Host/Controllers/JobController.cs @@ -28,7 +28,7 @@ namespace Tgstation.Server.Host.Controllers /// The for the /// The for the /// The value of - 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)); } diff --git a/src/Tgstation.Server.Host/Controllers/ModelController.cs b/src/Tgstation.Server.Host/Controllers/ModelController.cs index 7943e4ab91..69680526fe 100644 --- a/src/Tgstation.Server.Host/Controllers/ModelController.cs +++ b/src/Tgstation.Server.Host/Controllers/ModelController.cs @@ -24,7 +24,8 @@ namespace Tgstation.Server.Host.Controllers /// /// The for the /// The for the - public ModelController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory) : base(databaseContext, authenticationContextFactory) { } + /// If the requires an + public ModelController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, bool requireInstance) : base(databaseContext, authenticationContextFactory, requireInstance) { } /// /// Attempt to create a @@ -49,7 +50,7 @@ namespace Tgstation.Server.Host.Controllers /// The ID of the model to get /// The for the operation /// A resulting in the of the operation - [HttpGet("/{id}")] + [HttpGet("{id}")] public virtual Task GetId(long id, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound()); /// @@ -75,7 +76,7 @@ namespace Tgstation.Server.Host.Controllers /// /// The for the operation /// A resulting in the of the operation - [HttpGet("/List")] + [HttpGet("List")] public virtual Task List(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound()); } } diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index 376ccd3b26..25bd803e95 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -43,7 +43,7 @@ namespace Tgstation.Server.Host.Controllers /// The value of /// The value of /// The value of - public UserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, ILogger logger) : base(databaseContext, authenticationContextFactory) + public UserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, ILogger logger) : base(databaseContext, authenticationContextFactory, false) { this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory)); diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index c4f285701f..b70dd81236 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -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(); + + services.AddScoped(); + 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 3bddfce971..819e6471f3 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs @@ -10,7 +10,7 @@ using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Security { /// - sealed class AuthenticationContextFactory : IAuthenticationContextFactory, IDisposable + sealed class AuthenticationContextFactory : IAuthenticationContextFactory { /// public IAuthenticationContext CurrentAuthenticationContext { get; private set; } @@ -21,53 +21,22 @@ namespace Tgstation.Server.Host.Security readonly ISystemIdentityFactory systemIdentityFactory; /// - /// The for the + /// The for the /// - readonly IDatabaseContextFactory databaseContextFactory; + readonly IDatabaseContext databaseContext; - /// - /// Map of s to s for that user - /// - readonly Dictionary identityCache; + readonly IIdentityCache identityCache; /// /// Construct an /// /// The value of /// The value of - 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(); - } - - /// - 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); - } + this.databaseContext = databaseContext?? throw new ArgumentNullException(nameof(databaseContext)); + this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache)); } /// @@ -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; diff --git a/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs index 2e8552c037..121a9cc46b 100644 --- a/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs +++ b/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs @@ -15,14 +15,6 @@ 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/IIdentityCache.cs b/src/Tgstation.Server.Host/Security/IIdentityCache.cs new file mode 100644 index 0000000000..902424f6d3 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/IIdentityCache.cs @@ -0,0 +1,23 @@ +using System; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Security +{ + public interface IIdentityCache + { + /// + /// 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); + + /// + /// Attempt to load a cached + /// + /// The the belongs to + /// The cached or if it doesn't exist or expired + ISystemIdentity LoadCachedIdentity(User user); + } +} diff --git a/src/Tgstation.Server.Host/Security/IdentityCache.cs b/src/Tgstation.Server.Host/Security/IdentityCache.cs index 931f8f171c..81850ab4f2 100644 --- a/src/Tgstation.Server.Host/Security/IdentityCache.cs +++ b/src/Tgstation.Server.Host/Security/IdentityCache.cs @@ -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 { - /// - /// For keeping a specific alive for a period of time - /// - sealed class IdentityCache : IDisposable + /// + sealed class IdentityCache : IIdentityCache, IDisposable { - /// - /// The the manages - /// - public ISystemIdentity SystemIdentity { get; } + readonly Dictionary cachedIdentities; - /// - /// 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) + 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(); } /// public void Dispose() { - cancellationTokenSource.Cancel(); - task.Wait(); - cancellationTokenSource.Dispose(); + foreach (var I in cachedIdentities) + 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 (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); + } + } + + /// + 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; + } } } } diff --git a/src/Tgstation.Server.Host/Security/IdentityCacheObject.cs b/src/Tgstation.Server.Host/Security/IdentityCacheObject.cs new file mode 100644 index 0000000000..310c68b34c --- /dev/null +++ b/src/Tgstation.Server.Host/Security/IdentityCacheObject.cs @@ -0,0 +1,68 @@ +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 IdentityCacheObject : 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 to take on expiry + /// The + 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); + } + + /// + public void Dispose() + { + cancellationTokenSource.Cancel(); + task.Wait(); + cancellationTokenSource.Dispose(); + } + } +} diff --git a/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs b/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs index 1e0dc1afcf..6f1ab56cba 100644 --- a/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs +++ b/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs @@ -40,7 +40,13 @@ namespace Tgstation.Server.Host.Security } /// - public void Dispose() => identity.Dispose(); + public void Dispose() + { + if (identity != null) + identity.Dispose(); + else + userPrincipal.Dispose(); + } /// public string Uid => (userPrincipal?.Sid ?? identity.User).ToString(); diff --git a/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs b/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs index 781f0b381e..871269f3ae 100644 --- a/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs +++ b/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs @@ -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 /// public Task 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); diff --git a/tools/TGS.postman_collection.json b/tools/TGS.postman_collection.json index 59bebb834f..b48b57f3d2 100644 --- a/tools/TGS.postman_collection.json +++ b/tools/TGS.postman_collection.json @@ -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" } ]