C# helper class to perform an operation impersonating a Windows account.
#if !NETCF // .NET Compact Framework 1.0 has no support for WindowsIdentity
#if !MONO // MONO 1.0 has no support for Win32 Logon APIs
#if !SSCLI // SSCLI 1.0 has no support for Win32 Logon APIs
#if !CLI_1_0 // We don't want framework or platform specific code in the CLI version of log4net
using System;
using System.Runtime.InteropServices;
using System.Security.Principal;
using log4net.Core;
namespace log4net.Util
{
/// <summary>
/// Impersonate a Windows Account
/// </summary>
/// <remarks>
/// <para>
/// This <see cref="SecurityContext"/> impersonates a Windows account.
/// </para>
/// <para>
/// How the impersonation is done depends on the value of <see cref="Impersonate"/>.
/// This allows the context to either impersonate a set of user credentials specified
/// using username, domain name and password or to revert to the process credentials.
/// </para>
/// </remarks>
public class WindowsSecurityContext : SecurityContext, IOptionHandler
{
/// <summary>
/// The impersonation modes for the <see cref="WindowsSecurityContext"/>
/// </summary>
/// <remarks>
/// <para>
/// See the <see cref="WindowsSecurityContext.Credentials"/> property for
/// details.
/// </para>
/// </remarks>
public enum ImpersonationMode
{
/// <summary>
/// Impersonate a user using the credentials supplied
/// </summary>
User,
/// <summary>
/// Revert this the thread to the credentials of the process
/// </summary>
Process
}
#region Member Variables
private ImpersonationMode _impersonationMode = ImpersonationMode.User;
private string _userName;
private string _domainName = Environment.MachineName;
private string _password;
private WindowsIdentity _identity;
#endregion
#region Constructor
/// <summary>
/// Default constructor
/// </summary>
/// <remarks>
/// <para>
/// Default constructor
/// </para>
/// </remarks>
public WindowsSecurityContext()
{
}
#endregion
#region Public Properties
/// <summary>
/// Gets or sets the impersonation mode for this security context
/// </summary>
/// <value>
/// The impersonation mode for this security context
/// </value>
/// <remarks>
/// <para>
/// Impersonate either a user with user credentials or
/// revert this thread to the credentials of the process.
/// The value is one of the <see cref="ImpersonationMode"/>
/// enum.
/// </para>
/// <para>
/// The default value is <see cref="ImpersonationMode.User"/>
/// </para>
/// <para>
/// When the mode is set to <see cref="ImpersonationMode.User"/>
/// the user's credentials are established using the
/// <see cref="UserName"/>, <see cref="DomainName"/> and <see cref="Password"/>
/// values.
/// </para>
/// <para>
/// When the mode is set to <see cref="ImpersonationMode.Process"/>
/// no other properties need to be set. If the calling thread is
/// impersonating then it will be reverted back to the process credentials.
/// </para>
/// </remarks>
public ImpersonationMode Credentials
{
get { return _impersonationMode; }
set { _impersonationMode = value; }
}
/// <summary>
/// Gets or sets the Windows username for this security context
/// </summary>
/// <value>
/// The Windows username for this security context
/// </value>
/// <remarks>
/// <para>
/// This property must be set if <see cref="Credentials"/>
/// is set to <see cref="ImpersonationMode.User"/> (the default setting).
/// </para>
/// </remarks>
public string UserName
{
get { return _userName; }
set { _userName = value; }
}
/// <summary>
/// Gets or sets the Windows domain name for this security context
/// </summary>
/// <value>
/// The Windows domain name for this security context
/// </value>
/// <remarks>
/// <para>
/// The default value for <see cref="DomainName"/> is the local machine name
/// taken from the <see cref="Environment.MachineName"/> property.
/// </para>
/// <para>
/// This property must be set if <see cref="Credentials"/>
/// is set to <see cref="ImpersonationMode.User"/> (the default setting).
/// </para>
/// </remarks>
public string DomainName
{
get { return _domainName; }
set { _domainName = value; }
}
/// <summary>
/// Sets the password for the Windows account specified by the <see cref="UserName"/> and <see cref="DomainName"/> properties.
/// </summary>
/// <value>
/// The password for the Windows account specified by the <see cref="UserName"/> and <see cref="DomainName"/> properties.
/// </value>
/// <remarks>
/// <para>
/// This property must be set if <see cref="Credentials"/>
/// is set to <see cref="ImpersonationMode.User"/> (the default setting).
/// </para>
/// </remarks>
public string Password
{
set { _password = value; }
}
#endregion
#region IOptionHandler Members
/// <summary>
/// Initialize the SecurityContext based on the options set.
/// </summary>
/// <remarks>
/// <para>
/// This is part of the <see cref="IOptionHandler"/> delayed object
/// activation scheme. The <see cref="ActivateOptions"/> method must
/// be called on this object after the configuration properties have
/// been set. Until <see cref="ActivateOptions"/> is called this
/// object is in an undefined state and must not be used.
/// </para>
/// <para>
/// If any of the configuration properties are modified then
/// <see cref="ActivateOptions"/> must be called again.
/// </para>
/// <para>
/// The security context will try to Logon the specified user account and
/// capture a primary token for impersonation.
/// </para>
/// </remarks>
/// <exception cref="ArgumentNullException">The required <see cref="UserName" />,
/// <see cref="DomainName" /> or <see cref="Password" /> properties were not specified.</exception>
public void ActivateOptions()
{
if (_impersonationMode == ImpersonationMode.User)
{
if (_userName == null) throw new ArgumentNullException(nameof(_userName));
if (_domainName == null) throw new ArgumentNullException(nameof(_domainName));
if (_password == null) throw new ArgumentNullException(nameof(_password));
_identity = LogonUser(_userName, _domainName, _password);
}
}
#endregion
/// <summary>
/// Impersonate the Windows account specified by the <see cref="UserName"/> and <see cref="DomainName"/> properties.
/// </summary>
/// <param name="state">caller provided state</param>
/// <returns>
/// An <see cref="IDisposable"/> instance that will revoke the impersonation of this SecurityContext
/// </returns>
/// <remarks>
/// <para>
/// Depending on the <see cref="Credentials"/> property either
/// impersonate a user using credentials supplied or revert
/// to the process credentials.
/// </para>
/// </remarks>
public override IDisposable Impersonate(object state)
{
switch (_impersonationMode)
{
case ImpersonationMode.User when _identity != null:
return new DisposableImpersonationContext(_identity.Impersonate());
case ImpersonationMode.Process:
// Impersonate(0) will revert to the process credentials
return new DisposableImpersonationContext(WindowsIdentity.Impersonate(IntPtr.Zero));
default:
return null;
}
}
/// <summary>
/// Create a <see cref="WindowsIdentity"/> given the userName, domainName and password.
/// </summary>
/// <param name="userName">the user name</param>
/// <param name="domainName">the domain name</param>
/// <param name="password">the password</param>
/// <returns>the <see cref="WindowsIdentity"/> for the account specified</returns>
/// <remarks>
/// <para>
/// Uses the Windows API call LogonUser to get a principal token for the account. This
/// token is used to initialize the WindowsIdentity.
/// </para>
/// </remarks>
#if NET_4_0 || MONO_4_0
[System.Security.SecuritySafeCritical]
#endif
[System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand, UnmanagedCode = true)]
private static WindowsIdentity LogonUser(string userName, string domainName, string password)
{
const int logon32ProviderDefault = 0;
//This parameter causes LogonUser to create a primary token.
const int logon32LogonInteractive = 2;
// Call LogonUser to obtain a handle to an access token.
var tokenHandle = IntPtr.Zero;
if (!LogonUser(userName, domainName, password, logon32LogonInteractive, logon32ProviderDefault, ref tokenHandle))
{
var error = NativeError.GetLastError();
throw new Exception("Failed to LogonUser [" + userName + "] in Domain [" + domainName + "]. Error: " + error);
}
const int securityImpersonation = 2;
var dupeTokenHandle = IntPtr.Zero;
if (!DuplicateToken(tokenHandle, securityImpersonation, ref dupeTokenHandle))
{
var error = NativeError.GetLastError();
if (tokenHandle != IntPtr.Zero)
{
CloseHandle(tokenHandle);
}
throw new Exception("Failed to DuplicateToken after LogonUser. Error: " + error);
}
var identity = new WindowsIdentity(dupeTokenHandle);
// Free the tokens.
if (dupeTokenHandle != IntPtr.Zero)
{
CloseHandle(dupeTokenHandle);
}
if (tokenHandle != IntPtr.Zero)
{
CloseHandle(tokenHandle);
}
return identity;
}
#region Native Method Stubs
[DllImport("advapi32.dll", SetLastError = true)]
private static extern bool LogonUser(string lpszUsername, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
private static extern bool CloseHandle(IntPtr handle);
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool DuplicateToken(IntPtr existingTokenHandle, int securityImpersonationLevel, ref IntPtr duplicateTokenHandle);
#endregion
#region DisposableImpersonationContext class
/// <summary>
/// Adds <see cref="IDisposable"/> to <see cref="WindowsImpersonationContext"/>
/// </summary>
/// <remarks>
/// <para>
/// Helper class to expose the <see cref="WindowsImpersonationContext"/>
/// through the <see cref="IDisposable"/> interface.
/// </para>
/// </remarks>
private sealed class DisposableImpersonationContext : IDisposable
{
private readonly WindowsImpersonationContext _impersonationContext;
/// <summary>
/// Constructor
/// </summary>
/// <param name="impersonationContext">the impersonation context being wrapped</param>
/// <remarks>
/// <para>
/// Constructor
/// </para>
/// </remarks>
public DisposableImpersonationContext(WindowsImpersonationContext impersonationContext)
{
_impersonationContext = impersonationContext;
}
/// <summary>
/// Revert the impersonation
/// </summary>
/// <remarks>
/// <para>
/// Revert the impersonation
/// </para>
/// </remarks>
public void Dispose()
{
_impersonationContext.Undo();
}
}
#endregion
}
}
#endif // !CLI_1_0
#endif // !SSCLI
#endif // !MONO
#endif // !NETCF