mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(secret-manager): add configurable request timeout for Infisical and Vault
This commit is contained in:
@@ -64,9 +64,11 @@ class IntegrationTokensController extends Controller
|
||||
} elseif (($body['provider'] ?? null) === 'infisical') {
|
||||
$rules['metadata.base_url'] = ['required', 'url:http,https'];
|
||||
$rules['metadata.client_id'] = ['required', 'string'];
|
||||
$rules['metadata.timeout'] = ['nullable', 'integer', 'min:1', 'max:300'];
|
||||
} elseif (($body['provider'] ?? null) === 'vault') {
|
||||
$rules['metadata.base_url'] = ['required', 'url:http,https'];
|
||||
$rules['metadata.namespace'] = ['nullable', 'string'];
|
||||
$rules['metadata.timeout'] = ['nullable', 'integer', 'min:1', 'max:300'];
|
||||
}
|
||||
|
||||
$validator = customApiValidator($body, $rules);
|
||||
|
||||
@@ -64,11 +64,13 @@ class IntegrationTokenEditor extends Component
|
||||
if ($this->integrationToken->provider === 'infisical') {
|
||||
$rules['metadata.base_url'] = ['required', 'url:http,https'];
|
||||
$rules['metadata.client_id'] = ['required', 'string'];
|
||||
$rules['metadata.timeout'] = ['nullable', 'integer', 'min:1', 'max:300'];
|
||||
}
|
||||
|
||||
if ($this->integrationToken->provider === 'vault') {
|
||||
$rules['metadata.base_url'] = ['required', 'url:http,https'];
|
||||
$rules['metadata.namespace'] = ['nullable', 'string'];
|
||||
$rules['metadata.timeout'] = ['nullable', 'integer', 'min:1', 'max:300'];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
|
||||
@@ -66,6 +66,7 @@ class IntegrationTokenForm extends Component
|
||||
if ($this->provider === 'infisical') {
|
||||
$rules['metadata.base_url'] = ['required', 'url:http,https'];
|
||||
$rules['metadata.client_id'] = ['required', 'string'];
|
||||
$rules['metadata.timeout'] = ['nullable', 'integer', 'min:1', 'max:300'];
|
||||
}
|
||||
|
||||
if ($this->provider === 'doppler') {
|
||||
@@ -75,6 +76,7 @@ class IntegrationTokenForm extends Component
|
||||
if ($this->provider === 'vault') {
|
||||
$rules['metadata.base_url'] = ['required', 'url:http,https'];
|
||||
$rules['metadata.namespace'] = ['nullable', 'string'];
|
||||
$rules['metadata.timeout'] = ['nullable', 'integer', 'min:1', 'max:300'];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
|
||||
@@ -82,6 +82,16 @@ class IntegrationToken extends BaseModel
|
||||
return $this->provider === 'cloudflare' && data_get($this->metadata, 'proxied_dns') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional request timeout in seconds for self-hosted secret managers.
|
||||
*
|
||||
* @param array<string, mixed> $metadata
|
||||
*/
|
||||
public static function requestTimeout(array $metadata): ?int
|
||||
{
|
||||
return filled($metadata['timeout'] ?? null) ? (int) $metadata['timeout'] : null;
|
||||
}
|
||||
|
||||
public function dopplerTokenType(): ?string
|
||||
{
|
||||
if ($this->provider !== 'doppler') {
|
||||
|
||||
@@ -63,6 +63,7 @@ class SecretManagerLink extends BaseModel
|
||||
data_get($metadata, 'base_url', 'https://app.infisical.com'),
|
||||
(string) data_get($metadata, 'client_id'),
|
||||
$token->token,
|
||||
IntegrationToken::requestTimeout($metadata),
|
||||
))->fetchSecrets(
|
||||
(string) data_get($settings, 'project_id'),
|
||||
(string) data_get($settings, 'environment'),
|
||||
@@ -72,6 +73,7 @@ class SecretManagerLink extends BaseModel
|
||||
(string) data_get($metadata, 'base_url'),
|
||||
$token->token,
|
||||
data_get($metadata, 'namespace'),
|
||||
IntegrationToken::requestTimeout($metadata),
|
||||
))->fetchSecrets(
|
||||
(string) data_get($settings, 'mount', 'secret'),
|
||||
(string) data_get($settings, 'path'),
|
||||
|
||||
@@ -14,7 +14,7 @@ class InfisicalService
|
||||
/** @var array<string, mixed> */
|
||||
private array $httpClientOptions;
|
||||
|
||||
public function __construct(string $baseUrl, private string $clientId, private string $clientSecret)
|
||||
public function __construct(string $baseUrl, private string $clientId, private string $clientSecret, private ?int $timeout = null)
|
||||
{
|
||||
$this->baseUrl = rtrim($baseUrl, '/');
|
||||
Validator::make(['base_url' => $this->baseUrl], ['base_url' => new SafeExternalUrl])->validate();
|
||||
@@ -87,7 +87,7 @@ class InfisicalService
|
||||
{
|
||||
return Http::acceptJson()
|
||||
->withOptions($this->httpClientOptions)
|
||||
->connectTimeout(5)
|
||||
->timeout(10);
|
||||
->connectTimeout($this->timeout ?? 5)
|
||||
->timeout($this->timeout ?? 10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\IntegrationToken;
|
||||
|
||||
/**
|
||||
* Validates an integration token against its provider API before it is saved.
|
||||
*/
|
||||
@@ -16,11 +18,13 @@ class IntegrationTokenValidator
|
||||
(string) data_get($metadata, 'base_url', 'https://app.infisical.com'),
|
||||
(string) data_get($metadata, 'client_id'),
|
||||
$token,
|
||||
IntegrationToken::requestTimeout($metadata),
|
||||
))->validate(),
|
||||
'vault' => (new VaultService(
|
||||
(string) data_get($metadata, 'base_url'),
|
||||
$token,
|
||||
data_get($metadata, 'namespace'),
|
||||
IntegrationToken::requestTimeout($metadata),
|
||||
))->validate(),
|
||||
default => false,
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ class VaultService
|
||||
/** @var array<string, mixed> */
|
||||
private array $httpClientOptions;
|
||||
|
||||
public function __construct(string $baseUrl, private string $token, private ?string $namespace = null)
|
||||
public function __construct(string $baseUrl, private string $token, private ?string $namespace = null, private ?int $timeout = null)
|
||||
{
|
||||
$this->baseUrl = rtrim($baseUrl, '/');
|
||||
Validator::make(['base_url' => $this->baseUrl], ['base_url' => new SafeExternalUrl])->validate();
|
||||
@@ -56,8 +56,8 @@ class VaultService
|
||||
$client = Http::withHeaders(['X-Vault-Token' => $this->token])
|
||||
->acceptJson()
|
||||
->withOptions($this->httpClientOptions)
|
||||
->connectTimeout(5)
|
||||
->timeout(10);
|
||||
->connectTimeout($this->timeout ?? 5)
|
||||
->timeout($this->timeout ?? 10);
|
||||
|
||||
if (filled($this->namespace)) {
|
||||
$client = $client->withHeaders(['X-Vault-Namespace' => $this->namespace]);
|
||||
|
||||
@@ -20,12 +20,16 @@
|
||||
<x-forms.input required id="metadata.base_url" label="Base URL"
|
||||
placeholder="https://app.infisical.com" />
|
||||
<x-forms.input required id="metadata.client_id" label="Client ID" />
|
||||
<x-forms.input type="number" min="1" max="300" id="metadata.timeout" label="Request timeout (optional)"
|
||||
placeholder="10" helper="Seconds to wait for each request. Increase it if your instance responds slowly." />
|
||||
</div>
|
||||
@elseif ($integrationToken->provider === 'vault')
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<x-forms.input required id="metadata.base_url" label="Base URL"
|
||||
placeholder="https://vault.example.com:8200" />
|
||||
<x-forms.input id="metadata.namespace" label="Namespace (optional)" />
|
||||
<x-forms.input type="number" min="1" max="300" id="metadata.timeout" label="Request timeout (optional)"
|
||||
placeholder="10" helper="Seconds to wait for each request. Increase it if your instance responds slowly." />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
<x-forms.input required id="metadata.base_url" label="Base URL"
|
||||
placeholder="https://app.infisical.com"
|
||||
helper="Use the URL of your self-hosted Infisical instance, or the Infisical cloud URL." />
|
||||
<x-forms.input type="number" min="1" max="300" id="metadata.timeout" label="Request timeout (optional)"
|
||||
placeholder="10" helper="Seconds to wait for each request. Increase it if your instance responds slowly." />
|
||||
</div>
|
||||
@else
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
@@ -34,6 +36,8 @@
|
||||
placeholder="https://vault.example.com:8200" />
|
||||
<x-forms.input id="metadata.namespace" label="Namespace (optional)"
|
||||
placeholder="admin/team-a" helper="Only for Vault Enterprise / HCP Vault." />
|
||||
<x-forms.input type="number" min="1" max="300" id="metadata.timeout" label="Request timeout (optional)"
|
||||
placeholder="10" helper="Seconds to wait for each request. Increase it if your instance responds slowly." />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
@@ -187,6 +187,25 @@ test('a vault link uses the base url and namespace from the token metadata', fun
|
||||
Http::assertSent(fn ($request) => $request->hasHeader('X-Vault-Namespace', 'team-a'));
|
||||
});
|
||||
|
||||
test('an infisical link uses the request timeout from the token metadata', function () {
|
||||
$timeouts = [];
|
||||
Http::fake(function ($request, array $options) use (&$timeouts) {
|
||||
$timeouts[] = [$options['connect_timeout'], $options['timeout']];
|
||||
|
||||
return str_contains($request->url(), '/auth/universal-auth/login')
|
||||
? Http::response(['accessToken' => 'short-lived-token'])
|
||||
: Http::response(['secrets' => [['secretKey' => 'KEY', 'secretValue' => 'value']]]);
|
||||
});
|
||||
|
||||
$link = createSecretManagerLink('infisical',
|
||||
['project_id' => 'project-1', 'environment' => 'prod'],
|
||||
['base_url' => 'https://example.com', 'client_id' => 'client-id', 'timeout' => '30'],
|
||||
);
|
||||
|
||||
expect($link->fetchSecrets())->toBe(['KEY' => 'value'])
|
||||
->and($timeouts)->toBe([[30, 30], [30, 30]]);
|
||||
});
|
||||
|
||||
test('services resolve environment variables from their configured secret manager', function () {
|
||||
Http::fake([
|
||||
'https://api.doppler.com/v3/configs/config/secrets/download*' => Http::response([
|
||||
|
||||
@@ -125,6 +125,21 @@ describe('InfisicalService', function () {
|
||||
&& $request['expandSecretReferences'] === 'true');
|
||||
});
|
||||
|
||||
test('uses a 5 second connect timeout and a 10 second request timeout by default', function (?int $timeout, array $expected) {
|
||||
$timeouts = [];
|
||||
Http::fake(function ($request, array $options) use (&$timeouts) {
|
||||
$timeouts[] = [$options['connect_timeout'], $options['timeout']];
|
||||
|
||||
return Http::response(['accessToken' => 'short-lived-token']);
|
||||
});
|
||||
|
||||
expect((new InfisicalService('https://example.com', 'client-id', 'client-secret', $timeout))->validate())->toBeTrue()
|
||||
->and($timeouts)->toBe([$expected]);
|
||||
})->with([
|
||||
'default' => [null, [5, 10]],
|
||||
'custom' => [60, [60, 60]],
|
||||
]);
|
||||
|
||||
test('throws when the login fails', function () {
|
||||
Http::fake([
|
||||
'https://example.com/infisical/api/v1/auth/universal-auth/login' => Http::response([
|
||||
|
||||
@@ -469,3 +469,36 @@ test('saving a vault token with unchanged connection settings and a blank token
|
||||
->and($savedToken->token)->toBe('hvs.stored-secret')
|
||||
->and($savedToken->metadata)->toBe(['base_url' => 'https://example.com:8200', 'namespace' => 'team-a']);
|
||||
});
|
||||
|
||||
test('changing the vault request timeout keeps the stored token and validates with the new timeout', function () {
|
||||
$timeouts = [];
|
||||
Http::fake(function ($request, array $options) use (&$timeouts) {
|
||||
$timeouts[] = $options['timeout'];
|
||||
|
||||
return Http::response(['data' => []]);
|
||||
});
|
||||
$savedToken = vaultIntegrationToken($this->team);
|
||||
|
||||
Livewire::test(IntegrationTokenEditor::class, ['integration_token_uuid' => $savedToken->uuid])
|
||||
->set('metadata.timeout', '45')
|
||||
->call('save')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('success');
|
||||
|
||||
$savedToken->refresh();
|
||||
expect($timeouts)->toBe([45])
|
||||
->and($savedToken->token)->toBe('hvs.stored-secret')
|
||||
->and(IntegrationToken::requestTimeout($savedToken->metadata))->toBe(45);
|
||||
});
|
||||
|
||||
test('a request timeout outside 1 to 300 seconds is rejected', function (string $timeout) {
|
||||
Http::fake();
|
||||
$savedToken = vaultIntegrationToken($this->team);
|
||||
|
||||
Livewire::test(IntegrationTokenEditor::class, ['integration_token_uuid' => $savedToken->uuid])
|
||||
->set('metadata.timeout', $timeout)
|
||||
->call('save')
|
||||
->assertHasErrors(['metadata.timeout']);
|
||||
|
||||
Http::assertNothingSent();
|
||||
})->with(['0', '301', 'abc']);
|
||||
|
||||
Reference in New Issue
Block a user