mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(api): hide locked values in resource and environment lists
Closes #12391 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
72c8209285
commit
4b541011c9
@@ -154,12 +154,7 @@ class ApplicationsController extends Controller
|
||||
$application->settings?->makeHidden(['id', 'application_id', 'created_at', 'updated_at']);
|
||||
}
|
||||
|
||||
$payload = serializeApiResponse($application);
|
||||
if ($application instanceof Application && filled(data_get($payload, 'docker_compose'))) {
|
||||
$payload['docker_compose'] = redactLockedValuesInGeneratedCompose($payload['docker_compose'], $application);
|
||||
}
|
||||
|
||||
return $payload;
|
||||
return redactLockedApiResourceValues($application, serializeApiResponse($application));
|
||||
}
|
||||
|
||||
private function applicationSettingsFromRequest(Request $request): array
|
||||
|
||||
@@ -258,19 +258,7 @@ class DatabasesController extends Controller
|
||||
$this->hideNestedServerSecrets($database, $loadNestedServerSecrets);
|
||||
}
|
||||
|
||||
$payload = serializeApiResponse($database);
|
||||
|
||||
// A locked password variable is never shown again, also not inside the connection URLs.
|
||||
if (request()->attributes->get('can_read_sensitive', false) === true
|
||||
&& method_exists($database, 'isCredentialLocked') && $database->isCredentialLocked()) {
|
||||
foreach (['internal_db_url', 'external_db_url'] as $key) {
|
||||
if ($payload->has($key)) {
|
||||
$payload->put($key, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $payload;
|
||||
return redactLockedApiResourceValues($database, serializeApiResponse($database));
|
||||
}
|
||||
|
||||
private function hideNestedServerSecrets(Model $model, bool $loadRelations = false): void
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Models\Project;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
@@ -168,13 +169,16 @@ class ProjectController extends Controller
|
||||
return response()->json(['message' => 'Environment not found.'], 404);
|
||||
}
|
||||
// Redis connection URLs read REDIS_PASSWORD and REDIS_USERNAME; load the variables once, not per database.
|
||||
$environment = $environment->load(['applications', 'postgresqls', 'redis.runtime_environment_variables', 'mongodbs', 'mysqls', 'mariadbs', 'services']);
|
||||
collect(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services'])
|
||||
->flatMap(fn (string $relation) => $environment->{$relation})
|
||||
->each(fn ($resource) => exposeSensitiveFields($resource));
|
||||
$environment->redis->each(fn ($redis) => $redis->makeHidden('runtime_environment_variables'));
|
||||
$relations = ['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services'];
|
||||
$environment = $environment->load([...$relations, 'redis.runtime_environment_variables']);
|
||||
$payload = $environment->withoutRelations()->toArray();
|
||||
foreach ($relations as $relation) {
|
||||
$payload[$relation] = $environment->{$relation}
|
||||
->map(fn ($resource) => Arr::except(serializeApiResource($resource), 'runtime_environment_variables'))
|
||||
->all();
|
||||
}
|
||||
|
||||
return response()->json(serializeApiResponse($environment));
|
||||
return response()->json(serializeApiResponse($payload));
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
|
||||
@@ -69,8 +69,7 @@ class ResourcesController extends Controller
|
||||
}
|
||||
$resources = $resources->flatten();
|
||||
$resources = $resources->map(function ($resource) {
|
||||
exposeSensitiveFields($resource);
|
||||
$payload = $resource->toArray();
|
||||
$payload = serializeApiResource($resource);
|
||||
$payload['status'] = $resource->status;
|
||||
$payload['type'] = $resource->type();
|
||||
|
||||
|
||||
@@ -78,12 +78,7 @@ class ServicesController extends Controller
|
||||
$service->makeHidden(['value', 'real_value']);
|
||||
}
|
||||
|
||||
$payload = serializeApiResponse($service);
|
||||
if ($service instanceof Service && filled(data_get($payload, 'docker_compose'))) {
|
||||
$payload['docker_compose'] = redactLockedValuesInGeneratedCompose($payload['docker_compose'], $service);
|
||||
}
|
||||
|
||||
return $payload;
|
||||
return redactLockedApiResourceValues($service, serializeApiResponse($service));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Models\ApplicationSetting;
|
||||
use App\Models\Environment;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\ServerSetting;
|
||||
use App\Models\Service;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\SwarmDocker;
|
||||
use App\Rules\ManualWebhookSecret;
|
||||
@@ -178,6 +179,41 @@ function exposeSensitiveFields(Model $model): Model
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the locked (shown once) values that a serialized resource still contains, also for tokens
|
||||
* that can read sensitive data: the generated compose of an application or service, and the
|
||||
* connection URLs of a database with a locked password.
|
||||
*
|
||||
* @template T of array|SupportCollection
|
||||
*
|
||||
* @param T $payload
|
||||
* @return T
|
||||
*/
|
||||
function redactLockedApiResourceValues(Model $resource, array|SupportCollection $payload): array|SupportCollection
|
||||
{
|
||||
if (($resource instanceof Application || $resource instanceof Service) && filled($payload['docker_compose'] ?? null)) {
|
||||
$payload['docker_compose'] = redactLockedValuesInGeneratedCompose($payload['docker_compose'], $resource);
|
||||
}
|
||||
|
||||
$urlKeys = array_filter(['internal_db_url', 'external_db_url'], fn (string $key): bool => filled($payload[$key] ?? null));
|
||||
if ($urlKeys !== [] && method_exists($resource, 'isCredentialLocked') && $resource->isCredentialLocked()) {
|
||||
foreach ($urlKeys as $key) {
|
||||
$payload[$key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a resource for a list API response: sensitive fields when the token can read them,
|
||||
* without locked values.
|
||||
*/
|
||||
function serializeApiResource(Model $resource): array
|
||||
{
|
||||
return redactLockedApiResourceValues($resource, exposeSensitiveFields($resource)->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared validation rules for application create and update API endpoints.
|
||||
*
|
||||
|
||||
@@ -228,6 +228,18 @@ it('redacts a locked redis password and its connection urls in the api', functio
|
||||
|
||||
$list = $this->withHeaders($headers)->getJson('/api/v1/databases')->assertOk();
|
||||
expect($list->getContent())->not->toContain('locked-redis-secret');
|
||||
|
||||
$resources = $this->withHeaders($headers)->getJson('/api/v1/resources')->assertOk();
|
||||
expect($resources->getContent())->not->toContain('locked-redis-secret')
|
||||
->and($resources->json('0.uuid'))->toBe($redis->uuid)
|
||||
->and($resources->json('0.internal_db_url'))->toBeNull();
|
||||
|
||||
$environment = $this->withHeaders($headers)
|
||||
->getJson("/api/v1/projects/{$this->environment->project->uuid}/{$this->environment->uuid}")
|
||||
->assertOk();
|
||||
expect($environment->getContent())->not->toContain('locked-redis-secret')
|
||||
->and($environment->json('redis.0.uuid'))->toBe($redis->uuid)
|
||||
->and($environment->json('redis.0.internal_db_url'))->toBeNull();
|
||||
});
|
||||
|
||||
it('still returns the connection url of an unlocked redis password in the api', function () {
|
||||
|
||||
@@ -164,3 +164,24 @@ it('hides locked values in the generated compose returned by the API', function
|
||||
expectLockedValuesRedacted($response->json('docker_compose'));
|
||||
expect($response->json('docker_compose_raw'))->toBe(LOCKED_COMPOSE_RAW);
|
||||
})->with(['service', 'application']);
|
||||
|
||||
it('hides locked values in the generated compose returned by the resource list API', function () {
|
||||
$response = $this->withToken(lockedComposeApiToken())->getJson('/api/v1/resources')->assertOk();
|
||||
|
||||
foreach ([$this->service, $this->application] as $resource) {
|
||||
expectLockedValuesRedacted(collect($response->json())->firstWhere('uuid', $resource->uuid)['docker_compose']);
|
||||
}
|
||||
});
|
||||
|
||||
it('hides locked values in the generated compose returned by the environment API', function (string $relation, string $resource) {
|
||||
$response = $this->withToken(lockedComposeApiToken())
|
||||
->getJson("/api/v1/projects/{$this->project->uuid}/{$this->environment->uuid}")
|
||||
->assertOk();
|
||||
|
||||
expectLockedValuesRedacted($response->json("{$relation}.0.docker_compose"));
|
||||
expect($response->json("{$relation}.0.uuid"))->toBe($this->{$resource}->uuid)
|
||||
->and($response->json("{$relation}.0.docker_compose_raw"))->toBe(LOCKED_COMPOSE_RAW);
|
||||
})->with([
|
||||
'service' => ['services', 'service'],
|
||||
'application' => ['applications', 'application'],
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user