#region Apache License
//
// Licensed to the Apache Software Foundation (ASF) under one or more 
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership. 
// The ASF licenses this file to you under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with 
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#endregion

#if NET462_OR_GREATER
using System;
using System.Runtime.InteropServices;
using System.Security;
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 : Core.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
  }

  private string? _password;
  private WindowsIdentity? _identity;

  /// <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; set; } = ImpersonationMode.User;

  /// <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; set; }

  /// <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; set; } = Environment.MachineName;

  /// <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>
  [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1044:Properties should not be write only", 
    Justification = "Password must be write only")]
  public string Password { set => _password = value; }

  /// <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 (Credentials == ImpersonationMode.User)
    {
      _identity = LogonUser(UserName.EnsureNotNull(), DomainName.EnsureNotNull(), _password.EnsureNotNull());
    }
  }

  /// <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)
  {
    if (Credentials == ImpersonationMode.User)
    {
      if (_identity is not null)
      {
        return new DisposableImpersonationContext(_identity.Impersonate());
      }
    }
    else if (Credentials == ImpersonationMode.Process)
    {
      // Impersonate(0) will revert to the process credentials
      return new DisposableImpersonationContext(WindowsIdentity.Impersonate(IntPtr.Zero));
    }
    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>
  [System.Security.SecuritySafeCritical]
  [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.
    IntPtr tokenHandle = IntPtr.Zero;
    if (!NativeMethods.LogonUser(userName, domainName, password, logon32LogonInteractive, logon32ProviderDefault, ref tokenHandle))
    {
      NativeError error = NativeError.GetLastError();
      throw new SecurityException($"Failed to LogonUser [{userName}] in Domain [{domainName}]. Error: {error}");
    }

    const int securityImpersonation = 2;
    IntPtr dupeTokenHandle = IntPtr.Zero;
    if (!NativeMethods.DuplicateToken(tokenHandle, securityImpersonation, ref dupeTokenHandle))
    {
      NativeError error = NativeError.GetLastError();
      if (tokenHandle != IntPtr.Zero)
      {
        NativeMethods.CloseHandle(tokenHandle);
      }
      throw new SecurityException($"Failed to DuplicateToken after LogonUser. Error: {error}");
    }

    WindowsIdentity identity = new(dupeTokenHandle);

    // Free the tokens.
    if (dupeTokenHandle != IntPtr.Zero)
    {
      NativeMethods.CloseHandle(dupeTokenHandle);
    }
    if (tokenHandle != IntPtr.Zero)
    {
      NativeMethods.CloseHandle(tokenHandle);
    }

    return identity;
  }

  /// <summary>
  /// Adds <see cref="IDisposable"/> to <see cref="WindowsImpersonationContext"/>
  /// </summary>
  /// <param name="impersonationContext">the impersonation context being wrapped</param>
  /// <remarks>
  /// <para>
  /// Helper class to expose the <see cref="WindowsImpersonationContext"/>
  /// through the <see cref="IDisposable"/> interface.
  /// </para>
  /// </remarks>
  private sealed class DisposableImpersonationContext(WindowsImpersonationContext impersonationContext) : IDisposable
  {
    /// <summary>
    /// Revert the impersonation
    /// </summary>
    public void Dispose() => impersonationContext.Undo();
  }
}
#endif // NET462_OR_GREATER