using System; using System.Diagnostics.CodeAnalysis; using System.DirectoryServices.AccountManagement; using System.Runtime.Versioning; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Win32.SafeHandles; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Security { /// /// for windows systems. Uses long running tasks due to potential networked domains. /// [SupportedOSPlatform("windows")] sealed class WindowsSystemIdentityFactory : ISystemIdentityFactory { /// /// The for the . /// readonly ILogger logger; /// /// Extract the username and domain name from a in the format "username\\domainname". /// /// The input . /// The output username. /// The output domain name. May be . static void GetUserAndDomainName(string input, out string username, out string? domainName) { var splits = input.Split('\\'); username = splits.Length > 1 ? splits[1] : splits[0]; domainName = splits.Length > 1 ? splits[0] : null; } /// /// Initializes a new instance of the class. /// /// The value of logger. public WindowsSystemIdentityFactory(ILogger logger) { this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// public ISystemIdentity GetCurrent() => new WindowsSystemIdentity(WindowsIdentity.GetCurrent()); /// public Task CreateSystemIdentity(User user, CancellationToken cancellationToken) => Task.Factory.StartNew( () => { ArgumentNullException.ThrowIfNull(user); if (user.SystemIdentifier == null) throw new InvalidOperationException("User's SystemIdentifier must not be null!"); PrincipalContext? pc = null; GetUserAndDomainName(user.SystemIdentifier, out _, out var domainName); bool TryGetPrincipalFromContextType(ContextType contextType, [NotNullWhen(true)] out UserPrincipal? principal) { principal = null; try { pc = domainName != null ? new PrincipalContext(contextType, domainName) : new PrincipalContext(contextType); cancellationToken.ThrowIfCancellationRequested(); principal = UserPrincipal.FindByIdentity(pc, user.SystemIdentifier); } catch (OperationCanceledException) { throw; } catch (Exception ex) { logger.LogDebug( ex, "Error loading user for context type {contextType} and principal \"{domainName}\"!", contextType, domainName); } finally { if (principal == null) { pc?.Dispose(); cancellationToken.ThrowIfCancellationRequested(); } } return principal != null; } if (!TryGetPrincipalFromContextType(ContextType.Machine, out var principal) && !TryGetPrincipalFromContextType(ContextType.Domain, out principal)) return null; return (ISystemIdentity)new WindowsSystemIdentity(principal); }, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current); /// public Task CreateSystemIdentity(string username, string password, CancellationToken cancellationToken) => Task.Factory.StartNew( () => { ArgumentNullException.ThrowIfNull(username); ArgumentNullException.ThrowIfNull(password); var originalUsername = username; GetUserAndDomainName(originalUsername, out username, out var domainName); var res = NativeMethods.LogonUser(username, domainName, password, 3 /*LOGON32_LOGON_NETWORK*/, 0 /*LOGON32_PROVIDER_DEFAULT*/, out var token); if (!res) { logger.LogTrace("Invalid system identity/password combo for username {0}!", originalUsername); return null; } logger.LogTrace("Authenticated username {0} using system identity!", originalUsername); // checked internally, windows identity always duplicates the handle when constructed using var handle = new SafeAccessTokenHandle(token); return (ISystemIdentity)new WindowsSystemIdentity( new WindowsIdentity(handle.DangerousGetHandle())); // https://github.com/dotnet/corefx/blob/6ed61acebe3214fcf79b4274f2bb9b55c0604a4d/src/System.Security.Principal.Windows/src/System/Security/Principal/WindowsIdentity.cs#L271 }, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current); } }