1

I want to create a New Local User and be able to remove a Local User via a PyQt5 application using subprocess. I am going to have the User enter the New Local User’s information into QLineEdits.

I found several Stack Overflow questions similar to my own, however none of them are exactly related to what I am trying to achieve.

Of course, I Googled the topic and found some information that I tried but am still unsuccessful.

The code I have tried is shown below:

account_type = "\"Local Administrator Account\""
user_name = "\"DIdaho\""
full_name = "\"Duncan Idaho\""
user_password = "\"password\""

For the variables above, I placed the strings in double quotes just as I used for the script I wrote in PowerShell itself which is shown below:

# PowerShell provides a text box to enter the password which obviously is stored in the $Password variable using this line
$Password = Read-Host -AsSecureString
New-LocalUser “DIdaho” -Password $Password -FullName “Duncan Idaho” Description “Local Administrator Account”
Add-LocalGroupMember -Group “Administrators” -Member “DIdaho”

I have written the Python script to match the PowerShell script as close as possible within the subprocess.run(). However, I am not sure at all if I can declare and initialize a PowerShell variable within subprocess, or if I am doing it correctly. I have tried three different ways as shown below:

user_password = subprocess.run([“Powershell.exe”, “$Password = Read-Host -AsSecureString”], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, shell=True)
subprocess.run([“Powershell.exe”, “$Password = Read-Host -AsSecureString”], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, shell=True)
subprocess.Popen([“Powershell.exe”, “$Password = Read-Host -AsSecureString”] , stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, shell=True) )

I am also not sure if the Python variables work within the subprocess.run/Popen() . The code I am using is below:

# Strings placed in double quotes
account_type = "\"Loacal Administrator Account\""
user_name = "\"DIdaho\""
full_name = "\"Amiri Baraka\""
local_group = "\"Administrators\""

subprocess.run(["Powershell.exe", "$Password = Read-Host -AsSecureString"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, shell=True) 
subprocess.run(["Powershell.exe", "New-LocalUser", f"{user_name}", "-Password $Password”, “-FullName {full_name}", f"-Description {account_type}"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, shell=True)
subprocess.run(["Powershell.exe", "Add-LocalGroupMember", "-Group", f"{local_group}", "-Member", f"{user_name}"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, shell=True)
# Strings placed in double quotes
account_type = "\"Loacal Administrator Account\""
user_name = "\"DIdaho\""
full_name = "\"Amiri Baraka\""
local_group = "\"Administrators\""
user_password = "\"password\""
subprocess.run(["Powershell.exe", "$Password = Read-Host -AsSecureString"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, shell=True) 
subprocess.run(["Powershell.exe", "New-LocalUser", f"{user_name}", f"-Password $Password", f"-FullName {full_name}", f"-Description {account_type}"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, shell=True)
subprocess.run(["Powershell.exe", "Add-LocalGroupMember", "-Group", f"{local_group}", "-Member", f"{user_name}"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, shell=True)
# Strings placed in double quotes
account_type = "\"Loacal Administrator Account\""
user_name = "\"DIdaho\""
full_name = "\"Amiri Baraka\""
local_group = "\"Administrators\""

# I tried  Popen believing it would display the text box like it does when working in PowerShell itself.
subprocess.Popen(["Powershell.exe", "$Password = Read-Host -AsSecureString"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, shell=True) 
subprocess.run(["Powershell.exe", "New-LocalUser", f"{user_name}", "-Password $Password”, “-FullName {full_name}", f"-Description {account_type}"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, shell=True)
subprocess.run(["Powershell.exe", "Add-LocalGroupMember", "-Group", f"{local_group}", "-Member", f"{user_name}"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, shell=True)

When attempting to remove a User Account, the application simply does nothing. After clicking the button and going to Local Users and Groups, the User account is still there. The PowerShell script and the Python script are below:

PowerShell:

Remove-LocalUser -Name “DIdaho”

Python:

subprocess.run([“Powershell.exe”, “Remove-LocalUser”, f“-Name {user_name}”], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE,  shell=True)

PYQt5 GUI Application Example:

from PyQt5.QtWidgets import *
from PyQt5 import QtGui
from PyQt5.QtCore import Qt
import sys
import subprocess


class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.resize(850, 200)
        self.setWindowTitle("Create New Local User")
        self.setFont(QtGui.QFont("Arial", 14))
        self.gui()

    def gui(self):
        self.widgets()
        self.layouts()
        self.show()

    def widgets(self):
        self.user_name_ldt = QLineEdit()
        self.user_name_ldt.setPlaceholderText("Enter Username")

        self.user_password_ldt = QLineEdit()
        self.user_password_ldt.setPlaceholderText("Enter User Password")
        self.user_password_ldt.setEchoMode(QLineEdit.Password)

        self.full_name_ldt = QLineEdit()
        self.full_name_ldt.setPlaceholderText("Enter Full Name")

        self.account_description_ldt = QLineEdit()
        self.account_description_ldt.setPlaceholderText("Enter Account Description")

        self.local_group_lbl = QLabel("Local Group:")

        # Define a List initialized with the available Groups
        self.local_groups = ["--None--", "Administrators", "Guest", "Human Resources", "Marketing", "Security"]

        self.local_group_cbx = QComboBox()

        # Populate the local_groups_cbx combobox with the account_types List items
        for local_group in self.local_groups:
            self.local_group_cbx.addItem(local_group)

        self.create_user_btn = QPushButton("Create User")
        self.create_user_btn.clicked.connect(self.create_new_local_user)

    def layouts(self):
        self.main_layout = QVBoxLayout()
        self.row_one_layot = QHBoxLayout()
        self.row_two_layot = QHBoxLayout()
        self.row_three_layout = QHBoxLayout()
        self.row_four_layout = QHBoxLayout()

        self.form_layout = QFormLayout()
        self.form_layout.setSpacing(15)
        self.form_layout.setAlignment(Qt.AlignCenter)

        self.row_one_layot.addWidget(self.local_group_lbl)
        self.row_one_layot.addWidget(self.local_group_cbx)
        self.row_one_layot.addStretch()

        self.row_two_layot.addWidget(self.user_name_ldt)
        self.row_two_layot.addWidget(self.user_password_ldt)

        self.row_three_layout.addWidget(self.full_name_ldt)
        self.row_three_layout.addWidget(self.account_description_ldt)

        # self.row_four_layout.addStretch()
        self.row_four_layout.addWidget(self.create_user_btn)

        self.form_layout.addRow(self.row_one_layot)
        self.form_layout.addRow(self.row_two_layot)
        self.form_layout.addRow(self.row_three_layout)
        self.form_layout.addRow(self.row_four_layout)

        self.main_layout.addLayout(self.form_layout)
        self.main_layout.setContentsMargins(10, 20, 10, 20)

        self.setLayout(self.main_layout)

    def create_new_local_user(self):
        account_description = self.account_description_ldt.text()
        user_name = self.user_name_ldt.text()
        full_name = self.full_name_ldt.text()
        local_group = self.local_group_cbx.currentText()
        user_password = self.user_password_ldt.text()

        if local_group == self.local_groups[0]:
            msgbox = QMessageBox.warning(self, "Null Value", "Please select a Group.")

        else:
            print(account_description)
            print(user_name)
            print(full_name)
            print(local_group)
            print(user_password)

            commands = f'''
                            $Password = Read-Host -AsSecureString
                            New- LocalUser {user_name} -Password $Password -FullName "{full_name}" -Description "{account_description}"
                            Add-LocalGroupMember -Group {local_group} -Member {user_name}
                       '''

            subprocess.run(['Powershell.exe', '-NoProfile', '-Command', commands], stdout=subprocess.PIPE)


def main():
    App = QApplication(sys.argv)
    window = Window()
    sys.exit(App.exec_())


if __name__ == '__main__':
    main()
grippnault
  • 31
  • 6

1 Answers1

0

Use a single call to powershell.exe and pass all commands to the -Command (-c) parameter[1] (multiple calls, aside from being inefficient, run in unrelated child processes that do not share any state):

  • For conceptual clarity, pass the PowerShell code as a single argument to -Command, and use embedded quoting as necessary.

  • For ease of quoting and legible formatting, construct that argument via a multi-line f-string (Python 3.6+):

import subprocess

# Values to embed in the PowerShell code.
# Note that they are NOT placed in embedded double quotes here - 
# that is handled below.
account_type = "Local Administrator Account"
user_name = "DIdaho"
full_name = "Duncan Idaho"
local_group = "Administrators"

# Construct the PowerShell commands with string interpolation,
# *as a single string*.
# Use embedded "..." quoting as needed.
commands = f'''
$Password = Read-Host -AsSecureString
New-LocalUser {user_name} -Password $Password -FullName "{full_name}" -Description "{account_type}"
Add-LocalGroupMember -Group {local_group} -Member {user_name}
'''

# Print prompt text for the Read-Host call that the PowerShell
# command will perform - see remarks below the code.
print('Please enter a password for the new user:')

# Run a powershell.exe subprocess with the commands and capture its
# stdout output (adjust as needed)
# Note: Do NOT use shell=True - doing so would needlessly involve cmd.exe 
#       and introduce syntax pitfalls.
proc = subprocess.run(
  [
    'powershell.exe',
    '-Command',
    commands 
  ],
  stdout=subprocess.PIPE
)

# Print captured stdout output.
print(proc.stdout.decode())

To avoid unnecessary overhead and to ensure a predictable runtime environment, consider placing '-NoProfile', before '-Command',

Note:

  • As in your original code, the above makes PowerShell prompt for the password, which invariably happens in a console window.

    • The reason that a Python print() call is used to print the prompt message beforehand is that due to capturing the PowerShell command's output with stdout=subprocess.PIPE, any prompt text you tried to display via Read-Host itself would become part of the captured output and therefore only show there, after the command has finished.
    • Also note that the captured output will invariably contain the masked representation (e.g. ****) of the password typed by the user in response to the Read-Host call.
  • Your later update revealed that you're creating a GUI application, where making the PowerShell console prompt for the password is probably undesired; there are two solutions that, although note that both involve communicating the password as plain text to the PowerShell process, which is less secure than using Read-Host -AsSecureString from PowerShell:

    • Solution A: Pass the password via stdin to the PowerShell process, which will automatically respond to the Read-Host call. See this answer for an example of sending stdin input to a subprocess, using subprocess.Popen() and Popen.communicate().

    • Solution B, as used by many CI/CD tools: define the password as an environment variable in your Python application (e.g., os.environ['__p'] = 'foo', which PowerShell can then query and convert to a secure string with (in lieu of the Read-Host call):

      ConvertTo-SecureString -AsPlainText -Force $env:__p
      
    • Note: While passing the password as an argument embedded in the PowerShell command string (via string interpolation) technically works too, it is the least secure option, because the plain-text password will then be visible to anyone who can query the child process' command-line string.


[1] Note: With powershell.exe, -Command (-c) is implied, but for conceptual clarity it is better to be explicit, not least because with pwsh, the CLI of the modern, cross-platform PowerShell (Core) edition, -File (-f) is now the default.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thank you very much for your help. Unfortunately, your solution did not work for me. I was thinking that I was not thorough enough when explaining my issue. Above, I have added the code to an example PyQt5 application. This example application gives a better example of what I am trying to accomplish. – grippnault Apr 04 '22 at 02:44
  • As you can see I used your code in the create_new_local_user(). After clicking the Create User button, the application freezes. I then click the close button which displays a message box stating the application is Not Responding and gives me a choice to close the application. – grippnault Apr 04 '22 at 02:55
  • @grippnault, the application doesn't freeze, it waits for you to enter a password \ in response to the `Read-Host -AsSecureString` call. Note that `proc.stdout` will include the masked response you type (e.g., `*****`), and that if you tried to display a prompt message (e.g., `$Password = Read-Host -AsSecureString -Prompt 'Please enter a password'`) it would _not_ be visible (it would only show later in `proc.stdout`. The simplest solution is to print a prompt string _in Python_, before calling PowerShell. – mklement0 Apr 04 '22 at 04:59
  • @grippnault, I missed that you're creating a GUI app. Please see my update, which shows you two ways to communicate a password that you determine (prompt for) on the Python side to the PowerShell subprocess, so that you don't need PowerShell to prompt for the password in a _console_ - but note the security implications. – mklement0 Apr 04 '22 at 16:06