diff --git a/.env b/.env index 8b49c8d..9f098d4 100644 --- a/.env +++ b/.env @@ -7,6 +7,7 @@ OPR_EXECUTOR_MAINTENANCE_INTERVAL=60 OPR_EXECUTOR_NETWORK=executor_runtimes OPR_EXECUTOR_IMAGE=executor-local OPR_EXECUTOR_SECRET=executor-secret-key +OPR_EXECUTOR_SECRET_PREVIOUS=executor-secret-key-previous OPR_EXECUTOR_DOCKER_HUB_USERNAME= OPR_EXECUTOR_DOCKER_HUB_PASSWORD= OPR_EXECUTOR_RUNTIME_VERSIONS=v2,v5 diff --git a/README.md b/README.md index 5beb066..b194fc7 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,7 @@ docker compose down | OPR_EXECUTOR_MAINTENANCE_INTERVAL| Interval (in seconds) at which the Executor performs maintenance tasks, ex: `60` | | OPR_EXECUTOR_NETWORK | Network used by the executor for runtimes, ex: `openruntimes-runtimes` | | OPR_EXECUTOR_SECRET | Secret key used by the executor for authentication | +| OPR_EXECUTOR_SECRET_PREVIOUS | Optional second key the executor also accepts, so callers can move to a new `OPR_EXECUTOR_SECRET` without downtime | | OPR_EXECUTOR_DOCKER_HUB_USERNAME | Username for Docker Hub authentication (if applicable) | | OPR_EXECUTOR_DOCKER_HUB_PASSWORD | Password for Docker Hub authentication (if applicable) | | OPR_EXECUTOR_RUNTIME_VERSIONS | Version tag for runtime environments, ex: `v5` | diff --git a/app/controllers.php b/app/controllers.php index cb7e5e5..52e5f54 100644 --- a/app/controllers.php +++ b/app/controllers.php @@ -295,7 +295,14 @@ function ( ->inject('request') ->action(function (Request $request): void { $secretKey = \explode(' ', $request->getHeaderLine('authorization'))[1] ?? ''; - if ($secretKey === '' || $secretKey === '0' || $secretKey !== System::getEnv('OPR_EXECUTOR_SECRET', '')) { + + // OPR_EXECUTOR_SECRET_PREVIOUS keeps the old secret valid while callers move to a new one. + $accepted = \array_filter( + [System::getEnv('OPR_EXECUTOR_SECRET', ''), System::getEnv('OPR_EXECUTOR_SECRET_PREVIOUS', '')], + fn (string $secret): bool => $secret !== '' && $secret !== '0', + ); + + if ($secretKey === '' || $secretKey === '0' || \array_filter($accepted, fn (string $secret): bool => \hash_equals($secret, $secretKey)) === []) { throw new Exception(Exception::GENERAL_UNAUTHORIZED, 'Missing executor key'); } }); diff --git a/docker-compose.yml b/docker-compose.yml index 656c95d..d4528db 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,6 +27,7 @@ services: - OPR_EXECUTOR_MAINTENANCE_INTERVAL - OPR_EXECUTOR_NETWORK - OPR_EXECUTOR_SECRET + - OPR_EXECUTOR_SECRET_PREVIOUS - OPR_EXECUTOR_DOCKER_HUB_USERNAME - OPR_EXECUTOR_DOCKER_HUB_PASSWORD - OPR_EXECUTOR_RUNTIME_VERSIONS diff --git a/tests/e2e/ExecutorTest.php b/tests/e2e/ExecutorTest.php index 17c6738..c8eac0e 100644 --- a/tests/e2e/ExecutorTest.php +++ b/tests/e2e/ExecutorTest.php @@ -250,6 +250,21 @@ public function testGetRuntimesUnauthorized(): void $this->client->setKey($this->key); } + public function testPreviousSecretAuthorizes(): void + { + // docker-compose.yml sets OPR_EXECUTOR_SECRET_PREVIOUS so a rotation can be exercised end to end. + $this->client->setKey('executor-secret-key-previous'); + $response = $this->client->call(Client::METHOD_GET, '/runtimes', [], []); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertIsArray($response['body']); + + $this->client->setKey('executor-secret-key-unknown'); + $response = $this->client->call(Client::METHOD_GET, '/runtimes', [], []); + $this->assertEquals(401, $response['headers']['status-code']); + + $this->client->setKey($this->key); + } + public function testCommandsUnauthorized(): void { $this->client->setKey('');