Search code examples
symfonyconsolesudo

Call symfony console command with sudo from Controller


I'm implementing a web interface using symfony which allow some system restricted command.

To better separate the logic of my code, I've create some console command like:

app/console system:do-restricted --option

And then I call the command from controller like this:

$status = $console->run(new ArrayInput([
    'command' => 'system:do-restricted',
    '--option' => true
]), $output = new BufferedOutput());

Is there a way to allow sudo of the console command?

I think the only way is to reconvert the above command to the shell form and use Process, in which case, there is a simple way to convert InputArray to command and stdout to OutputBaffer (+ansi colors)?


Solution

  • In the end, I've implemented this class to be used in place of symfony's Console\Application:

    <?php
    
    namespace Acme\Model;
    
    use Symfony\Component\Process\Process;
    use Symfony\Component\Console\Input\ArrayInput;
    use Psr\Log\LoggerAwareInterface;
    use Psr\Log\LoggerAwareTrait;
    use Symfony\Component\Console\Output\OutputInterface;
    
    final class Console implements LoggerAwareInterface
    {
        use LoggerAwareTrait;
    
        private $consoleCommand;
    
        public function __construct($consoleCommand = 'sudo app/console')
        {
            $this->consoleCommand = $consoleCommand;
        }
    
        /**
        * Create a process for console command.
        *
        * @param string  $command
        * @param array[] $argv    Same syntax as symfony ArrayInput
        *
        * @see Symfony\Component\Console\Input\ArrayInput
        */
        public function process($command, array $argv = [])
        {
            $console = escapeshellcmd($this->consoleCommand);
    
            $command = escapeshellarg($command);
    
            $options = [];
    
            $arguments = [];
    
            foreach ($argv as $name => $value) {
                if ('--' === substr($name, 0, 2)) {
                    if (false === $value) {
                        continue;
                    }
                    $option = $name;
                    if (is_string($value)) {
                        $option .= '='.$value;
                    }
                    $options[] = escapeshellarg($option);
                } else {
                    $arguments[] = escapeshellarg($value);
                }
            }
    
            $process = new Process(
                $console.' '
                .$command.' '
                .implode(' ', $options).' '
                .implode(' ', $arguments)
            );
    
            if ($this->logger) {
                $this->logger->info(sprintf('Created process for command: %s', $process->getCommandLine()));
            }
    
            return $process;
        }
    
        /**
        * Run a console command.
        *
        * @param string               $command One of the 'app/console' commands
        * @param array[]              $argv    Assoc array '--opt' => true/false, '--opt' => 'value' or 'arg_name' => 'arg_value'
        * @param OutputInterface|null $output  Output object
        *
        * @see Symfony\Component\Console\Input\ArrayInput
        */
        public function run($command, array $argv = [], OutputInterface $output = null)
        {
            if ($output->isDecorated()) {
                $argv['--ansi'] = true;
            }
            $process = $this->process($command, $argv);
    
            $callable = null;
            if (null !== $output) {
                $callable = function ($type, $line) use ($output) {
                    $output->writeln($line);
                };
            }
    
            $exitCode = $process->run($callable);
    
            if ($this->logger) {
                $this->logger->info(sprintf('Command returned: %d', $exitCode), ['output' => $output]);
            }
    
            return $exitCode;
        }
    }
    

    And then I call it like this:

    $status = $this->console->run(
        'balancer:cluster:copy-config',
        ['--option' => true, '--opt-val' => 'value', 'arg1' => 'value1'],
        $output = new BufferedOutput()
    );