Ctrl K
ComunidadGuías…

app/Models/SecretManagerLink.php

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
<?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,
                IntegrationToken::requestTimeout($metadata),
            ))->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'),
                IntegrationToken::requestTimeout($metadata),
            ))->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 => '',
        };
    }
}