mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(env): resolve and redact server-scoped shared variables in compose
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
00d0b4be1d
commit
106a6d7ac0
@@ -5978,7 +5978,7 @@ function convertContainerMemoryBytesToMegabytes(array $metrics): array
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve shared environment variable patterns like {{environment.VAR}}, {{project.VAR}}, {{team.VAR}}.
|
||||
* Resolve shared environment variable patterns like {{environment.VAR}}, {{project.VAR}}, {{team.VAR}}, {{server.VAR}}.
|
||||
*
|
||||
* This is the canonical implementation used by both EnvironmentVariable::realValue and the compose parsers
|
||||
* to ensure shared variable references are replaced with their actual values.
|
||||
@@ -6006,6 +6006,8 @@ function resolveSharedEnvironmentVariables(?string $value, $resource): ?string
|
||||
$id = $resource->environment->project->id;
|
||||
} elseif ($type->value() === 'team') {
|
||||
$id = $resource->team()->id;
|
||||
} elseif ($type->value() === 'server') {
|
||||
$id = data_get($resource, 'destination.server_id');
|
||||
}
|
||||
if (is_null($id)) {
|
||||
continue;
|
||||
@@ -6029,7 +6031,7 @@ function resolveSharedEnvironmentVariables(?string $value, $resource): ?string
|
||||
* The compose parsers write resolved values into `docker_compose`: environment variables, database
|
||||
* overrides, and shared variable references. Deployments need those values, so only the displayed
|
||||
* copy is redacted. Covered: the resource's locked variables (also variables that reference a locked
|
||||
* shared variable) and locked team, project, and environment variables that the raw compose references.
|
||||
* shared variable) and locked team, project, environment, and server variables that the raw compose references.
|
||||
* Values shorter than 4 characters are not replaced, because they would match unrelated text.
|
||||
* A parsed compose (array or collection) gets the same replacement in every string value.
|
||||
*
|
||||
@@ -6047,13 +6049,14 @@ function redactLockedValuesInGeneratedCompose(string|array|Collection|null $comp
|
||||
$values = EnvironmentVariable::logRedactionSecretsFor($resource, includePreview: true, resolveRemoteSecrets: false)['locked'];
|
||||
|
||||
preg_match_all('/{{\s*(\w+)\.([^}\s]+)\s*}}/', (string) $resource->docker_compose_raw, $matches, PREG_SET_ORDER);
|
||||
$references = collect($matches)->filter(fn (array $match): bool => in_array($match[1], ['team', 'project', 'environment'], true));
|
||||
$references = collect($matches)->filter(fn (array $match): bool => in_array($match[1], SHARED_VARIABLE_TYPES, true));
|
||||
$teamId = $resource->team()?->id;
|
||||
if ($references->isNotEmpty() && $teamId !== null) {
|
||||
$scopeIds = [
|
||||
'team' => $teamId,
|
||||
'project' => $resource->environment?->project_id,
|
||||
'environment' => $resource->environment_id,
|
||||
'server' => data_get($resource, 'destination.server_id'),
|
||||
];
|
||||
$values = SharedEnvironmentVariable::query()
|
||||
->where('team_id', $teamId)
|
||||
|
||||
@@ -28,6 +28,7 @@ services:
|
||||
LOCKED_TOKEN: ""
|
||||
PLAIN_TOKEN: ""
|
||||
TEAM_SECRET: '{{team.TEAM_SECRET}}'
|
||||
SERVER_SECRET: '{{server.SERVER_SECRET}}'
|
||||
YAML;
|
||||
|
||||
beforeEach(function () {
|
||||
@@ -52,6 +53,15 @@ beforeEach(function () {
|
||||
'is_shown_once' => true,
|
||||
]);
|
||||
|
||||
SharedEnvironmentVariable::create([
|
||||
'type' => 'server',
|
||||
'key' => 'SERVER_SECRET',
|
||||
'value' => 'server-shared-locked-secret',
|
||||
'team_id' => $this->team->id,
|
||||
'server_id' => $server->id,
|
||||
'is_shown_once' => true,
|
||||
]);
|
||||
|
||||
$this->service = Service::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'server_id' => $server->id,
|
||||
@@ -92,6 +102,7 @@ function expectLockedValuesRedacted(?string $text): void
|
||||
{
|
||||
expect($text)->not->toContain('env-locked-secret-value')
|
||||
->not->toContain('team-shared-locked-secret')
|
||||
->not->toContain('server-shared-locked-secret')
|
||||
->toContain('<locked>')
|
||||
->toContain('plain-visible-value');
|
||||
}
|
||||
@@ -101,7 +112,8 @@ function expectStoredComposeKeepsRealValues(Application|Service $resource): void
|
||||
expect($resource->fresh()->docker_compose)
|
||||
->not->toContain('<locked>')
|
||||
->toContain('env-locked-secret-value')
|
||||
->toContain('team-shared-locked-secret');
|
||||
->toContain('team-shared-locked-secret')
|
||||
->toContain('server-shared-locked-secret');
|
||||
}
|
||||
|
||||
it('writes the locked values into the generated compose that deployments use', function (string $resource) {
|
||||
@@ -117,7 +129,8 @@ it('hides locked values in the service compose that an owner sees', function (st
|
||||
expectLockedValuesRedacted($livewire->get('dockerCompose'));
|
||||
expect(json_encode($livewire->snapshot).$livewire->html())
|
||||
->not->toContain('env-locked-secret-value')
|
||||
->not->toContain('team-shared-locked-secret');
|
||||
->not->toContain('team-shared-locked-secret')
|
||||
->not->toContain('server-shared-locked-secret');
|
||||
})->with(['stack form' => [StackForm::class], 'compose editor' => [EditCompose::class]]);
|
||||
|
||||
it('hides locked values in the application compose that an owner sees', function () {
|
||||
@@ -129,7 +142,8 @@ it('hides locked values in the application compose that an owner sees', function
|
||||
expectLockedValuesRedacted($livewire->get('dockerCompose'));
|
||||
expect(json_encode($livewire->snapshot).$livewire->html())
|
||||
->not->toContain('env-locked-secret-value')
|
||||
->not->toContain('team-shared-locked-secret');
|
||||
->not->toContain('team-shared-locked-secret')
|
||||
->not->toContain('server-shared-locked-secret');
|
||||
});
|
||||
|
||||
it('keeps the real generated compose when an owner saves the service stack form', function () {
|
||||
|
||||
@@ -4,6 +4,7 @@ use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\SharedEnvironmentVariable;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
@@ -21,8 +22,13 @@ beforeEach(function () {
|
||||
'project_id' => $this->project->id,
|
||||
]);
|
||||
|
||||
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$destination = $this->server->standaloneDockers()->firstOrFail();
|
||||
|
||||
$this->application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -64,6 +70,19 @@ test('resolveSharedEnvironmentVariables resolves team-scoped variable', function
|
||||
expect($resolved)->toBe('sk-123456');
|
||||
});
|
||||
|
||||
test('resolveSharedEnvironmentVariables resolves server-scoped variable', function () {
|
||||
SharedEnvironmentVariable::create([
|
||||
'key' => 'COMMON_PG_HOST',
|
||||
'value' => 'postgres.coolify',
|
||||
'type' => 'server',
|
||||
'server_id' => $this->server->id,
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$resolved = resolveSharedEnvironmentVariables('{{server.COMMON_PG_HOST}}', $this->application);
|
||||
expect($resolved)->toBe('postgres.coolify');
|
||||
});
|
||||
|
||||
test('resolveSharedEnvironmentVariables returns original when no match found', function () {
|
||||
$resolved = resolveSharedEnvironmentVariables('{{environment.NONEXISTENT}}', $this->application);
|
||||
expect($resolved)->toBe('{{environment.NONEXISTENT}}');
|
||||
|
||||
Reference in New Issue
Block a user