From 2ec9647e750feb5f3d52baf3d8b6e888e12b7abd Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sat, 26 Oct 2019 14:59:20 +0200 Subject: [PATCH] [FrameworkBundle] Remove suffix convention when using env vars to override secrets from the vault --- .../Command/SecretsDecryptToLocalCommand.php | 2 +- .../SecretsEncryptFromLocalCommand.php | 26 +++++------------ .../Command/SecretsListCommand.php | 6 ++-- .../Command/SecretsSetCommand.php | 6 ++++ .../FrameworkBundle/Secrets/DotenvVault.php | 19 ++++++------- .../Secrets/SecretEnvVarProcessor.php | 4 +-- .../FrameworkBundle/Secrets/SodiumVault.php | 28 +++++++++++++++---- .../Tests/Secrets/DotenvVaultTest.php | 10 +++---- 8 files changed, 54 insertions(+), 47 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsDecryptToLocalCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsDecryptToLocalCommand.php index c9370768f0..e4fbfd287e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsDecryptToLocalCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsDecryptToLocalCommand.php @@ -45,7 +45,7 @@ final class SecretsDecryptToLocalCommand extends Command ->setDescription('Decrypts all secrets and stores them in the local vault.') ->addOption('force', 'f', InputOption::VALUE_NONE, 'Forces overriding of secrets that already exist in the local vault') ->setHelp(<<<'EOF' -The %command.name% command list decrypts all secrets and stores them in the local vault.. +The %command.name% command decrypts all secrets and copies them in the local vault. %command.full_name% diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsEncryptFromLocalCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsEncryptFromLocalCommand.php index 3161a21982..607140e616 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsEncryptFromLocalCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsEncryptFromLocalCommand.php @@ -14,7 +14,6 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; @@ -43,15 +42,10 @@ final class SecretsEncryptFromLocalCommand extends Command { $this ->setDescription('Encrypts all local secrets to the vault.') - ->addOption('force', 'f', InputOption::VALUE_NONE, 'Forces overriding of secrets that already exist in the vault') ->setHelp(<<<'EOF' -The %command.name% command list encrypts all local secrets and stores them in the vault.. +The %command.name% command encrypts all locally overridden secrets to the vault. %command.full_name% - -When the option --force is provided, secrets that already exist in the vault are overriden. - - %command.full_name% --force EOF ) ; @@ -67,22 +61,16 @@ EOF return 1; } - $secrets = $this->localVault->list(true); + foreach ($this->vault->list(true) as $name => $value) { + $localValue = $this->localVault->reveal($name); - if (!$input->getOption('force')) { - foreach ($this->vault->list() as $k => $v) { - unset($secrets[$k]); - } - } - - foreach ($secrets as $k => $v) { - if (null === $v) { - $io->error($this->localVault->getLastMessage()); + if (null !== $localValue && $value !== $localValue) { + $this->vault->seal($name, $localValue); + } elseif (null !== $message = $this->localVault->getLastMessage()) { + $io->error($message); return 1; } - - $this->vault->seal($k, $v); } return 0; diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php index 4ae190435d..1b0fbdf4ce 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php @@ -89,11 +89,11 @@ EOF } foreach ($localSecrets ?? [] as $name => $value) { - $rows[$name] = [$name, $rows[$name][1] ?? '', $dump($value)]; + if (isset($rows[$name])) { + $rows[$name][] = $dump($value); + } } - uksort($rows, 'strnatcmp'); - if (null !== $this->localVault && null !== $message = $this->localVault->getLastMessage()) { $io->comment($message); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php index 850cf08ee3..555d616712 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php @@ -86,6 +86,12 @@ EOF return 1; } + if ($this->localVault === $vault && !\array_key_exists($name, $this->vault->list())) { + $io->error(sprintf('Secret "%s" does not exist in the vault, you cannot override it locally.', $name)); + + return 1; + } + if (0 < $random = $input->getOption('random') ?? 16) { $value = strtr(substr(base64_encode(random_bytes($random)), 0, $random), '+/', '-_'); } elseif (!$file = $input->getArgument('file')) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php b/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php index 16df2a6045..93d8989502 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php +++ b/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php @@ -36,14 +36,13 @@ class DotenvVault extends AbstractVault { $this->lastMessage = null; $this->validateName($name); - $k = $name.'_SECRET'; $v = str_replace("'", "'\\''", $value); $content = file_exists($this->dotenvFile) ? file_get_contents($this->dotenvFile) : ''; - $content = preg_replace("/^$k=((\\\\'|'[^']++')++|.*)/m", "$k='$v'", $content, -1, $count); + $content = preg_replace("/^$name=((\\\\'|'[^']++')++|.*)/m", "$name='$v'", $content, -1, $count); if (!$count) { - $content .= "$k='$v'\n"; + $content .= "$name='$v'\n"; } file_put_contents($this->dotenvFile, $content); @@ -55,8 +54,7 @@ class DotenvVault extends AbstractVault { $this->lastMessage = null; $this->validateName($name); - $k = $name.'_SECRET'; - $v = \is_string($_SERVER[$k] ?? null) ? $_SERVER[$k] : ($_ENV[$k] ?? null); + $v = \is_string($_SERVER[$name] ?? null) ? $_SERVER[$name] : ($_ENV[$name] ?? null); if (null === $v) { $this->lastMessage = sprintf('Secret "%s" not found in "%s".', $name, $this->getPrettyPath($this->dotenvFile)); @@ -71,10 +69,9 @@ class DotenvVault extends AbstractVault { $this->lastMessage = null; $this->validateName($name); - $k = $name.'_SECRET'; $content = file_exists($this->dotenvFile) ? file_get_contents($this->dotenvFile) : ''; - $content = preg_replace("/^$k=((\\\\'|'[^']++')++|.*)\n?/m", '', $content, -1, $count); + $content = preg_replace("/^$name=((\\\\'|'[^']++')++|.*)\n?/m", '', $content, -1, $count); if ($count) { file_put_contents($this->dotenvFile, $content); @@ -94,14 +91,14 @@ class DotenvVault extends AbstractVault $secrets = []; foreach ($_ENV as $k => $v) { - if (preg_match('/^(\w+)_SECRET$/D', $k, $m)) { - $secrets[$m[1]] = $reveal ? $v : null; + if (preg_match('/^\w+$/D', $k)) { + $secrets[$k] = $reveal ? $v : null; } } foreach ($_SERVER as $k => $v) { - if (\is_string($v) && preg_match('/^(\w+)_SECRET$/D', $k, $m)) { - $secrets[$m[1]] = $reveal ? $v : null; + if (\is_string($v) && preg_match('/^\w+$/D', $k)) { + $secrets[$k] = $reveal ? $v : null; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Secrets/SecretEnvVarProcessor.php b/src/Symfony/Bundle/FrameworkBundle/Secrets/SecretEnvVarProcessor.php index 5a4771fa36..e3a15febe4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Secrets/SecretEnvVarProcessor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Secrets/SecretEnvVarProcessor.php @@ -44,9 +44,9 @@ class SecretEnvVarProcessor implements EnvVarProcessorInterface /** * {@inheritdoc} */ - public function getEnv($prefix, $name, \Closure $getEnv) + public function getEnv($prefix, $name, \Closure $getEnv): string { - if (null !== $this->localVault && null !== $secret = $this->localVault->reveal($name)) { + if (null !== $this->localVault && null !== ($secret = $this->localVault->reveal($name)) && \array_key_exists($name, $this->vault->list())) { return $secret; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php b/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php index 8aae669d8f..17328b2a6d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php +++ b/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php @@ -87,6 +87,12 @@ class SodiumVault extends AbstractVault $this->validateName($name); $this->loadKeys(); $this->export($name.'.'.substr_replace(md5($name), '.sodium', -26), sodium_crypto_box_seal($value, $this->encryptionKey ?? sodium_crypto_box_publickey($this->decryptionKey))); + + $list = $this->list(); + $list[$name] = null; + uksort($list, 'strnatcmp'); + file_put_contents($this->pathPrefix.'sodium.list', sprintf("lastMessage = sprintf('Secret "%s" encrypted in "%s"; you can commit it.', $name, $this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR)); } @@ -123,6 +129,10 @@ class SodiumVault extends AbstractVault return false; } + $list = $this->list(); + unset($list[$name]); + file_put_contents($this->pathPrefix.'sodium.list', sprintf("lastMessage = sprintf('Secret "%s" removed from "%s".', $name, $this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR)); return @unlink($file) || !file_exists($file); @@ -131,13 +141,19 @@ class SodiumVault extends AbstractVault public function list(bool $reveal = false): array { $this->lastMessage = null; - $secrets = []; - $regexp = sprintf('{^%s(\w++)\.[0-9a-f]{6}\.sodium$}D', preg_quote(basename($this->pathPrefix))); - foreach (scandir(\dirname($this->pathPrefix)) as $name) { - if (preg_match($regexp, $name, $m)) { - $secrets[$m[1]] = $reveal ? $this->reveal($m[1]) : null; - } + if (!file_exists($file = $this->pathPrefix.'sodium.list')) { + return []; + } + + $secrets = include $file; + + if (!$reveal) { + return $secrets; + } + + foreach ($secrets as $name => $value) { + $secrets[$name] = $this->reveal($name); } return $secrets; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/DotenvVaultTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/DotenvVaultTest.php index ba234349d7..d494c82e68 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/DotenvVaultTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/DotenvVaultTest.php @@ -37,21 +37,21 @@ class DotenvVaultTest extends TestCase $vault->seal('foo', $plain); - unset($_SERVER['foo_SECRET'], $_ENV['foo_SECRET']); + unset($_SERVER['foo'], $_ENV['foo']); (new Dotenv(false))->load($this->envFile); $decrypted = $vault->reveal('foo'); $this->assertSame($plain, $decrypted); - $this->assertSame(['foo' => null], $vault->list()); - $this->assertSame(['foo' => $plain], $vault->list(true)); + $this->assertSame(['foo' => null], array_intersect_key($vault->list(), ['foo' => 123])); + $this->assertSame(['foo' => $plain], array_intersect_key($vault->list(true), ['foo' => 123])); $this->assertTrue($vault->remove('foo')); $this->assertFalse($vault->remove('foo')); - unset($_SERVER['foo_SECRET'], $_ENV['foo_SECRET']); + unset($_SERVER['foo'], $_ENV['foo']); (new Dotenv(false))->load($this->envFile); - $this->assertSame([], $vault->list()); + $this->assertArrayNotHasKey('foo', $vault->list()); } }