Skip to content

Commit 364d22f

Browse files
committed
📦 Move secret into a new component
1 parent aa51dc7 commit 364d22f

19 files changed

+1347
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/Tests export-ignore
2+
/phpunit.xml.dist export-ignore
3+
/.gitattributes export-ignore
4+
/.gitignore export-ignore
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/vendor/
2+
/phpunit.xml
3+
.phpunit.result.cache
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Symfony\Component\Secret;
15+
16+
/**
17+
* @author Nicolas Grekas <p@tchwork.com>
18+
*/
19+
abstract class AbstractVault
20+
{
21+
/** @var string|null */
22+
protected $lastMessage;
23+
24+
public function getLastMessage(): string
25+
{
26+
return $this->lastMessage ?? '';
27+
}
28+
29+
abstract public function generateKeys(bool $override = false): bool;
30+
31+
abstract public function seal(string $name, string $value): void;
32+
33+
abstract public function reveal(string $name): ?string;
34+
35+
abstract public function remove(string $name): bool;
36+
37+
abstract public function list(bool $reveal = false): array;
38+
39+
protected function validateName(string $name): void
40+
{
41+
if (!preg_match('/^\w++$/D', $name)) {
42+
throw new \LogicException(sprintf('Invalid secret name "%s": only "word" characters are allowed.', $name));
43+
}
44+
}
45+
46+
protected function getPrettyPath(string $path): string
47+
{
48+
return str_replace(getcwd().\DIRECTORY_SEPARATOR, '', $path);
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Symfony\Component\Secret\Command;
15+
16+
use Symfony\Component\Console\Command\Command;
17+
use Symfony\Component\Console\Input\InputInterface;
18+
use Symfony\Component\Console\Input\InputOption;
19+
use Symfony\Component\Console\Output\ConsoleOutputInterface;
20+
use Symfony\Component\Console\Output\OutputInterface;
21+
use Symfony\Component\Console\Style\SymfonyStyle;
22+
use Symfony\Component\Secret\AbstractVault;
23+
24+
/**
25+
* @author Nicolas Grekas <p@tchwork.com>
26+
*/
27+
final class SecretsDecryptToLocalCommand extends Command
28+
{
29+
protected static $defaultName = 'decrypt-to-local';
30+
31+
private $vault;
32+
private $localVault;
33+
34+
public function __construct(AbstractVault $vault, AbstractVault $localVault = null)
35+
{
36+
$this->vault = $vault;
37+
$this->localVault = $localVault;
38+
39+
parent::__construct();
40+
}
41+
42+
protected function configure(): void
43+
{
44+
$this
45+
->setDescription('Decrypt all secrets and stores them in the local vault')
46+
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force overriding of secrets that already exist in the local vault')
47+
->addOption('dir', 'd', InputOption::VALUE_REQUIRED, 'Target directory')
48+
->setHelp(<<<'EOF'
49+
The <info>%command.name%</info> command decrypts all secrets and copies them in the local vault.
50+
51+
<info>%command.full_name%</info>
52+
53+
When the option <info>--force</info> is provided, secrets that already exist in the local vault are overriden.
54+
55+
<info>%command.full_name% --force</info>
56+
EOF
57+
)
58+
;
59+
}
60+
61+
protected function execute(InputInterface $input, OutputInterface $output): int
62+
{
63+
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
64+
65+
if (null === $this->localVault) {
66+
$io->error('The local vault is disabled.');
67+
68+
return 1;
69+
}
70+
71+
$secrets = $this->vault->list(true);
72+
73+
$io->comment(sprintf('%d secret%s found in the vault.', \count($secrets), 1 !== \count($secrets) ? 's' : ''));
74+
75+
$skipped = 0;
76+
if (!$input->getOption('force')) {
77+
foreach ($this->localVault->list() as $k => $v) {
78+
if (isset($secrets[$k])) {
79+
++$skipped;
80+
unset($secrets[$k]);
81+
}
82+
}
83+
}
84+
85+
if ($skipped > 0) {
86+
$io->warning([
87+
sprintf('%d secret%s already overridden in the local vault and will be skipped.', $skipped, 1 !== $skipped ? 's are' : ' is'),
88+
'Use the --force flag to override these.',
89+
]);
90+
}
91+
92+
foreach ($secrets as $k => $v) {
93+
if (null === $v) {
94+
$io->error($this->vault->getLastMessage());
95+
96+
return 1;
97+
}
98+
99+
$this->localVault->seal($k, $v);
100+
$io->note($this->localVault->getLastMessage());
101+
}
102+
103+
return 0;
104+
}
105+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Symfony\Component\Secret\Command;
15+
16+
use Symfony\Component\Console\Command\Command;
17+
use Symfony\Component\Console\Input\InputInterface;
18+
use Symfony\Component\Console\Input\InputOption;
19+
use Symfony\Component\Console\Output\ConsoleOutputInterface;
20+
use Symfony\Component\Console\Output\OutputInterface;
21+
use Symfony\Component\Console\Style\SymfonyStyle;
22+
use Symfony\Component\Secret\AbstractVault;
23+
24+
/**
25+
* @author Nicolas Grekas <p@tchwork.com>
26+
*/
27+
final class SecretsEncryptFromLocalCommand extends Command
28+
{
29+
protected static $defaultName = 'encrypt-from-local';
30+
31+
private $vault;
32+
private $localVault;
33+
34+
public function __construct(AbstractVault $vault, AbstractVault $localVault = null)
35+
{
36+
$this->vault = $vault;
37+
$this->localVault = $localVault;
38+
39+
parent::__construct();
40+
}
41+
42+
protected function configure(): void
43+
{
44+
$this
45+
->setDescription('Encrypt all local secrets to the vault')
46+
->addOption('dir', 'd', InputOption::VALUE_REQUIRED, 'Target directory')
47+
->setHelp(<<<'EOF'
48+
The <info>%command.name%</info> command encrypts all locally overridden secrets to the vault.
49+
50+
<info>%command.full_name%</info>
51+
EOF
52+
)
53+
;
54+
}
55+
56+
protected function execute(InputInterface $input, OutputInterface $output): int
57+
{
58+
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
59+
60+
if (null === $this->localVault) {
61+
$io->error('The local vault is disabled.');
62+
63+
return 1;
64+
}
65+
66+
foreach ($this->vault->list(true) as $name => $value) {
67+
$localValue = $this->localVault->reveal($name);
68+
69+
if (null !== $localValue && $value !== $localValue) {
70+
$this->vault->seal($name, $localValue);
71+
} elseif (null !== $message = $this->localVault->getLastMessage()) {
72+
$io->error($message);
73+
74+
return 1;
75+
}
76+
}
77+
78+
return 0;
79+
}
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Symfony\Component\Secret\Command;
15+
16+
use Symfony\Component\Console\Command\Command;
17+
use Symfony\Component\Console\Input\InputInterface;
18+
use Symfony\Component\Console\Input\InputOption;
19+
use Symfony\Component\Console\Output\ConsoleOutputInterface;
20+
use Symfony\Component\Console\Output\OutputInterface;
21+
use Symfony\Component\Console\Style\SymfonyStyle;
22+
use Symfony\Component\Secret\AbstractVault;
23+
24+
/**
25+
* @author Tobias Schultze <http://tobion.de>
26+
* @author Jérémy Derussé <jeremy@derusse.com>
27+
* @author Nicolas Grekas <p@tchwork.com>
28+
*/
29+
final class SecretsGenerateKeysCommand extends Command
30+
{
31+
protected static $defaultName = 'generate-keys';
32+
33+
private $vault;
34+
private $localVault;
35+
36+
public function __construct(AbstractVault $vault, AbstractVault $localVault = null)
37+
{
38+
$this->vault = $vault;
39+
$this->localVault = $localVault;
40+
41+
parent::__construct();
42+
}
43+
44+
protected function configure(): void
45+
{
46+
$this
47+
->setDescription('Generate new encryption keys')
48+
->addOption('local', 'l', InputOption::VALUE_NONE, 'Update the local vault.')
49+
->addOption('rotate', 'r', InputOption::VALUE_NONE, 'Re-encrypt existing secrets with the newly generated keys.')
50+
->addOption('dir', 'd', InputOption::VALUE_REQUIRED, 'Target directory')
51+
->setHelp(<<<'EOF'
52+
The <info>%command.name%</info> command generates a new encryption key.
53+
54+
<info>%command.full_name%</info>
55+
56+
If encryption keys already exist, the command must be called with
57+
the <info>--rotate</info> option in order to override those keys and re-encrypt
58+
existing secrets.
59+
60+
<info>%command.full_name% --rotate</info>
61+
EOF
62+
)
63+
;
64+
}
65+
66+
protected function execute(InputInterface $input, OutputInterface $output): int
67+
{
68+
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
69+
$vault = $input->getOption('local') ? $this->localVault : $this->vault;
70+
71+
if (null === $vault) {
72+
$io->success('The local vault is disabled.');
73+
74+
return 1;
75+
}
76+
77+
if (!$input->getOption('rotate')) {
78+
if ($vault->generateKeys()) {
79+
$io->success($vault->getLastMessage());
80+
81+
if ($this->vault === $vault) {
82+
$io->caution('DO NOT COMMIT THE DECRYPTION KEY FOR THE PROD ENVIRONMENT⚠️');
83+
}
84+
85+
return 0;
86+
}
87+
88+
$io->warning($vault->getLastMessage());
89+
90+
return 1;
91+
}
92+
93+
$secrets = [];
94+
foreach ($vault->list(true) as $name => $value) {
95+
if (null === $value) {
96+
$io->error($vault->getLastMessage());
97+
98+
return 1;
99+
}
100+
101+
$secrets[$name] = $value;
102+
}
103+
104+
if (!$vault->generateKeys(true)) {
105+
$io->warning($vault->getLastMessage());
106+
107+
return 1;
108+
}
109+
110+
$io->success($vault->getLastMessage());
111+
112+
if ($secrets) {
113+
foreach ($secrets as $name => $value) {
114+
$vault->seal($name, $value);
115+
}
116+
117+
$io->comment('Existing secrets have been rotated to the new keys.');
118+
}
119+
120+
if ($this->vault === $vault) {
121+
$io->caution('DO NOT COMMIT THE DECRYPTION KEY FOR THE PROD ENVIRONMENT⚠️');
122+
}
123+
124+
return 0;
125+
}
126+
}

0 commit comments

Comments
 (0)