¿Solicitar elevación de UAC desde un script de Python?

91

Quiero que mi secuencia de comandos de Python copie archivos en Vista. Cuando lo ejecuto desde una cmd.exeventana normal , no se generan errores, pero los archivos NO se copian. Si ejecuto cmd.exe"como administrador" y luego ejecuto mi script, funciona bien.

Esto tiene sentido ya que el Control de cuentas de usuario (UAC) normalmente evita muchas acciones del sistema de archivos.

¿Hay alguna manera de que pueda, desde un script de Python, invocar una solicitud de elevación de UAC (esos cuadros de diálogo que dicen algo como "tal y tal aplicación necesita acceso de administrador, está bien?")

Si eso no es posible, ¿hay alguna forma en que mi script pueda al menos detectar que no está elevado para que pueda fallar correctamente?

jwfearn
fuente
3
stackoverflow.com/a/1445547/1628132 después de esta respuesta, crea un .exe desde el script .py usando py2exe y usando una bandera llamada 'uac_info' es una solución bastante ordenada
foxcoreg

Respuestas:

96

A partir de 2017, un método sencillo para lograrlo es el siguiente:

import ctypes, sys

def is_admin():
    try:
        return ctypes.windll.shell32.IsUserAnAdmin()
    except:
        return False

if is_admin():
    # Code of your program here
else:
    # Re-run the program with admin rights
    ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1)

Si está utilizando Python 2.x, debe reemplazar la última línea por:

ctypes.windll.shell32.ShellExecuteW(None, u"runas", unicode(sys.executable), unicode(" ".join(sys.argv)), None, 1)

También nota que si usted convirtió script en Python en un archivo ejecutable (usando herramientas como py2exe, cx_freeze, pyinstaller), entonces usted debe utilizar sys.argv[1:]en lugar desys.argv en el cuarto parámetro.

Algunas de las ventajas aquí son:

  • No se requieren bibliotecas externas. Solo usa ctypesy sysde la biblioteca estándar.
  • Funciona tanto en Python 2 como en Python 3.
  • No es necesario modificar los recursos del archivo ni crear un archivo de manifiesto.
  • Si no agrega el código debajo de la declaración if / else, el código nunca se ejecutará dos veces.
  • Puede obtener el valor de retorno de la llamada a la API en la última línea y realizar una acción si falla (código <= 32). Consulta los posibles valores devueltos aquí .
  • Puede cambiar el método de visualización del proceso generado modificando el sexto parámetro.

La documentación de la llamada ShellExecute subyacente está aquí .

Martín De la Fuente
fuente
9
Tuve que usar instancias Unicode como parámetros para ShellExecuteW (como u'runas 'y unicode (sys.executable)) para que esto funcione.
Janosch
6
@Janosch, eso es porque estás usando Python 2.x, mientras que mi código está en Python 3 (donde todas las cadenas se tratan como Unicodes). Pero es bueno mencionarlo, ¡gracias!
Martín De la Fuente
2
@Martin si estoy ejecutando este código desde la línea de comandos de Windows como esta: "python yourcode.py" simplemente abre python.exe. ¿Hay alguna forma de solucionarlo?
user2978216
1
@ user2978216 Tuve el mismo problema. En la línea se ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, "", None, 1) sys.executableresuelve solo el intérprete de Python (p C:\Python27\Python.exe. Ej. ) La solución es agregar el script en ejecución como argumento (reemplazar ""). ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, __file__, None, 1)También tenga en cuenta que para que esto funcione en python 2.x, todos los argumentos de cadena deben ser unicode (es decir u"runas", unicode(sys.executable)y unicode(__file__))
Javier Ubillos
2
@HrvojeT Ambos, ShellExecuteWy ShellExecuteAson llamadas a la ShellExecutefunción en la API de Windows. El primero obliga a que las cadenas estén en formato unicode y el segundo se usa con formato ANSI
Martín De la Fuente
69

Me tomó un poco de tiempo hacer que la respuesta de dguaraglia funcionara, así que en el interés de ahorrar tiempo a otros, esto es lo que hice para implementar esta idea:

import os
import sys
import win32com.shell.shell as shell
ASADMIN = 'asadmin'

if sys.argv[-1] != ASADMIN:
    script = os.path.abspath(sys.argv[0])
    params = ' '.join([script] + sys.argv[1:] + [ASADMIN])
    shell.ShellExecuteEx(lpVerb='runas', lpFile=sys.executable, lpParameters=params)
    sys.exit(0)
Jorenko
fuente
1
esto simplemente parece elevarse y luego salir ... si pongo algunas declaraciones impresas, no se ejecutan por segunda vez
Joran Beasley
6
@JoranBeasley, no verá ningún resultado. ShellExecuteEx no publica su STDOUT en el shell de origen. En ese sentido, la depuración será ... desafiante. Pero el truco de elevación de privilegios definitivamente funciona.
Tim Keating
1
@TimKeating, ActiveState tiene una receta que debería facilitar un poco la depuración: use la utilidad DebugView con el registro estándar de Python
samwyse
1
parece imposible obtener el resultado en la misma consola, pero con el argumento nShow = 5 para ShellExecuteEx, se abrirá una nueva ventana de comando con el resultado del script elevado.
Emil Styrke
2
Para la cotización, puede utilizar subprocess.list2cmdlinepara hacerlo correctamente.
coderforlife
29

Parece que no hay forma de elevar los privilegios de la aplicación durante un tiempo para que pueda realizar una tarea en particular. Windows necesita saber al inicio del programa si la aplicación requiere ciertos privilegios, y le pedirá al usuario que confirme cuándo la aplicación realiza alguna tarea que necesita esos privilegios. Hay dos maneras de hacer esto:

  1. Escriba un archivo de manifiesto que le diga a Windows que la aplicación podría requerir algunos privilegios
  2. Ejecute la aplicación con privilegios elevados desde dentro de otro programa

Estos dos artículos explican con mucho más detalle cómo funciona.

Lo que haría, si no desea escribir un contenedor ctypes desagradable para la API CreateElevatedProcess, es usar el truco ShellExecuteEx explicado en el artículo del Proyecto de código (Pywin32 viene con un contenedor para ShellExecute). ¿Cómo? Algo como esto:

Cuando su programa se inicia, comprueba si tiene privilegios de administrador, si no los tiene, se ejecuta a sí mismo usando el truco ShellExecute y sale inmediatamente, si los tiene, realiza la tarea en cuestión.

Como describe su programa como un "script", supongo que es suficiente para sus necesidades.

Salud.

dguaraglia
fuente
Gracias por esos enlaces, me resultaron muy útiles para descubrir muchas cosas sobre UAC.
Colen
4
Algo que quizás desee tener en cuenta al respecto es que puede ejecutar ShellExecute sin PyWin32 (tuve problemas para instalarlo) usando os.startfile ($ EXECUTABLE, "runas").
Mike McQuaid
@Mike, pero runasmuestra un nuevo mensaje. Y startfile no acepta argumentos de línea de comando para$EXECUTABLE.
Sridhar Ratnakumar
Agregué otra respuesta con una implementación completa de esta técnica que debería poder agregarse al inicio de cualquier script de Python.
Jorenko
El artículo del segundo vínculo era "Privilegio mínimo: enseña a tus aplicaciones a jugar bien con el control de cuentas de usuario de Windows Vista" en "MSDN Magazine de enero de 2007", pero este número ahora solo está disponible como .chmarchivo.
Peter
6

Solo agrego esta respuesta en caso de que la Búsqueda de Google dirija a otros aquí como yo. Usé el elevatemódulo en mi secuencia de comandos de Python y la secuencia de comandos ejecutada con privilegios de administrador en Windows 10.

https://pypi.org/project/elevate/

Irving Moy
fuente
Oye, intenté usar el elevatemódulo y aparece el error "El sistema no puede acceder al archivo", ¿alguna idea de por qué sucedería eso?
paxos1977
@ paxos1977 ¿Puede publicar un fragmento de código que demuestre ese error? ¡Gracias!
Irving Moy
4

El siguiente ejemplo se basa en el excelente trabajo y la respuesta aceptada de MARTIN DE LA FUENTE SAAVEDRA . En particular, se introducen dos enumeraciones. El primero permite especificar fácilmente cómo se abrirá un programa elevado y el segundo ayuda cuando los errores deben identificarse fácilmente. Tenga en cuenta que si desea que todos los argumentos de línea de comandos pasados al nuevo proceso, sys.argv[0]probablemente debería ser reemplazada con una llamada a la función: subprocess.list2cmdline(sys.argv).

#! /usr/bin/env python3
import ctypes
import enum
import subprocess
import sys

# Reference:
# msdn.microsoft.com/en-us/library/windows/desktop/bb762153(v=vs.85).aspx


# noinspection SpellCheckingInspection
class SW(enum.IntEnum):
    HIDE = 0
    MAXIMIZE = 3
    MINIMIZE = 6
    RESTORE = 9
    SHOW = 5
    SHOWDEFAULT = 10
    SHOWMAXIMIZED = 3
    SHOWMINIMIZED = 2
    SHOWMINNOACTIVE = 7
    SHOWNA = 8
    SHOWNOACTIVATE = 4
    SHOWNORMAL = 1


class ERROR(enum.IntEnum):
    ZERO = 0
    FILE_NOT_FOUND = 2
    PATH_NOT_FOUND = 3
    BAD_FORMAT = 11
    ACCESS_DENIED = 5
    ASSOC_INCOMPLETE = 27
    DDE_BUSY = 30
    DDE_FAIL = 29
    DDE_TIMEOUT = 28
    DLL_NOT_FOUND = 32
    NO_ASSOC = 31
    OOM = 8
    SHARE = 26


def bootstrap():
    if ctypes.windll.shell32.IsUserAnAdmin():
        main()
    else:
       # noinspection SpellCheckingInspection
        hinstance = ctypes.windll.shell32.ShellExecuteW(
            None,
            'runas',
            sys.executable,
            subprocess.list2cmdline(sys.argv),
            None,
            SW.SHOWNORMAL
        )
        if hinstance <= 32:
            raise RuntimeError(ERROR(hinstance))


def main():
    # Your Code Here
    print(input('Echo: '))


if __name__ == '__main__':
    bootstrap()
Noctis Skytower
fuente
4

Reconociendo que esta pregunta se hizo hace años, creo que frmdstryr ofrece una solución más elegante en github usando su módulo pywinutils:

Extracto:

import pythoncom
from win32com.shell import shell,shellcon

def copy(src,dst,flags=shellcon.FOF_NOCONFIRMATION):
    """ Copy files using the built in Windows File copy dialog

    Requires absolute paths. Does NOT create root destination folder if it doesn't exist.
    Overwrites and is recursive by default 
    @see http://msdn.microsoft.com/en-us/library/bb775799(v=vs.85).aspx for flags available
    """
    # @see IFileOperation
    pfo = pythoncom.CoCreateInstance(shell.CLSID_FileOperation,None,pythoncom.CLSCTX_ALL,shell.IID_IFileOperation)

    # Respond with Yes to All for any dialog
    # @see http://msdn.microsoft.com/en-us/library/bb775799(v=vs.85).aspx
    pfo.SetOperationFlags(flags)

    # Set the destionation folder
    dst = shell.SHCreateItemFromParsingName(dst,None,shell.IID_IShellItem)

    if type(src) not in (tuple,list):
        src = (src,)

    for f in src:
        item = shell.SHCreateItemFromParsingName(f,None,shell.IID_IShellItem)
        pfo.CopyItem(item,dst) # Schedule an operation to be performed

    # @see http://msdn.microsoft.com/en-us/library/bb775780(v=vs.85).aspx
    success = pfo.PerformOperations()

    # @see sdn.microsoft.com/en-us/library/bb775769(v=vs.85).aspx
    aborted = pfo.GetAnyOperationsAborted()
    return success is None and not aborted    

Esto utiliza la interfaz COM e indica automáticamente que se necesitan privilegios de administrador con el cuadro de diálogo familiar que vería si estuviera copiando en un directorio donde se requieren privilegios de administrador y también proporciona el cuadro de diálogo de progreso de archivo típico durante la operación de copia.

KenV99
fuente
2

Puede hacer un acceso directo en algún lugar y como destino usar: python yourscript.py luego en propiedades y selección avanzada ejecutar como administrador.

Cuando el usuario ejecuta el acceso directo, le pedirá que eleve la aplicación.

tiendas oficiales
fuente
1

Si su secuencia de comandos siempre requiere privilegios de administrador, entonces:

runas /user:Administrator "python your_script.py"
jfs
fuente
15
¡cuidado, elevación! = corriendo como administrador
Kugel
Soy nuevo en Python ... ¿puedes decirme dónde pondré ese código?
Rahat Islam Khan
@RahatIslamKhan: Abra una ventana del símbolo del sistema y colóquela donde: el comando se ejecuta your_script.pycomo un usuario administrador. Asegúrese de entender el comentario de @ Kugel .
jfs
1

Una variación del trabajo de Jorenko anterior permite que el proceso elevado use la misma consola (pero vea mi comentario a continuación):

def spawn_as_administrator():
    """ Spawn ourself with administrator rights and wait for new process to exit
        Make the new process use the same console as the old one.
          Raise Exception() if we could not get a handle for the new re-run the process
          Raise pywintypes.error() if we could not re-spawn
        Return the exit code of the new process,
          or return None if already running the second admin process. """
    #pylint: disable=no-name-in-module,import-error
    import win32event, win32api, win32process
    import win32com.shell.shell as shell
    if '--admin' in sys.argv:
        return None
    script = os.path.abspath(sys.argv[0])
    params = ' '.join([script] + sys.argv[1:] + ['--admin'])
    SEE_MASK_NO_CONSOLE = 0x00008000
    SEE_MASK_NOCLOSE_PROCESS = 0x00000040
    process = shell.ShellExecuteEx(lpVerb='runas', lpFile=sys.executable, lpParameters=params, fMask=SEE_MASK_NO_CONSOLE|SEE_MASK_NOCLOSE_PROCESS)
    hProcess = process['hProcess']
    if not hProcess:
        raise Exception("Could not identify administrator process to install drivers")
    # It is necessary to wait for the elevated process or else
    #  stdin lines are shared between 2 processes: they get one line each
    INFINITE = -1
    win32event.WaitForSingleObject(hProcess, INFINITE)
    exitcode = win32process.GetExitCodeProcess(hProcess)
    win32api.CloseHandle(hProcess)
    return exitcode
Berwyn
fuente
Lo siento. la misma opción de consola (SEE_MASK_NO_CONSOLE) solo funciona si ya está elevado. Culpa mía.
Berwyn
1

Esta es principalmente una actualización de la respuesta de Jorenko, que permite usar parámetros con espacios en Windows, pero también debería funcionar bastante bien en Linux :) Además, funcionará con cx_freeze o py2exe ya que no usamos __file__pero sys.argv[0]como ejecutable

import sys,ctypes,platform

def is_admin():
    try:
        return ctypes.windll.shell32.IsUserAnAdmin()
    except:
        raise False

if __name__ == '__main__':

    if platform.system() == "Windows":
        if is_admin():
            main(sys.argv[1:])
        else:
            # Re-run the program with admin rights, don't use __file__ since py2exe won't know about it
            # Use sys.argv[0] as script path and sys.argv[1:] as arguments, join them as lpstr, quoting each parameter or spaces will divide parameters
            lpParameters = ""
            # Litteraly quote all parameters which get unquoted when passed to python
            for i, item in enumerate(sys.argv[0:]):
                lpParameters += '"' + item + '" '
            try:
                ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, lpParameters , None, 1)
            except:
                sys.exit(1)
    else:
        main(sys.argv[1:])
Orsiris de Jong
fuente