fix(secret-manager): add configurable request timeout for Infisical and Vault

This commit is contained in:
Andras Bacsai
2026-10-20 13:00:38 +02:00
parent 2ade696375
commit 87dade4517
13 changed files with 103 additions and 6 deletions
@@ -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;
+10
View File
@@ -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') {
+2
View File
@@ -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'),
+3 -3
View File
@@ -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,
};
+3 -3
View File
@@ -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']);