Files
coolify/app/Models/SecretManagerLink.php
T
Andras Bacsai 8d7469230c fix(secrets): skip remote secret names that are not valid variable keys
Import all stopped at the first remote secret whose name is not a valid
environment variable key (for example db-password in Vault or
Infisical). The key setter threw, so the import was partial, no audit
entry was written, the variable list did not refresh, and the user saw
only a generic error.

Invalid names are now skipped and listed in the result toast and the
audit entry, and the rest are imported. Browse marks invalid names and
offers no add action for them, and adding one shows a clear error. The
refreshEnvs event now goes only to the environment variable list.
2026-10-01 07:16:45 +02:00

139 lines
4.5 KiB
PHP

<?php
namespace App\Models;
use App\Services\DopplerService;
use App\Services\InfisicalService;
use App\Services\VaultService;
use App\Support\ValidationPatterns;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* Links a resource (currently Application) to a remote secret manager source.
* Holds only the source coordinates — secret values are never persisted.
*/
class SecretManagerLink extends BaseModel
{
protected $fillable = [
'resourceable_type',
'resourceable_id',
'integration_token_id',
'settings',
];
protected function casts(): array
{
return [
'settings' => 'array',
];
}
public function resourceable(): MorphTo
{
return $this->morphTo();
}
public function integrationToken(): BelongsTo
{
return $this->belongsTo(IntegrationToken::class);
}
/**
* Fetch the secrets from the remote manager. Values live only in memory.
*
* @return array<string, string>
*/
public function fetchSecrets(): array
{
$token = $this->integrationToken;
$teamId = $this->resourceable?->team()?->id;
if ($teamId === null || (int) $token->team_id !== (int) $teamId) {
throw new \RuntimeException('The secret manager token does not belong to the team of this resource.');
}
$settings = $this->settings ?? [];
$metadata = $token->metadata ?? [];
return match ($token->provider) {
'doppler' => (new DopplerService($token->token))->fetchSecrets(
data_get($settings, 'project'),
data_get($settings, 'config'),
),
'infisical' => (new InfisicalService(
data_get($metadata, 'base_url', 'https://app.infisical.com'),
(string) data_get($metadata, 'client_id'),
$token->token,
))->fetchSecrets(
(string) data_get($settings, 'project_id'),
(string) data_get($settings, 'environment'),
(string) data_get($settings, 'secret_path', '/'),
),
'vault' => (new VaultService(
(string) data_get($metadata, 'base_url'),
$token->token,
data_get($metadata, 'namespace'),
))->fetchSecrets(
(string) data_get($settings, 'mount', 'secret'),
(string) data_get($settings, 'path'),
),
default => throw new \RuntimeException("Unsupported secret manager provider [{$token->provider}]."),
};
}
/**
* Create one {{vault.KEY}} reference variable per remote key that has no
* variable with that key yet. Only key names touch the database. Remote
* names that are not valid variable keys (e.g. `db-password`) are skipped
* as they are, never renamed.
*
* @return array{imported: list<string>, skipped: list<string>}
*/
public function importMissingReferences(): array
{
$keys = array_keys($this->fetchSecrets());
sort($keys);
$existing = $this->resourceable->environment_variables()->pluck('key')->flip();
$imported = [];
$skipped = [];
foreach ($keys as $key) {
$key = (string) $key;
if (! ValidationPatterns::isValidEnvironmentVariableKey($key)) {
$skipped[] = $key;
continue;
}
if (isset($existing[$key])) {
continue;
}
$this->resourceable->environment_variables()->create([
'key' => $key,
'value' => '{{vault.'.$key.'}}',
]);
$imported[] = $key;
}
return ['imported' => $imported, 'skipped' => $skipped];
}
/** Short human-readable description of the remote source for the UI. */
public function sourceSummary(): string
{
$settings = $this->settings ?? [];
return match ($this->integrationToken->provider) {
'doppler' => trim(implode('/', array_filter([
data_get($settings, 'project'),
data_get($settings, 'config'),
])), '/') ?: 'token scope',
'infisical' => data_get($settings, 'project_id').'/'.data_get($settings, 'environment').data_get($settings, 'secret_path', '/'),
'vault' => data_get($settings, 'mount', 'secret').'/'.data_get($settings, 'path'),
default => '',
};
}
}