fix(infisical): include imported secrets when fetching secrets

Closes #12267

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-10-23 22:26:32 +02:00
co-authored by Claude Opus 5.5
parent f72041c513
commit 5131832cd8
2 changed files with 77 additions and 1 deletions
+16 -1
View File
@@ -46,6 +46,7 @@ class InfisicalService
'projectId' => $projectId,
'environment' => $environment,
'secretPath' => $secretPath,
'includeImports' => 'true',
...$valueOptions,
]);
@@ -55,6 +56,7 @@ class InfisicalService
'workspaceId' => $projectId,
'environment' => $environment,
'secretPath' => $secretPath,
'include_imports' => 'true',
...$valueOptions,
]);
}
@@ -63,7 +65,20 @@ class InfisicalService
throw new \RuntimeException('Infisical API error: '.($response->json('message') ?? 'HTTP '.$response->status()));
}
return collect($response->json('secrets', []))
// Like `infisical export`: later imports override earlier ones, and the folder's own secrets override all imports.
return collect($response->json('imports', []))
->pluck('secrets')
->push($response->json('secrets', []))
->reduce(fn (array $merged, ?array $secrets) => array_replace($merged, $this->keyValues($secrets ?? [])), []);
}
/**
* @param array<int, array<string, mixed>> $secrets
* @return array<string, string>
*/
private function keyValues(array $secrets): array
{
return collect($secrets)
->mapWithKeys(fn ($secret) => [(string) data_get($secret, 'secretKey') => (string) data_get($secret, 'secretValue', '')])
->all();
}
@@ -125,6 +125,67 @@ describe('InfisicalService', function () {
&& $request['expandSecretReferences'] === 'true');
});
test('merges imported secrets, with own secrets and later imports taking precedence', function () {
Http::fake([
'https://example.com/infisical/api/v1/auth/universal-auth/login' => Http::response([
'accessToken' => 'short-lived-token',
]),
'https://example.com/infisical/api/v4/secrets*' => Http::response([
'secrets' => [
['secretKey' => 'APP_KEY', 'secretValue' => 'app'],
['secretKey' => 'OVERRIDDEN', 'secretValue' => 'own'],
],
'imports' => [
['secretPath' => '/shared', 'environment' => 'prod', 'secrets' => [
['secretKey' => 'SHARED_KEY', 'secretValue' => 'shared'],
['secretKey' => 'OVERRIDDEN', 'secretValue' => 'from-shared'],
['secretKey' => 'IMPORT_CONFLICT', 'secretValue' => 'first-import'],
]],
['secretPath' => '/common', 'environment' => 'prod', 'secrets' => [
['secretKey' => 'IMPORT_CONFLICT', 'secretValue' => 'last-import'],
]],
],
]),
]);
$service = new InfisicalService('https://example.com/infisical', 'client-id', 'client-secret');
expect($service->fetchSecrets('project-1', 'prod', '/apps/api'))->toBe([
'SHARED_KEY' => 'shared',
'OVERRIDDEN' => 'own',
'IMPORT_CONFLICT' => 'last-import',
'APP_KEY' => 'app',
]);
Http::assertSent(fn ($request) => str_contains($request->url(), '/api/v4/secrets')
&& $request['includeImports'] === 'true');
});
test('requests imported secrets from the v3 raw endpoint', function () {
Http::fake([
'https://example.com/infisical/api/v1/auth/universal-auth/login' => Http::response([
'accessToken' => 'short-lived-token',
]),
'https://example.com/infisical/api/v4/secrets*' => Http::response([], 404),
'https://example.com/infisical/api/v3/secrets/raw*' => Http::response([
'secrets' => [['secretKey' => 'APP_KEY', 'secretValue' => 'app']],
'imports' => [['secretPath' => '/shared', 'secrets' => [
['secretKey' => 'SHARED_KEY', 'secretValue' => 'shared'],
]]],
]),
]);
$service = new InfisicalService('https://example.com/infisical', 'client-id', 'client-secret');
expect($service->fetchSecrets('project-1', 'prod', '/apps/api'))->toBe([
'SHARED_KEY' => 'shared',
'APP_KEY' => 'app',
]);
Http::assertSent(fn ($request) => str_contains($request->url(), '/api/v3/secrets/raw')
&& $request['include_imports'] === '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) {