diff --git a/.gitignore b/.gitignore index 73d8e98..d350f0b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ /composer.phar /composer.lock /.phpunit.result.cache +.env +.env* +/tests/env/ +.idea \ No newline at end of file diff --git a/composer.json b/composer.json index fac18e5..517fc5d 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,8 @@ }, "require": { "php": ">=8.0", - "async-aws/ssm": "^1.3" + "async-aws/ssm": "^1.3", + "vlucas/phpdotenv": "^5.6" }, "require-dev": { "phpunit/phpunit": "^9.6.10", diff --git a/src/Secrets.php b/src/Secrets.php index 639e815..d4abd93 100644 --- a/src/Secrets.php +++ b/src/Secrets.php @@ -4,6 +4,7 @@ use AsyncAws\Ssm\SsmClient; use Closure; +use Dotenv\Dotenv; use JsonException; use RuntimeException; @@ -17,11 +18,7 @@ class Secrets */ public static function loadSecretEnvironmentVariables(?SsmClient $ssmClient = null): void { - /** @var array|string|false $envVars */ - $envVars = getenv(local_only: true); // @phpstan-ignore-line PHPStan is wrong - if (! is_array($envVars)) { - return; - } + $envVars = self::getEnvVars(); // Only consider environment variables that start with "bref-ssm:" $envVarsToDecrypt = array_filter($envVars, function (string $value): bool { @@ -130,4 +127,38 @@ private static function retrieveParametersFromSsm(?SsmClient $ssmClient, array $ return $parameters; } + + private static function getEnvironment(): ?string + { + if ($environment = getenv('BREF_ENV')){ + return $environment; + } + return getenv('APP_ENV') ?: null; + } + + private static function getEnvironmentPath(): ?string + { + if ($environment = getenv('BREF_ENV_PATH')){ + return $environment; + } + return getenv('LAMBDA_TASK_ROOT') ?: getcwd(); + } + + private static function getEnvFile(): string + { + $env = self::getEnvironment(); + $envFilePath = self::getEnvironmentPath()."/.env.{$env}"; + return $env && file_exists($envFilePath) ? ".env.{$env}" : '.env'; + } + + private static function getEnvVars(): array + { + $env = getenv(null, true); + return array_merge( + is_array($env) ? $env : [], + Dotenv::createUnsafeImmutable( + self::getEnvironmentPath(), + self::getEnvFile() + )->safeLoad()); + } } diff --git a/tests/SecretsTest.php b/tests/SecretsTest.php index 6792ace..aefdedf 100644 --- a/tests/SecretsTest.php +++ b/tests/SecretsTest.php @@ -11,11 +11,61 @@ class SecretsTest extends TestCase { + + const envToReset = [ + 'SOME_VARIABLE', + 'SOME_OTHER_VARIABLE', + 'APP_ENV', + 'BREF_ENV', + 'BREF_ENV_PATH', + 'LAMBDA_TASK_ROOT' + ]; + + const envFilesToCleanup = [ + '.env', + '.env.foobar' + ]; + + public static function tearDownAfterClass(): void + { + self::cleanupFiles(); + parent::tearDownAfterClass(); + + } + public function setUp(): void + { + + self::cleanupFiles(); + + // reset env + array_map(function ($env) { + putenv($env); + unset($_ENV[$env], $_SERVER[$env]); + }, self::envToReset); + } + + protected static function cleanupFiles() { if (file_exists(sys_get_temp_dir() . '/bref-ssm-parameters.php')) { unlink(sys_get_temp_dir() . '/bref-ssm-parameters.php'); } + + // cleanup dotenv files + array_map(function ($envFile){ + + if (file_exists(getcwd() .'/'. $envFile)) { + unlink(getcwd() .'/'. $envFile); + } + + if (file_exists(__DIR__ . '/env/' . $envFile)) { + unlink(__DIR__ . '/env/' . $envFile); + } + }, self::envFilesToCleanup); + + if(file_exists(__DIR__ . '/env')) { + rmdir(__DIR__ . '/env'); + } } public function test decrypts env variables(): void @@ -64,6 +114,80 @@ public function test throws a clear error message on missing permissions Secrets::loadSecretEnvironmentVariables($ssmClient); } + /** + * @testWith [null, null] + * ["BREF_ENV_PATH", null] + * ["LAMBDA_TASK_ROOT", null] + * [null, "BREF_ENV"] + * [null, "APP_ENV"] + * ["BREF_ENV_PATH", "BREF_ENV"] + * ["LAMBDA_TASK_ROOT", "APP_ENV"] + */ + public function testLoadsSecretsFromDotenv(?string $envPath, ?string $envKey): void + { + + if ($envPath) { + putenv("$envPath=" . __DIR__ . '/env'); + mkdir( __DIR__ . '/env'); + } + $envPath = $envPath ? __DIR__ . '/env' : getcwd(); + copy(__DIR__ . '/fixtures/.env', "$envPath/.env"); + + if($envKey) { + putenv("$envKey=foobar"); + } + + putenv('SOME_VARIABLE'); + putenv('SOME_OTHER_VARIABLE=helloworld'); + + // Sanity checks + $this->assertFalse(getenv('SOME_VARIABLE')); + $this->assertSame('helloworld', getenv('SOME_OTHER_VARIABLE')); + + Secrets::loadSecretEnvironmentVariables($this->mockSsmClient()); + + $this->assertSame('foobar', getenv('SOME_VARIABLE')); + $this->assertSame('foobar', $_SERVER['SOME_VARIABLE']); + $this->assertSame('foobar', $_ENV['SOME_VARIABLE']); + // Check that the other variable was not modified + $this->assertSame('helloworld', getenv('SOME_OTHER_VARIABLE')); + } + + /** + * @testWith [null, "BREF_ENV"] + * [null, "APP_ENV"] + * ["BREF_ENV_PATH", "BREF_ENV"] + * ["BREF_ENV_PATH", "APP_ENV"] + * ["LAMBDA_TASK_ROOT", "BREF_ENV"] + * ["LAMBDA_TASK_ROOT", "APP_ENV"] + */ + public function testLoadsSecretsFromDotenvForSpecificEnv(?string $envPath, string $envKey): void + { + if ($envPath) { + putenv("$envPath=" . __DIR__ . '/env'); + mkdir( __DIR__ . '/env'); + } + + $envPath = $envPath ? __DIR__ . '/env' : getcwd(); + copy(__DIR__ . '/fixtures/.env', "$envPath/.env.foobar"); + putenv('SOME_VARIABLE'); + putenv("$envKey=foobar"); + putenv('SOME_OTHER_VARIABLE=helloworld'); + + // Sanity checks + $this->assertFalse(getenv('SOME_VARIABLE')); + $this->assertSame('foobar', getenv($envKey)); + $this->assertSame('helloworld', getenv('SOME_OTHER_VARIABLE')); + + Secrets::loadSecretEnvironmentVariables($this->mockSsmClient()); + + $this->assertSame('foobar', getenv('SOME_VARIABLE')); + $this->assertSame('foobar', $_SERVER['SOME_VARIABLE']); + $this->assertSame('foobar', $_ENV['SOME_VARIABLE']); + // Check that the other variable was not modified + $this->assertSame('helloworld', getenv('SOME_OTHER_VARIABLE')); + } + private function mockSsmClient(): SsmClient { $ssmClient = $this->getMockBuilder(SsmClient::class) diff --git a/tests/fixtures/.env b/tests/fixtures/.env new file mode 100644 index 0000000..a4200e8 --- /dev/null +++ b/tests/fixtures/.env @@ -0,0 +1,2 @@ +SOME_VARIABLE=bref-ssm:/some/parameter +SOME_OTHER_VARIABLE=i-should-not-overwrite-existing-value \ No newline at end of file