miércoles, 26 de octubre de 2011

Cómo impersonar el usuario en una aplicación Windows Forms .Net

Hay veces en los que puede ser necesario ejecutar la aplicación con unas credenciales diferentes a las del usuario que ha iniciado sesión en la máquina. Imagínate por ejemplo que la aplicación necesita acceder a un recurso (directorio en un servidor remoto) al que únicamente se puede acceder con una cuenta de usuario (que justamente no es la tuya).

Después de buscar por Internet encontré la forma de hacerlo en Visual Basic .Net en la página de CodeProject. Puedes consultar la fuente original aquí (te animo hacerlo que el mérito aquí lo tiene Alex B. Clarke).

Os cuelgo el código de la clase que realiza "el milagro" para que os sea fácil reutilizarla en cualquier solución .Net. Como veréis, su uso es muy sencillo: simplemente tenéis que llamar al método ImpersonateUser() de la clase Impersonate desde vuestra aplicación de escritorio y a partir de esa llamada vuestra aplicación estará utilizando las credenciales del usuario que hayáis especificado.
Para que la aplicación se vuelva a ejecutar con los permisos del usuario que ha ejecutado el programa, basta con llamar a la función Undo().

Tened presente que el usuario utilizado en la impersonalización puede que no tenga permisos de acceso a los recursos de la máquina en los que se está ejecutando la aplicación, con lo que podéis tener ciertas restricciones en este sentido (en mi caso por ejemplo la aplicación en cuestión accede a un directorio de un servidor con unas credenciales diferentes a las que iniciado sesión en la máquina, accede a un PDF y lo visualiza por pantalla; pues bien, la visualización en PDF no funcionaba si no deshacía antes la impersonalización).

Y ahora el código:

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Runtime.InteropServices;  // DllImport
using System.Security.Principal; // WindowsImpersonationContext
using System.Security.Permissions; // PermissionSetAttribute
using System.IO;

namespace ImpersonateLib
{
    // group type enum
    public enum SECURITY_IMPERSONATION_LEVEL : int
    {
        SecurityAnonymous = 0,
        SecurityIdentification = 1,
        SecurityImpersonation = 2,
        SecurityDelegation = 3
    }

    /// <summary>
    /// Summary description for Form1.
    /// </summary>
    public static class Impersonate
    {
        // obtains user token
        [DllImport("advapi32.dll", SetLastError = true)]
        private static extern bool LogonUser(string pszUsername, string pszDomain, string pszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);

        // closes open handes returned by LogonUser
        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        private extern static bool CloseHandle(IntPtr handle);

        // creates duplicate token handle
        [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private extern static bool DuplicateToken(IntPtr ExistingTokenHandle, int SECURITY_IMPERSONATION_LEVEL, ref IntPtr DuplicateTokenHandle);

        /// <summary>
        /// Attempts to impersonate a user.  If successful, returns
        /// a WindowsImpersonationContext of the new users identity.
        /// </summary>
        /// <param name="sUsername">Username you want to impersonate</param>
        /// <param name="sDomain">Logon domain</param>
        /// <param name="sPassword">User's password to logon with</param></param>
        /// <returns></returns>
        public static WindowsImpersonationContext ImpersonateUser(string sUsername, string sDomain, string sPassword)
        {
            // initialize tokens
            IntPtr pExistingTokenHandle = new IntPtr(0);
            IntPtr pDuplicateTokenHandle = new IntPtr(0);
            pExistingTokenHandle = IntPtr.Zero;
            pDuplicateTokenHandle = IntPtr.Zero;

            // if domain name was blank, assume local machine
            if (sDomain == "")
                sDomain = System.Environment.MachineName;

            try
            {
                string sResult = null;

                const int LOGON32_PROVIDER_DEFAULT = 0;
                // create token
                const int LOGON32_LOGON_INTERACTIVE = 2;

                // get handle to token
                bool bImpersonated = LogonUser(sUsername, sDomain, sPassword,
                    LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, ref pExistingTokenHandle);

                // did impersonation fail?
                if (false == bImpersonated)
                {
                    int nErrorCode = Marshal.GetLastWin32Error();
                    sResult = "LogonUser() failed with error code: " + nErrorCode + "\r\n";

                    // show the reason why LogonUser failed
                    throw new Exception(sResult);
                }

                // Get identity before impersonation
                sResult += "Before impersonation: " + WindowsIdentity.GetCurrent().Name + "\r\n";

                bool bRetVal = DuplicateToken(pExistingTokenHandle, (int)SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, ref pDuplicateTokenHandle);

                // did DuplicateToken fail?
                if (false == bRetVal)
                {
                    int nErrorCode = Marshal.GetLastWin32Error();
                    CloseHandle(pExistingTokenHandle); // close existing handle
                    sResult += "DuplicateToken() failed with error code: " + nErrorCode + "\r\n";

                    // show the reason why DuplicateToken failed
                    throw new Exception(sResult);
                }
                else
                {
                    // create new identity using new primary token
                    WindowsIdentity newId = new WindowsIdentity(pDuplicateTokenHandle);
                    WindowsImpersonationContext impersonatedUser = newId.Impersonate();

                    return impersonatedUser;

                    //// check the identity after impersonation
                    //sResult += "After impersonation: " + WindowsIdentity.GetCurrent().Name + "\r\n";

                    //throw new Exception(sResult);
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
            finally
            {
                // close handle(s)
                if (pExistingTokenHandle != IntPtr.Zero)
                    CloseHandle(pExistingTokenHandle);
                if (pDuplicateTokenHandle != IntPtr.Zero)
                    CloseHandle(pDuplicateTokenHandle);
            }
        }
    }
}

2 comentarios:

  1. Fantástica clase. Yo estoy haciendo lo mismo que tu hiciste. Tengo el mismo problema, no puedo abrir el pdf que se encuentra en el servidor porque el usuario que sí accede a la carpeta donde está el archivo PDF no tiene permisos para usar el Acrobat de la máquina donde se ha lanzado la aplicación. ¿Podrías ayudarme? No se como poner el código para que se pueda abrir.

    ResponderEliminar
  2. Buenas tardes.

    Antes de nada agradecerte mucho el código que nos has regalado. Es de mucha utilidad.
    No estoy muy puesto en esto de la impersonación pero me parece muy util para el tema de seguridad de aplicaciones.

    En la primera parte comentas que para restaurar los valores originales tras la acción que se realice con la impersonación debemos utilizar la función Undo, pero, ¿esa función pertenece a la clase Impersonate?, porque no la encuentro o pertenece a otra clase.

    Muchas gracias.

    ResponderEliminar