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