diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 43d7f62950e83..cc2151d6addbf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * Move the Router `cache_dir` to `kernel.build_dir` * Deprecate the `router.cache_dir` config option * Add `rate_limiter` tags to rate limiter services + * Add `secrets:reveal` command 7.0 --- diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRevealCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRevealCommand.php new file mode 100644 index 0000000000000..bcbdea11f079c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRevealCommand.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Command; + +use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * @internal + */ +#[AsCommand(name: 'secrets:reveal', description: 'Reveal the value of a secret')] +final class SecretsRevealCommand extends Command +{ + public function __construct( + private readonly AbstractVault $vault, + private readonly ?AbstractVault $localVault = null, + ) { + parent::__construct(); + } + + protected function configure(): void + { + $this + ->addArgument('name', InputArgument::REQUIRED, 'The name of the secret to reveal', null, fn () => array_keys($this->vault->list())) + ->setHelp(<<<'EOF' +The %command.name% command reveals a stored secret. + + %command.full_name% +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + + $secrets = $this->vault->list(true); + $localSecrets = $this->localVault?->list(true); + + $name = (string) $input->getArgument('name'); + + if (null !== $localSecrets && \array_key_exists($name, $localSecrets)) { + $io->writeln($localSecrets[$name]); + } else { + if (!\array_key_exists($name, $secrets)) { + $io->error(sprintf('The secret "%s" does not exist.', $name)); + + return self::INVALID; + } + + $io->writeln($secrets[$name]); + } + + return self::SUCCESS; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 095d782aa12a3..38de7421dacc2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1773,6 +1773,7 @@ private function registerSecretsConfiguration(array $config, ContainerBuilder $c if (!$this->readConfigEnabled('secrets', $container, $config)) { $container->removeDefinition('console.command.secrets_set'); $container->removeDefinition('console.command.secrets_list'); + $container->removeDefinition('console.command.secrets_reveal'); $container->removeDefinition('console.command.secrets_remove'); $container->removeDefinition('console.command.secrets_generate_key'); $container->removeDefinition('console.command.secrets_decrypt_to_local'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php index 334d20426c68c..b4f7dfcf3ea5e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php @@ -33,6 +33,7 @@ use Symfony\Bundle\FrameworkBundle\Command\SecretsGenerateKeysCommand; use Symfony\Bundle\FrameworkBundle\Command\SecretsListCommand; use Symfony\Bundle\FrameworkBundle\Command\SecretsRemoveCommand; +use Symfony\Bundle\FrameworkBundle\Command\SecretsRevealCommand; use Symfony\Bundle\FrameworkBundle\Command\SecretsSetCommand; use Symfony\Bundle\FrameworkBundle\Command\TranslationDebugCommand; use Symfony\Bundle\FrameworkBundle\Command\TranslationUpdateCommand; @@ -355,6 +356,13 @@ ]) ->tag('console.command') + ->set('console.command.secrets_reveal', SecretsRevealCommand::class) + ->args([ + service('secrets.vault'), + service('secrets.local_vault')->ignoreOnInvalid(), + ]) + ->tag('console.command') + ->set('console.command.secrets_decrypt_to_local', SecretsDecryptToLocalCommand::class) ->args([ service('secrets.vault'), diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsRevealCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsRevealCommandTest.php new file mode 100644 index 0000000000000..94643db2c92c5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsRevealCommandTest.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Command; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Command\SecretsRevealCommand; +use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; +use Symfony\Bundle\FrameworkBundle\Secrets\DotenvVault; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Tester\CommandTester; + +class SecretsRevealCommandTest extends TestCase +{ + public function testExecute() + { + $vault = $this->createMock(AbstractVault::class); + $vault->method('list')->willReturn(['secretKey' => 'secretValue']); + + $command = new SecretsRevealCommand($vault); + + $tester = new CommandTester($command); + $this->assertSame(Command::SUCCESS, $tester->execute(['name' => 'secretKey'])); + + $this->assertEquals('secretValue', trim($tester->getDisplay(true))); + } + + public function testInvalidName() + { + $vault = $this->createMock(AbstractVault::class); + $vault->method('list')->willReturn(['secretKey' => 'secretValue']); + + $command = new SecretsRevealCommand($vault); + + $tester = new CommandTester($command); + $this->assertSame(Command::INVALID, $tester->execute(['name' => 'undefinedKey'])); + + $this->assertStringContainsString('The secret "undefinedKey" does not exist.', trim($tester->getDisplay(true))); + } + + /** + * @backupGlobals enabled + */ + public function testLocalVaultOverride() + { + $vault = $this->createMock(AbstractVault::class); + $vault->method('list')->willReturn(['secretKey' => 'secretValue']); + + $_ENV = ['secretKey' => 'newSecretValue']; + $localVault = new DotenvVault('/not/a/path'); + + $command = new SecretsRevealCommand($vault, $localVault); + + $tester = new CommandTester($command); + $this->assertSame(Command::SUCCESS, $tester->execute(['name' => 'secretKey'])); + + $this->assertEquals('newSecretValue', trim($tester->getDisplay(true))); + } + + /** + * @backupGlobals enabled + */ + public function testOnlyLocalVaultContainsName() + { + $vault = $this->createMock(AbstractVault::class); + $vault->method('list')->willReturn(['otherKey' => 'secretValue']); + + $_ENV = ['secretKey' => 'secretValue']; + $localVault = new DotenvVault('/not/a/path'); + + $command = new SecretsRevealCommand($vault, $localVault); + + $tester = new CommandTester($command); + $this->assertSame(Command::SUCCESS, $tester->execute(['name' => 'secretKey'])); + + $this->assertEquals('secretValue', trim($tester->getDisplay(true))); + } +}