Erwin.Ried.cl / Desarrollos / Windows /
Implementando el control de cuentas de usuario de Vista

Muchas personas odian el control de cuentas de usuario, ¿porqué mi computador debe preguntarme si estoy seguro?. Windows debe paulatinamente implementar la diferenciación del usuario normal sobre el usuario administrador y si deseas aprender una forma fácil de Vistalizar tus aplicaciones, te invito a leer este documento y comenzar a ser parte de la solución y no del problema (10/01/2009 03:52 PM)


Importante
El código de este artículo es C#. El entorno original utilizado para su desarrollo es la versión Express de C# 2008, disponible gratuitamente para su descarga en la página de Microsoft.


Todos tenemos claro que Windows arrastra años de malas prácticas, en efecto la crítica más conocida por los usuarios más avanzados es que no existe una diferenciación entre los usuarios y los administradores. Esta situación permite que cualquier usuario con poca experiencia o sólo por un pequeño descuido termine instalando algún tipo de malware, spyware o una de esas tonteras.

Para un usuario de Unix y en consecuencia de Mac y Linux, sería una gran irresponsabilidad trabajar utilizando la cuenta de "root", sin embargo para la mayoría de usuarios de Windows esto es una práctica diaria.

Si consideramos que incluso los antiguos entornos de desarrollo de aplicaciones para Windows, incluyendo los productos de Microsoft necesitaban funcionar con privilegios del grupo administrador, entonces podemos comenzar a presagiar el origen de todo el problema.

Pero la verdad es que no vengo a hablar del problema, ni de lo equivocado que estábamos. Con dificultad y rechazo Vista ha intentado imponer un nuevo esquema de seguridad, limpio, y nuestra tarea es (cuando lo necesitemos) otorgar la mejor experiencia de usuario siempre, desde el punto de vista de desarrolladores de aplicaciones. Así mi idea es demostrar de una forma sencilla y que no supone un gran esfuerzo, como integrar nuestras aplicaciones al mundo de Windows Vista de una forma transparente.



¿Cómo trabaja el control de cuentas de usuario, o UAC?

En su forma más sencilla, el control de cuentas de usuario tiene un flujo que va desde un usuario con permisos limitados, en donde el usuario especial SYSTEM muestra una alerta "protegida" en otro escritorio. Cuando la alerta es satisfactoria, se acciona la tarea "administrativa" en la que ahora el propietario es el usuario con privilegios de administrador.

De manera predeterminada, el mismo escritorio que mostré en el documento sobre El problema del compositor y el escritorio seguro es utilizado como transición "segura" pues ninguna otra aplicación puede interactuar directamente con aquel escritorio (a menos de pertenecer a SYSTEM), se captura la pantalla actual y se oscurece para usarla de fondo de las advertencias de UAC, así la transición es menos perceptible.

Nuestra tarea es tan simple como crear esa transición entre la aplicación sin privilegios y las “acciones” administrativas. La forma más sucia de lograr esto, sin realmente hacer nada, es requerir privilegios administrativos siempre, tal como lo hacen algunas aplicaciones (la última que recuerdo es Corel VideoStudio X2, la actualización de Ulead VideoStudio 11, que curiosamente no requería estos privilegios).

Para conseguir esta "solución" sucia, podemos adjuntar un manifiesto (un manifiesto es un documento XML adjunto e incrustado a un archivo ejecutable que puede definir ciertas características del mismo) que defina este requisito a la aplicación, de la siguiente forma:



De parte del usuario, es posible definir este requisito como parte de propiedades de compatibilidad de la aplicación, para lograr el mismo efecto:



Nuestro objetivo será generar una solución mejor que esta.


El paciente

Si no consideraste la presencia del control de cuentas de usuario, o incluso la ausencia de los permisos de administrador (como en una cuenta limitada en Windows XP) puede ser que adaptar tu aplicación cueste un poco de trabajo. Prácticas sucias como escribir archivos temporales a la raíz del disco, al registro y a otras secciones pueden dificultar aún más la tarea.

Para comenzar a trabajar, la aplicación que será mi paciente para el proceso de adaptación será una aplicación personal que desarrolle hace un tiempo, aplicación que fue publicada en el artículo sobre los errores y curiosidades de Windows Vista. Esta aplicación permite cambiar la evaluación del rendimiento que realiza Windows Vista a tu equipo, casi a modo de burla de la misma evaluación. La versión preliminar de esta aplicación luce de la siguiente manera:



Naturalmente, en mis comienzos con Windows Vista quite el control de cuentas de usuario, pertinaz por mis remembranzas de la filosofía existente en Windows XP, en donde como simples mortales somos constantemente dioses para el sistema. Dioses que, insólitamente, pueden cometer torpes errores.

Así la aplicación para modificar la evaluación del rendimiento, necesitaba iniciarse con privilegios de administrador para funcionar correctamente, aún cuando sólo necesitaba estos privilegios al momento de guardar la evaluación.


Leyendo la evaluación del rendimiento de Windows Vista

Los registros de rendimiento son almacenados en el directorio "\Windows\Performance\WinSAT\DataStore", del disco duro que contiene la instalación del sistema operativo. Los nombres de estos archivos tienen la particularidad de contener "Assessment" y "WinSAT.xml" pero internamente pueden contener la fecha y hora de la captura de los datos. Esta variabilidad del nombre nos lleva a crear un código que busque dinámicamente el último de estos archivos en ese directorio de sistema:

Código:
private string FindLastWinSAT()
{
    try
    {
        string wDir = Environment.GetEnvironmentVariable("SystemRoot"), myDir;
        myDir = wDir + "\\Performance\\WinSAT\\DataStore";

        DirectoryInfo myDir2 = new DirectoryInfo(myDir);
        FileInfo[] myFiles = myDir2.GetFiles("*Assessment*WinSAT.xml");

        string[] filePaths = new string[myFiles.Length];
        int i = 0;
        foreach (FileInfo file in myFiles)
            filePaths[i++] = file.FullName;

        Array.Sort(filePaths);

        return filePaths[myFiles.Length - 1];
    }
    catch
    {
        return String.Empty;
    }
}


Si el usuario nunca ha realizado una evaluación del rendimiento (aunque es prácticamente imposible dado que Windows realiza una como parte de la configuración inicial), o sucede cualquier otro problema, se devuelve una cadena vacía.

Como el archivo es un elegante XML, basta con recorrer el árbol en búsqueda de los datos que buscamos:

Código:
XmlDocument myXml = new XmlDocument();
myXml.Load(FindLastWinSAT());

XmlNode myXmlNode = myXml.DocumentElement;

foreach (XmlNode node1 in myXmlNode.ChildNodes)
    foreach (XmlNode node2 in node1.ChildNodes)
        switch (node2.Name)
        {
            // Total
            case "SystemScore": 
            indexValue[5] = ConvertToDouble(node2.InnerText); break;

            // Individuales
            case "CpuScore": 
            indexValue[0] = ConvertToDouble(node2.InnerText); break;
            case "MemoryScore": 
            indexValue[1] = ConvertToDouble(node2.InnerText); break;
            case "GraphicsScore": 
            indexValue[2] = ConvertToDouble(node2.InnerText); break;
            case "GamingScore": 
            indexValue[3] = ConvertToDouble(node2.InnerText); break;
            case "DiskScore": 
            indexValue[4] = ConvertToDouble(node2.InnerText); break;
        }


De esta forma ya hemos obtenido todos los índices actuales del rendimiento.


Escribiendo la evaluación modificada del rendimiento

Escribir los nuevos valores al XML es un proceso similar al de la lectura. En efecto es casi el mismo código. La única diferencia en que en este caso "lastWinSatFile" es el último archivo recuperado por "FindLastWinSat":

Código:
XmlDocument myXml = new XmlDocument();
myXml.Load(lastWinSatFile);

XmlNode myXmlNode = myXml.DocumentElement;

foreach (XmlNode node1 in myXmlNode.ChildNodes)
    foreach (XmlNode node2 in node1.ChildNodes)
        switch (node2.Name)
        {
            // Total
            case "SystemScore": 
            node2.InnerText = AdjustIndex(indexValue[5], "."); break;

            // Individuales
            case "CpuScore": 
            node2.InnerText = AdjustIndex(indexValue[0], "."); break;
            case "MemoryScore": 
            node2.InnerText = AdjustIndex(indexValue[1], "."); break;
            case "GraphicsScore": 
            node2.InnerText = AdjustIndex(indexValue[2], "."); break;
            case "GamingScore": 
            node2.InnerText = AdjustIndex(indexValue[3], "."); break;
            case "DiskScore": 
            node2.InnerText = AdjustIndex(indexValue[4], "."); break;
        }

// Guardando el archivo con los cambios
myXml.Save(lastWinSatFile);


Y eso es todo. El desarrollo del programa llegaría hasta aquí si asumimos en que siempre será ejecutado con privilegios de administración, pero esta vez, no es la idea.


Elevando nuestras acciones

Para elevar la acción de guardar usaré una pequeña triquiñuela. Primero intentaré guardar normalmente los datos en el XML, tal como expliqué en el punto anterior:

Código:
if (!SaveXML())
{
    RestartElevated();
    LoadXML();
}


Entonces, si logramos guardar la configuración, todo sigue naturalmente. Si no logramos nuestro objetivo, llamaremos a "RestartElevated". Esa función no es más que:

Código:
private void RestartElevated()
{
    String args = "-apply;" + indexValue[0] + ":" +
        indexValue[1] + ":" + indexValue[2] + ":" +
        indexValue[3] + ":" + indexValue[4] + ":" +
        indexValue[5];
    RestartElevated(args);
}


Aún no llegamos al punto crucial, por el momento lo único que hemos generado es una cadena de argumentos, cadena que usaremos para llamar al nuevo proceso (el elevado). Ahora veamos como lanzamos aquel nuevo proceso:

Código:
private void RestartElevated(string args)
{
    Process p = Process.Start(GetElevatedProcessStartInfo
        (Application.ExecutablePath, Environment.CurrentDirectory, args));
    p.WaitForExit(Settings.Default.ElevatedProcessTimeout);
}


Ahora finalmente hemos creado un proceso "p", el que retiene nuestra ejecución hasta que finaliza o según demore un tiempo considerable, definido en la configuración de la aplicación. Este proceso obtiene su información de inicio desde la siguiente función (que finalmente nos revelará el secreto de la "misteriosa" elevación de privilegios):

Código:
private ProcessStartInfo GetElevatedProcessStartInfo
    (string executablePath, string currentDirectory, string args)
{
    ProcessStartInfo elevatedProcessStartInfo = new ProcessStartInfo
        (executablePath, args);
    elevatedProcessStartInfo.UseShellExecute = true;
    elevatedProcessStartInfo.WorkingDirectory = currentDirectory;
    elevatedProcessStartInfo.Verb = "runas";

    return elevatedProcessStartInfo;
}


No les mentiré, aunque parezca muy sencillo... ¡lo es!, lo único que hice es colocar "runas" y establecer UseShellExecute. Veamos un poco los detalles de código.

Primero, "verb". En mi documento sobre "El control total sobre el menú contextual" enseño los misterios sobre los comandos y opciones del menú contextual de Windows, cada comando para Windows es un "verb". Por ejemplo, un documento Word tiene acciones como "Nuevo", "Abrir", "Imprimir" cuando hacemos clic secundario. Estas acciones internamente son los "verbs" existentes bajo la rama "HKCR\.doc\shell", nombrados como "New", "Open", "Print" (en realidad, los comandos se encuentran bajo la rama relacionada con la extensión, sobre el tipo maestro, es decir "HKCR\Word.Document.V\shell", en donde V es la versión del tipo de archivo).

Entonces, lo que establecemos como "runas" del archivo ejecutable, es que use el comando de ejecutar como administrador que los archivos ejecutables tienen predeterminado, en el menú contextual de Windows.

Posteriormente tenemos UseShellExecute. Esto viene a ser un requisito de "herencia" del proceso. Al ser un proceso sin privilegios administrativos, no es correcto ni posible lanzar un nuevo proceso que tenga privilegios de administrador directamente. Así, le pedimos a Windows que sea él, el que administre la creación del nuevo proceso.

Recapitulemos toda la situación:

Si no logro guardar los datos, me llamo a mi mismo con unos argumentos que me indiquen lo que debo hacer cuando tenga el poder, para que así no olvide mis deberes e inicie normalmente.


Hay un detalle adicional. En esta ocasión, no necesito mostrar un diálogo ni un mensaje elevado, así no me importa realmente como están interactuando mis dos aplicaciones, la que realiza las acciones con privilegios de administrador y la que la llama, con privilegios limitados. ¿Qué pasaría si lo que deseamos es realmente llamar a una nueva ventana elevada?, más ejemplificado:



En la imagen, para cambiar la hora se llama a un diálogo elevado. Este nuevo diálogo tiene permisos administrativos pero funciona como un formulario modal para el primero. Esto se puede lograr fácilmente añadiendo lo siguiente a la información de inicio del proceso:

Código:
elevatedProcessStartInfo.ErrorDialog = true;
elevatedProcessStartInfo.ErrorDialogParentHandle = this.Handle;



Tengo el poder, pero recibo instrucciones precisas

Lo del título, es lo que el proceso debe hacer ahora. Recibir las instrucciones de la línea de argumentos no es más que procesar rápidamente los mismos:

Nota
Los siguientes sucesos ocurren en el "Main" de la aplicación. En efecto:

Código:
FormMain f = new FormMain();

// Evita cargar el formulario en caso de no ser requerido
if(!f.doNotLoadMainForm)
    Application.Run(f);


El constructor del formulario principal procesará las instrucciones contenidas en los argumentos. De esta forma el proceso será totalmente silencioso cuando sólo haya sido llamado con instrucciones explícitas.


Y así, el constructor del formulario principal lo único que hace es recibir los argumentos como si fueran los valores de las barras de los puntajes y los aplica.

Código:
public FormMain()
{
    indexValue = new double[6];
    lastIndexValue = new double[6];

    // Comprobar si no hay solo que aplicar cambios
    String[] args = Environment.GetCommandLineArgs();

    if (args.Length >= 2)
        if (args[1].Split(';')[0].CompareTo("-apply") == 0)
        {
            String[] applyValues = args[1].Split(';')[1].Split(':');
            if (StartFromArgs(applyValues))
                doNotLoadMainForm = true;
        }

    InitializeComponent();
}



Finalizando con un retoque estético

Como el botón para guardar los cambios es el único que requiere elevación de privilegios, entonces debe ser destacado. El estándar propuesto por Microsoft es constantemente utilizado en su última versión de Windows:




¿Dónde está ese escudo?

Una forma sucia de añadir el distintivo del escudo sería creando un ícono y adjuntándolo al botón. Sin embargo, prácticas así hacen un desarrollo poco mantenible y dependiente (por ejemplo si en Windows 7 llega a alterarse el escudo, si la aplicación se ejecuta en una versión antigua de Windows, entre otros).

Ahora, ¿Dónde estará ese escudo?

No hay muchas posibilidades. Ya conociendo el sistema es fácil predecir que al ser parte de la interfaz de usuario lo más probable es que se encuentre en alguna de las librerías de System32, como por ejemplo Shell32.dll, User32.dll, Comctl32.dll, entre otras de las que todos debemos conocer.

Indagando por unos minutos con un editor de recursos, descubro que es User32.dll, en esta ocasión, quien tiene nuestro tesoro:



Así, la librería User32.dll es la que nos proporciona la capacidad de poder desplegar un escudo en un componente de botón de la interfaz de la aplicación. Más aún, esta hipótesis es fielmente avalada por la base de conocimientos de Microsoft.

A continuación, debemos declarar la librería y su punto de entrada en nuestro código C# (de una forma muy similar a la que lo haríamos en VB):

Código:
[DllImport("user32")]
public static extern int SendMessage
    (IntPtr hWnd, UInt32 msg, UInt32 wParam, UInt32 lParam);


La llamada, entonces, queda de la siguiente forma, siendo "buttonSave" el botón denominado para realizar las acciones con privilegios elevados:

Código:
buttonSave.FlatStyle = FlatStyle.System;
SendMessage(buttonSave.Handle, 0x160C, 0, 0xFFFFFFFF);


¿Porqué de ese código?

La primera sección, sobre "FlatStyle" es imperativa. Al establecerlo a "System" le pasamos el trabajo de dibujar el control directamente al sistema operativo. De esta forma, la alineación del texto y la imagen del control pueden ser establecidos por el sistema operativo.

Luego está la llamada a la librería User32. Tal como especifica la documentación de MSDN, la llamada debe tener la siguiente forma:

Código:
lResult = SendMessage(   // returns LRESULT in lResult     
   (HWND) hWndControl,   // handle to destination control  
   (UINT) BCM_SETSHIELD, // message ID 
   (WPARAM) wParam,      // = 0; not used, must be zero 
   (LPARAM) lParam       // = (LPARAM) (BOOL) fRequired; );


Con estos datos, la definición es sencilla. Se envía un mensaje al botón con su identificador "handle", el mensaje dice BCM_SETSHIELD (160C en hexadecimal). Los parámetros siguientes son fijos, el primero (wParam) siempre es cero y el segundo (lParam) es verdadero (el valor máximo FFFFFFFF, en hexadecimal) para cuando queremos establecer el escudo en el control al que hacemos referencia.


Descarga y código fuente



La nueva versión, aparte de las nuevas características, está localizada al inglés. Esta localización significa que tal como enseñé en el siguiente artículo, la aplicación lucirá completamente traducida al inglés cuando sea necesario. De manera predeterminada se mostrará la interfaz en idioma español.

El código fuente está escrito en C#, la solución está en formato para la versión Express 2008 de Visual Studio C#.

Modificador de la evaluación de la experiencia v1.0.0.4 (código fuente)

No se requiere instalación, simplemente baje los binarios ejecutables y haga doble clic en el ejecutable principal:

Modificador de la evaluación de la experiencia v1.0.0.4 (ejecutables)


Conclusiones

Aunque el proceso de adaptación de las aplicaciones para UAC de Vista, y seguramente para el futuro Windows, implica un trabajo adicional, los resultados son notables. La limpieza de las aplicaciones mezclandose naturalmente en su entorno es más que una simple mejora.

Aplicaciones integradas a este nivel producen una mejor experiencia de usuario y mayor sensación de seguridad, las acciones administrativas deben ser elevadas y no la aplicación en general. De igual forma, las aplicaciones siguen funcionando perfectamente en ambientes en que se cuente con todos los derechos de administración (por ejemplo con el control de cuentas de usuario desactivado).

Sin embargo, esta solución puede llegar a ser tediosa si los requisitos son diferentes, por ejemplo un panel de configuración con muchas opciones, en donde debería adaptarse la aplicación para abordar el problema de otra forma.

Haga clic sobre una de las estrellas para calificar este artículo.

Opiniones y comentarios (Escribir un nuevo comentario)
Hola Muy buen artículo. Es muy util para sistemas XP con multiples usuarios. Anque realmente en la practica, me he topado con muchos clientes a los que el UAC les provoca algunos errores con ciertos programas, me refiero a Vista, el caso mas raro, fue con el cliente de correo Eudora. Sucedía que cada vez que el usuario deseaba abrir el programa, Eudora arrojaba errores aleatorios, similares a: "file xxx.tmp not found".. o algo asi, no lo recuerdo bien. El asunto es que no encontraba un determinado archivo en cada inicio de programa y al intentar leer el inbox, arrojando ese molesto mensaje. Googleando en foros de eudora, la solucion, aunque no muy elegante, pero la unica por el momento, era desactivar el UAC de Vista. Cuento corto, lo hice, y Eudora dejó de arrojar errores aleatorios. El UAC no creo que sea una mala idea, o una basura como dicen muchos, pero es ahi donde entra mi dilema, ¿debe microsoft mejorar su UAC, o los programas son los que deben adaptarse de mejor manera al control de permisos de Vista? Bueno, sorry por lo extenso del comentario, pero creo que es un buen debate :P Gracias.
Escrito por Cesar (16/01/2009 12:52 AM)
En XP funciona de manera normal, en efecto no estoy seguro de como reaccione un programa "Vistalizado". Las aplicaciones siempre confiaron en poder hacer las cosas de formas sucias, UAC no es el culpable, aunque Microsoft nos mal acostumbro durante años.
Escrito por Erwin Ried (16/01/2009 01:20 PM)

Copyright © 2013 por Erwin Ried.