Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to run some CMD commands on Windows 10 as administrator using Nodejs and spawn?

I am working with Nodejs on Windows 10. Every time the app is opened I want to check if the files with the extension ext are associated to my app. If not I would need to run this commands on cmd in order to make the association again:

assoc .ext=CustomExt
ftype CustomExt=app.exe %1 %*

But I would need administrator privileges to make this work.

I have read this thread where Jeff M says exactly what I want to achieve, but nobody answers him:

From a usability perspective, the ideal would be that the user is prompted for elevated privileges only when he takes an action within my node program that actually requires those privileges. The second best option would be if the user was prompted for elevated privileges when they first start the node program. The least ideal is if the user isn't prompted at all, that it would be up to the user to always remember to start the program by right-click run as administrator. This is the least ideal because it puts to the burden on the user to remember to always start the program in a non-typical way.

So, to get back to my original question, right now if I try to spawn a child process where the program being spawned requires elevated privileges (example code in my original message), the current behavior is that the user is not prompted for those privileges, and instead the node program crashes with an error. How can I get my node program to request those privileges instead of just dying?

My nodejs script:

const { spawn } = require('child_process');

const assoc = spawn(                    // how to run this as admin privileges?
    'assoc',                            // command
    ['.ext=CustomExt'],                 // args
    {'shell': true }                    // options: run this into the shell
);

assoc.stdout.on('data', (buffer) => {
    var message = buffer.toString('utf8');
    console.log(message);
});

assoc.stderr.on('data', (data) => {
    console.log(`stderr: ${data}`);
});

assoc.on('close', (code) => {
    console.log(`child process exited with code ${code}`);
});

Any other way to create an extension association within Nodejs would be appreciated as well.

Update 21 Jan 2020

I believe that VSCode can do this. So I would need to explore its source code to find the solution. Now I am in other projects, so I do not have time to research for a while. When a file should be saved with root priviledges a message like this is prompted, and that's exactly what I need:

vscode prompt

Update 24 June 2021

I have found a useful thread in VSCode issues. Using sudo-prompt could be the solution. I haven't tried it yet, but the dependency is still in the package.json, so it looks promising.

like image 518
ChesuCR Avatar asked Oct 31 '18 18:10

ChesuCR


1 Answers

New and Improved Answer

See notes in old answer for background. I found a way to get this going without having to allow running scripts on the user machine:

import Shell from 'node-powershell'

/** Options to use when running an executable as admin */
interface RunAsAdminCommand {
  /** Path to the executable to run */
  path: string
  /** Working dir to run the executable from */
  workingDir?: string
}
/**
 * Runs a PowerShell command or an executable as admin
 *
 * @param command If a string is provided, it will be used as a command to
 *   execute in an elevated PowerShell. If an object with `path` is provided,
 *   the executable will be started in Run As Admin mode
 *
 * If providing a string for elevated PowerShell, ensure the command is parsed
 *   by PowerShell correctly by using an interpolated string and wrap the
 *   command in double quotes.
 *
 * Example:
 *
 * ```
 * `"Do-The-Thing -Param '${pathToFile}'"`
 * ```
 */
const runAsAdmin = async (command: string | RunAsAdminCommand): Promise<string> => {
  const usePowerShell = typeof command === 'string'
  const shell = new Shell({})
  await shell.addCommand('Start-Process')
  if (usePowerShell) await shell.addArgument('PowerShell')
  // Elevate the process
  await shell.addArgument('-Verb')
  await shell.addArgument('RunAs')
  // Hide the window for cleaner UX
  await shell.addArgument('-WindowStyle')
  await shell.addArgument('Hidden')
  // Propagate output from child process
  await shell.addArgument('-PassThru')
  // Wait for the child process to finish before exiting
  if (usePowerShell) await shell.addArgument('-Wait')

  if (usePowerShell) {
    // Pass argument list to use in elevated PowerShell
    await shell.addArgument('-ArgumentList')
    await shell.addArgument(command as string)
  } else {
    // Point to executable to run
    await shell.addArgument('-FilePath')
    await shell.addArgument(`'${(command as RunAsAdminCommand).path}'`)

    if ((command as RunAsAdminCommand).workingDir) {
      // Point to working directory to run the executable from
      await shell.addArgument('-WorkingDirectory')
      await shell.addArgument(`'${(command as RunAsAdminCommand).workingDir}'`)
    }
  }

  await shell.invoke()
  return await shell.dispose()
}

Example of usage:

const unzip = async (
  zipPath: string,
  destinationPath: string
): Promise<string> =>
  await runAsAdmin(
    `"Expand-Archive -Path '${zipPath}' -DestinationPath '${destinationPath}'"`
  )

Old Answer

I'm no security expert, please wave the red flag if you see something stupid going on here.

You can use PowerShell's Start-Process cmdlet in a child process. You'd need to include -Verb RunAs in order for it to elevate the prompt. This will prompt the user to allow the process to be run as Admin anytime the cmdlet is called. You can simplify things a bit by using node-powershell if desired.

Here's an example of the cmdlet with applicable flags (just replace [PATH_TO_EXECUTABLE] and [PATH_TO_DIR_TO_RUN_FROM] as needed):

Start-Process -FilePath [PATH_TO_EXECUTABLE] -PassThru -Verb RunAs -WorkingDirectory [PATH_TO_DIR_TO_RUN_FROM]

Caveat

Our app does this using .ps1 files that are packaged in an Electron app, rather than one-liner PowerShell scripts. This isn't a problem in our case because the app is an internal tool, so end users trust when we instruct them to set up applicable PowerShell permissions to allow running scripts. If you can't rely on .ps1 files your mileage may vary. If this is an option available to you, Set-ExecutionPolicy is your friend. We have users do something like this after installing the app:

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
dir "$env:USERPROFILE\AppData\Local\Programs\path\to\our\scripts" | Unblock-File

Where $env:USERPROFILE\AppData\Local\Programs\path\to\our\scripts points to the directory of .ps1 files packaged with the app. This way the user is only allowing PowerShell to run scripts we've provided to them, rather than doing something silly like having them allow all PowerShell scripts to be run regardless of origin.

The -ExecutionPolicy RemoteSigned is the secret sauce here; it's saying "only execute downloaded scripts that are from a trusted source". (You could also use -ExecutionPolicy AllSigned to be a bit more restrictive.) The next line where you pipe all files in the scripts directory to Unblock-File says "the scripts in this directory are from a trusted source."

like image 100
Jack Barry Avatar answered Oct 04 '22 22:10

Jack Barry