mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-14 20:17:05 +00:00
fix(api): hide sensitive fields by default
Add model-level hidden fields for secrets, tokens, keys, notification credentials, deployment logs, and environment values. Allow explicit read:sensitive API access to reveal gated private keys and deployment logs, and cover the behavior with feature and unit tests.
This commit is contained in:
1 parent
f40bb80f9d
commit
81a3bb0f07
20 files changed
+335
-2
No files matched your search
@@ -1,8 +1,10 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
@@ -101,6 +103,99 @@ describe('GET /api/v1/servers sensitive field gating', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/security/keys sensitive field gating', function () {
|
||||
beforeEach(function () {
|
||||
PrivateKey::withoutEvents(function () {
|
||||
PrivateKey::forceCreate([
|
||||
'uuid' => 'private-key-sensitive-test',
|
||||
'name' => 'Sensitive key',
|
||||
'description' => 'test',
|
||||
'private_key' => 'super-secret-private-key',
|
||||
'is_git_related' => false,
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('read token does not leak private key material', function () {
|
||||
$token = makeApiToken($this->user, $this->team, ['read']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson('/api/v1/security/keys/private-key-sensitive-test');
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
expect($response->getContent())->not->toContain('"private_key":');
|
||||
});
|
||||
|
||||
test('read sensitive token sees private key material', function () {
|
||||
$token = makeApiToken($this->user, $this->team, ['read', 'read:sensitive']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson('/api/v1/security/keys/private-key-sensitive-test');
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
expect($response->getContent())->toContain('"private_key":');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/deployments sensitive field gating', function () {
|
||||
beforeEach(function () {
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||
$destination = $this->server->standaloneDockers()->firstOrFail();
|
||||
|
||||
$this->application = Application::create([
|
||||
'name' => 'deployment-sensitive-test-app',
|
||||
'git_repository' => 'https://github.com/test/test',
|
||||
'git_branch' => 'main',
|
||||
'build_pack' => 'nixpacks',
|
||||
'ports_exposes' => '3000',
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
]);
|
||||
|
||||
ApplicationDeploymentQueue::create([
|
||||
'application_id' => $this->application->id,
|
||||
'deployment_uuid' => 'deployment-sensitive-test',
|
||||
'pull_request_id' => 0,
|
||||
'status' => 'in_progress',
|
||||
'logs' => '[{"output":"super-secret-deployment-log"}]',
|
||||
'server_id' => $this->server->id,
|
||||
'application_name' => $this->application->name,
|
||||
'server_name' => $this->server->name,
|
||||
]);
|
||||
});
|
||||
|
||||
test('read token does not leak deployment logs', function () {
|
||||
$token = makeApiToken($this->user, $this->team, ['read']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson('/api/v1/deployments/deployment-sensitive-test');
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
expect($response->getContent())->not->toContain('"logs":');
|
||||
});
|
||||
|
||||
test('read sensitive token sees deployment logs', function () {
|
||||
$token = makeApiToken($this->user, $this->team, ['read', 'read:sensitive']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson('/api/v1/deployments/deployment-sensitive-test');
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
expect($response->getContent())->toContain('"logs":');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/applications nested-relation scrubbing', function () {
|
||||
beforeEach(function () {
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\CloudInitScript;
|
||||
use App\Models\CloudProviderToken;
|
||||
use App\Models\DiscordNotificationSettings;
|
||||
use App\Models\EmailNotificationSettings;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\OauthSetting;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\PushoverNotificationSettings;
|
||||
use App\Models\S3Storage;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerSetting;
|
||||
use App\Models\Service;
|
||||
use App\Models\SharedEnvironmentVariable;
|
||||
use App\Models\SlackNotificationSettings;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
@@ -13,6 +26,8 @@ use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use App\Models\TelegramNotificationSettings;
|
||||
use App\Models\WebhookNotificationSettings;
|
||||
|
||||
describe('Sensitive model fields are hidden by default', function () {
|
||||
test('ServerSetting hides sentinel and logdrain secrets', function () {
|
||||
@@ -61,12 +76,133 @@ describe('Sensitive model fields are hidden by default', function () {
|
||||
expect($hidden)->toContain('value', 'real_value');
|
||||
});
|
||||
|
||||
test('SharedEnvironmentVariable hides value', function () {
|
||||
$hidden = (new SharedEnvironmentVariable)->getHidden();
|
||||
|
||||
expect($hidden)->toContain('value');
|
||||
});
|
||||
|
||||
test('Service hides docker_compose and docker_compose_raw', function () {
|
||||
$hidden = (new Service)->getHidden();
|
||||
|
||||
expect($hidden)->toContain('docker_compose', 'docker_compose_raw');
|
||||
});
|
||||
|
||||
test('ApplicationDeploymentQueue hides logs', function () {
|
||||
$hidden = (new ApplicationDeploymentQueue)->getHidden();
|
||||
|
||||
expect($hidden)->toContain('logs');
|
||||
});
|
||||
|
||||
test('PrivateKey hides private key material', function () {
|
||||
$hidden = (new PrivateKey)->getHidden();
|
||||
|
||||
expect($hidden)->toContain('private_key');
|
||||
});
|
||||
|
||||
test('CloudProviderToken hides provider token', function () {
|
||||
$hidden = (new CloudProviderToken)->getHidden();
|
||||
|
||||
expect($hidden)->toContain('token');
|
||||
});
|
||||
|
||||
test('CloudInitScript hides script content', function () {
|
||||
$hidden = (new CloudInitScript)->getHidden();
|
||||
|
||||
expect($hidden)->toContain('script');
|
||||
});
|
||||
|
||||
test('S3Storage hides credentials', function () {
|
||||
$hidden = (new S3Storage)->getHidden();
|
||||
|
||||
expect($hidden)->toContain('key', 'secret');
|
||||
});
|
||||
|
||||
test('OauthSetting hides client secret', function () {
|
||||
$hidden = (new OauthSetting)->getHidden();
|
||||
|
||||
expect($hidden)->toContain('client_secret');
|
||||
});
|
||||
|
||||
test('SslCertificate hides private key', function () {
|
||||
$hidden = (new SslCertificate)->getHidden();
|
||||
|
||||
expect($hidden)->toContain('ssl_private_key');
|
||||
});
|
||||
|
||||
test('EmailNotificationSettings hides SMTP and resend secrets', function () {
|
||||
$hidden = (new EmailNotificationSettings)->getHidden();
|
||||
|
||||
expect($hidden)->toContain(
|
||||
'smtp_from_address',
|
||||
'smtp_from_name',
|
||||
'smtp_recipients',
|
||||
'smtp_host',
|
||||
'smtp_username',
|
||||
'smtp_password',
|
||||
'resend_api_key',
|
||||
);
|
||||
});
|
||||
|
||||
test('InstanceSettings hides instance notification secrets', function () {
|
||||
$hidden = (new InstanceSettings)->getHidden();
|
||||
|
||||
expect($hidden)->toContain(
|
||||
'smtp_from_address',
|
||||
'smtp_from_name',
|
||||
'smtp_recipients',
|
||||
'smtp_host',
|
||||
'smtp_username',
|
||||
'smtp_password',
|
||||
'resend_api_key',
|
||||
'sentinel_token',
|
||||
);
|
||||
});
|
||||
|
||||
test('Webhook-style notification settings hide delivery endpoints', function () {
|
||||
expect((new DiscordNotificationSettings)->getHidden())->toContain('discord_webhook_url');
|
||||
expect((new SlackNotificationSettings)->getHidden())->toContain('slack_webhook_url');
|
||||
expect((new WebhookNotificationSettings)->getHidden())->toContain('webhook_url');
|
||||
});
|
||||
|
||||
test('PushoverNotificationSettings hides credentials', function () {
|
||||
$hidden = (new PushoverNotificationSettings)->getHidden();
|
||||
|
||||
expect($hidden)->toContain('pushover_user_key', 'pushover_api_token');
|
||||
});
|
||||
|
||||
test('TelegramNotificationSettings hides bot, chat, and thread identifiers', function () {
|
||||
$hidden = (new TelegramNotificationSettings)->getHidden();
|
||||
|
||||
expect($hidden)->toContain(
|
||||
'telegram_token',
|
||||
'telegram_chat_id',
|
||||
'telegram_notifications_deployment_success_thread_id',
|
||||
'telegram_notifications_deployment_failure_thread_id',
|
||||
'telegram_notifications_status_change_thread_id',
|
||||
'telegram_notifications_backup_success_thread_id',
|
||||
'telegram_notifications_backup_failure_thread_id',
|
||||
'telegram_notifications_scheduled_task_success_thread_id',
|
||||
'telegram_notifications_scheduled_task_failure_thread_id',
|
||||
'telegram_notifications_docker_cleanup_success_thread_id',
|
||||
'telegram_notifications_docker_cleanup_failure_thread_id',
|
||||
'telegram_notifications_server_disk_usage_thread_id',
|
||||
'telegram_notifications_server_reachable_thread_id',
|
||||
'telegram_notifications_server_unreachable_thread_id',
|
||||
'telegram_notifications_server_patch_thread_id',
|
||||
'telegram_notifications_traefik_outdated_thread_id',
|
||||
);
|
||||
});
|
||||
|
||||
test('TelegramNotificationSettings casts actual docker cleanup thread ids as encrypted', function () {
|
||||
$casts = (new TelegramNotificationSettings)->getCasts();
|
||||
|
||||
expect($casts)->toMatchArray([
|
||||
'telegram_notifications_docker_cleanup_success_thread_id' => 'encrypted',
|
||||
'telegram_notifications_docker_cleanup_failure_thread_id' => 'encrypted',
|
||||
]);
|
||||
});
|
||||
|
||||
test('StandalonePostgresql hides password, init_scripts, db urls', function () {
|
||||
$hidden = (new StandalonePostgresql)->getHidden();
|
||||
|
||||
|
||||
Reference in new issue
Block a user