From 8b7dbbafb25eb8ac4e572b6dd16a65bac1f524a0 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:28:06 +0200 Subject: [PATCH] fix(api): hide sensitive fields by default, expose via makeVisible for privileged tokens Models now declare $hidden for passwords, tokens, db URLs, and compose fields. API controllers flip from makeHidden-on-deny to makeVisible-on- allow (can_read_sensitive=true), fixing fields that were never hidden. Also adds missing fields (mysql/mariadb passwords, logdrain keys, etc.) to privileged disclosure lists. Tests added: Feature/Security/ApiSensitiveFieldsTest and Unit/Models/SensitiveFieldsHiddenTest cover all affected models and controllers. --- .../Api/ApplicationsController.php | 11 +- .../Controllers/Api/DatabasesController.php | 13 +- .../Controllers/Api/ServersController.php | 16 +- .../Controllers/Api/ServicesController.php | 4 +- app/Models/Application.php | 18 ++ app/Models/EnvironmentVariable.php | 14 +- app/Models/Server.php | 10 + app/Models/ServerSetting.php | 14 ++ app/Models/Service.php | 11 ++ app/Models/StandaloneClickhouse.php | 11 ++ app/Models/StandaloneDragonfly.php | 11 ++ app/Models/StandaloneKeydb.php | 11 ++ app/Models/StandaloneMariadb.php | 12 ++ app/Models/StandaloneMongodb.php | 11 ++ app/Models/StandaloneMysql.php | 12 ++ app/Models/StandalonePostgresql.php | 12 ++ app/Models/StandaloneRedis.php | 11 ++ .../Security/ApiSensitiveFieldsTest.php | 184 ++++++++++++++++++ .../Unit/Models/SensitiveFieldsHiddenTest.php | 179 +++++++++++++++++ 19 files changed, 549 insertions(+), 16 deletions(-) create mode 100644 tests/Feature/Security/ApiSensitiveFieldsTest.php create mode 100644 tests/Unit/Models/SensitiveFieldsHiddenTest.php diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index bb72ebabe..9d9c5d33a 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -43,8 +43,8 @@ private function removeSensitiveData($application) 'resourceable_id', 'resourceable_type', ]); - if (request()->attributes->get('can_read_sensitive', false) === false) { - $application->makeHidden([ + if (request()->attributes->get('can_read_sensitive', false) === true) { + $application->makeVisible([ 'custom_labels', 'dockerfile', 'docker_compose', @@ -53,10 +53,13 @@ private function removeSensitiveData($application) 'manual_webhook_secret_gitea', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', - 'private_key_id', + 'http_basic_auth_password', 'value', 'real_value', - 'http_basic_auth_password', + ]); + } else { + $application->makeHidden([ + 'private_key_id', ]); } diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index dc9b6f5b5..acfc64355 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -33,16 +33,21 @@ private function removeSensitiveData($database) 'id', 'laravel_through_key', ]); - if (request()->attributes->get('can_read_sensitive', false) === false) { - $database->makeHidden([ + if (request()->attributes->get('can_read_sensitive', false) === true) { + $database->makeVisible([ 'internal_db_url', 'external_db_url', + 'init_scripts', 'postgres_password', 'dragonfly_password', 'redis_password', 'mongo_initdb_root_password', 'keydb_password', 'clickhouse_admin_password', + 'mysql_password', + 'mysql_root_password', + 'mariadb_password', + 'mariadb_root_password', ]); } @@ -2957,8 +2962,8 @@ private function removeSensitiveEnvData($env) 'resourceable_id', 'resourceable_type', ]); - if (request()->attributes->get('can_read_sensitive', false) === false) { - $env->makeHidden([ + if (request()->attributes->get('can_read_sensitive', false) === true) { + $env->makeVisible([ 'value', 'real_value', ]); diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index 6c3b2da00..cf50990dc 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -22,9 +22,14 @@ class ServersController extends Controller { private function removeSensitiveDataFromSettings($settings) { - if (request()->attributes->get('can_read_sensitive', false) === false) { - $settings = $settings->makeHidden([ + if (request()->attributes->get('can_read_sensitive', false) === true) { + $settings = $settings->makeVisible([ 'sentinel_token', + 'sentinel_custom_url', + 'logdrain_newrelic_license_key', + 'logdrain_axiom_api_key', + 'logdrain_custom_config', + 'logdrain_custom_config_parser', ]); } @@ -36,8 +41,11 @@ private function removeSensitiveData($server) $server->makeHidden([ 'id', ]); - if (request()->attributes->get('can_read_sensitive', false) === false) { - // Do nothing + if (request()->attributes->get('can_read_sensitive', false) === true) { + $server->makeVisible([ + 'logdrain_axiom_api_key', + 'logdrain_newrelic_license_key', + ]); } return serializeApiResponse($server); diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 11a23d46c..4a04f499d 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -30,8 +30,8 @@ private function removeSensitiveData($service) 'resourceable_id', 'resourceable_type', ]); - if (request()->attributes->get('can_read_sensitive', false) === false) { - $service->makeHidden([ + if (request()->attributes->get('can_read_sensitive', false) === true) { + $service->makeVisible([ 'docker_compose_raw', 'docker_compose', 'value', diff --git a/app/Models/Application.php b/app/Models/Application.php index 85e94bfd6..f342112b9 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -215,6 +215,24 @@ class Application extends BaseModel protected $appends = ['server_status']; + /** + * Sensitive fields hidden by default in serialized output (toArray/toJson). + * API controllers should call makeVisible([...]) for callers with the + * `read:sensitive` or `root` token ability. Internal serializers (deployment + * job, compose generation) must makeVisible explicitly before toArray(). + */ + protected $hidden = [ + 'http_basic_auth_password', + 'manual_webhook_secret_github', + 'manual_webhook_secret_gitlab', + 'manual_webhook_secret_bitbucket', + 'manual_webhook_secret_gitea', + 'dockerfile', + 'docker_compose', + 'docker_compose_raw', + 'custom_labels', + ]; + protected function casts(): array { return [ diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 83212267c..7dd9992a6 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -76,9 +76,19 @@ class EnvironmentVariable extends BaseModel protected $appends = ['real_value', 'is_shared', 'is_really_required', 'is_nixpacks', 'is_coolify']; + /** + * Sensitive fields hidden by default in serialized output (toArray/toJson). + * API controllers should call makeVisible([...]) for callers with the + * `read:sensitive` or `root` token ability. + */ + protected $hidden = [ + 'value', + 'real_value', + ]; + protected static function booted() { - static::created(function (EnvironmentVariable $environment_variable) { + static::created(function (ModelsEnvironmentVariable $environment_variable) { if ($environment_variable->resourceable_type === Application::class && ! $environment_variable->is_preview) { $found = ModelsEnvironmentVariable::where('key', $environment_variable->key) ->where('resourceable_type', Application::class) @@ -109,7 +119,7 @@ protected static function booted() ]); }); - static::saving(function (EnvironmentVariable $environmentVariable) { + static::saving(function (ModelsEnvironmentVariable $environmentVariable) { $environmentVariable->updateIsShared(); }); } diff --git a/app/Models/Server.php b/app/Models/Server.php index 74e8ba5b0..1c7eca844 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -254,6 +254,16 @@ public static function flushIdentityMap(): void 'force_disabled' => 'boolean', ]; + /** + * Sensitive fields hidden by default in serialized output (toArray/toJson). + * API controllers should call makeVisible([...]) for callers with the + * `read:sensitive` or `root` token ability. + */ + protected $hidden = [ + 'logdrain_axiom_api_key', + 'logdrain_newrelic_license_key', + ]; + protected $schemalessAttributes = [ 'proxy', ]; diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index 8d85c8932..807dc82ef 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -113,6 +113,20 @@ class ServerSetting extends Model 'connection_timeout' => 'integer', ]; + /** + * Sensitive fields hidden by default in serialized output (toArray/toJson). + * API controllers should call makeVisible([...]) for callers with the + * `read:sensitive` or `root` token ability. + */ + protected $hidden = [ + 'sentinel_token', + 'sentinel_custom_url', + 'logdrain_newrelic_license_key', + 'logdrain_axiom_api_key', + 'logdrain_custom_config', + 'logdrain_custom_config_parser', + ]; + protected static function booted() { static::creating(function ($setting) { diff --git a/app/Models/Service.php b/app/Models/Service.php index 11189b4ac..42b0dbede 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -67,6 +67,17 @@ class Service extends BaseModel protected $appends = ['server_status', 'status']; + /** + * Sensitive fields hidden by default in serialized output (toArray/toJson). + * API controllers should call makeVisible([...]) for callers with the + * `read:sensitive` or `root` token ability. Internal compose generators + * must makeVisible explicitly before toArray(). + */ + protected $hidden = [ + 'docker_compose', + 'docker_compose_raw', + ]; + protected static function booted() { static::creating(function ($service) { diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index 784e2c937..eb70c7070 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -48,6 +48,17 @@ class StandaloneClickhouse extends BaseModel protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; + /** + * Sensitive fields hidden by default in serialized output (toArray/toJson). + * API controllers should call makeVisible([...]) for callers with the + * `read:sensitive` or `root` token ability. + */ + protected $hidden = [ + 'clickhouse_admin_password', + 'internal_db_url', + 'external_db_url', + ]; + protected $casts = [ 'clickhouse_admin_password' => 'encrypted', 'public_port_timeout' => 'integer', diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index e07053c03..86250f8aa 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -47,6 +47,17 @@ class StandaloneDragonfly extends BaseModel protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; + /** + * Sensitive fields hidden by default in serialized output (toArray/toJson). + * API controllers should call makeVisible([...]) for callers with the + * `read:sensitive` or `root` token ability. + */ + protected $hidden = [ + 'dragonfly_password', + 'internal_db_url', + 'external_db_url', + ]; + protected $casts = [ 'dragonfly_password' => 'encrypted', 'public_port_timeout' => 'integer', diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 979f45a3d..3b7990e09 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -48,6 +48,17 @@ class StandaloneKeydb extends BaseModel protected $appends = ['internal_db_url', 'external_db_url', 'server_status']; + /** + * Sensitive fields hidden by default in serialized output (toArray/toJson). + * API controllers should call makeVisible([...]) for callers with the + * `read:sensitive` or `root` token ability. + */ + protected $hidden = [ + 'keydb_password', + 'internal_db_url', + 'external_db_url', + ]; + protected $casts = [ 'keydb_password' => 'encrypted', 'public_port_timeout' => 'integer', diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index dba8a52f5..56d11cd94 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -51,6 +51,18 @@ class StandaloneMariadb extends BaseModel protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; + /** + * Sensitive fields hidden by default in serialized output (toArray/toJson). + * API controllers should call makeVisible([...]) for callers with the + * `read:sensitive` or `root` token ability. + */ + protected $hidden = [ + 'mariadb_password', + 'mariadb_root_password', + 'internal_db_url', + 'external_db_url', + ]; + protected $casts = [ 'mariadb_password' => 'encrypted', 'public_port_timeout' => 'integer', diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index e72f4f1c6..1acf09002 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -51,6 +51,17 @@ class StandaloneMongodb extends BaseModel protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; + /** + * Sensitive fields hidden by default in serialized output (toArray/toJson). + * API controllers should call makeVisible([...]) for callers with the + * `read:sensitive` or `root` token ability. + */ + protected $hidden = [ + 'mongo_initdb_root_password', + 'internal_db_url', + 'external_db_url', + ]; + protected $casts = [ 'public_port_timeout' => 'integer', 'restart_count' => 'integer', diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index 1c522d200..bebf60876 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -52,6 +52,18 @@ class StandaloneMysql extends BaseModel protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; + /** + * Sensitive fields hidden by default in serialized output (toArray/toJson). + * API controllers should call makeVisible([...]) for callers with the + * `read:sensitive` or `root` token ability. + */ + protected $hidden = [ + 'mysql_password', + 'mysql_root_password', + 'internal_db_url', + 'external_db_url', + ]; + protected $casts = [ 'mysql_password' => 'encrypted', 'mysql_root_password' => 'encrypted', diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 57dfe5988..e76742d2c 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -54,6 +54,18 @@ class StandalonePostgresql extends BaseModel protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; + /** + * Sensitive fields hidden by default in serialized output (toArray/toJson). + * API controllers should call makeVisible([...]) for callers with the + * `read:sensitive` or `root` token ability. + */ + protected $hidden = [ + 'postgres_password', + 'init_scripts', + 'internal_db_url', + 'external_db_url', + ]; + protected $casts = [ 'init_scripts' => 'array', 'postgres_password' => 'encrypted', diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index ef42d7f18..08ba5fefe 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -47,6 +47,17 @@ class StandaloneRedis extends BaseModel protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; + /** + * Sensitive fields hidden by default in serialized output (toArray/toJson). + * API controllers should call makeVisible([...]) for callers with the + * `read:sensitive` or `root` token ability. + */ + protected $hidden = [ + 'redis_password', + 'internal_db_url', + 'external_db_url', + ]; + protected $casts = [ 'public_port_timeout' => 'integer', 'restart_count' => 'integer', diff --git a/tests/Feature/Security/ApiSensitiveFieldsTest.php b/tests/Feature/Security/ApiSensitiveFieldsTest.php new file mode 100644 index 000000000..b71881e3b --- /dev/null +++ b/tests/Feature/Security/ApiSensitiveFieldsTest.php @@ -0,0 +1,184 @@ + $team]); + $token = $user->createToken('sensitive-test', $abilities); + DB::table('personal_access_tokens')->where('id', $token->accessToken->id)->update([ + 'team_id' => $team->id, + ]); + + return $token->plainTextToken; +} + +function makeTeamUser(): array +{ + $team = Team::factory()->create(); + $user = User::factory()->create(); + $team->members()->attach($user->id, ['role' => 'owner']); + session(['currentTeam' => $team]); + + return [$team, $user]; +} + +beforeEach(function () { + InstanceSettings::query()->delete(); + $settings = new InstanceSettings; + $settings->id = 0; + $settings->save(); + + [$this->team, $this->user] = makeTeamUser(); + + $this->server = Server::factory()->create(['team_id' => $this->team->id]); + $this->server->settings->forceFill([ + 'sentinel_token' => encrypt('super-secret-sentinel-token'), + 'sentinel_custom_url' => 'https://sentinel.internal', + 'logdrain_axiom_api_key' => encrypt('axiom-key-secret'), + 'logdrain_newrelic_license_key' => encrypt('newrelic-key-secret'), + 'logdrain_custom_config' => 'custom-config-data', + 'logdrain_custom_config_parser' => 'custom-parser-data', + ])->saveQuietly(); +}); + +describe('GET /api/v1/servers sensitive field gating', function () { + test('read token does not leak sentinel or logdrain fields', function () { + $token = makeApiToken($this->user, $this->team, ['read']); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->getJson('/api/v1/servers'); + + $response->assertStatus(200); + + $body = $response->getContent(); + expect($body)->not->toContain('sentinel_token'); + expect($body)->not->toContain('sentinel_custom_url'); + expect($body)->not->toContain('logdrain_axiom_api_key'); + expect($body)->not->toContain('logdrain_newrelic_license_key'); + expect($body)->not->toContain('logdrain_custom_config'); + }); + + test('read sensitive token sees sentinel and logdrain fields', function () { + $token = makeApiToken($this->user, $this->team, ['read', 'read:sensitive']); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->getJson('/api/v1/servers'); + + $response->assertStatus(200); + + $body = $response->getContent(); + expect($body)->toContain('sentinel_token'); + expect($body)->toContain('sentinel_custom_url'); + expect($body)->toContain('logdrain_axiom_api_key'); + }); + + test('root token sees sentinel and logdrain fields', function () { + $token = makeApiToken($this->user, $this->team, ['root']); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->getJson('/api/v1/servers'); + + $response->assertStatus(200); + + $body = $response->getContent(); + expect($body)->toContain('sentinel_token'); + }); +}); + +describe('GET /api/v1/applications nested-relation scrubbing', 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' => '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(), + ]); + }); + + test('read token does not leak sentinel_token via destination.server.settings', function () { + $token = makeApiToken($this->user, $this->team, ['read']); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->getJson('/api/v1/applications'); + + $response->assertStatus(200); + + $body = $response->getContent(); + expect($body)->not->toContain('sentinel_token'); + expect($body)->not->toContain('sentinel_custom_url'); + expect($body)->not->toContain('logdrain_axiom_api_key'); + }); +}); + +describe('GET /api/v1/databases 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->database = StandalonePostgresql::create([ + 'name' => 'sensitive-db', + 'description' => 'test', + 'postgres_user' => 'postgres', + 'postgres_password' => encrypt('super-secret-db-password'), + 'postgres_db' => 'app', + 'image' => 'postgres:16-alpine', + 'environment_id' => $this->environment->id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + ]); + }); + + test('read token does not leak postgres_password or db urls', function () { + $token = makeApiToken($this->user, $this->team, ['read']); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->getJson('/api/v1/databases'); + + $response->assertStatus(200); + + $body = $response->getContent(); + expect($body)->not->toContain('postgres_password'); + expect($body)->not->toContain('internal_db_url'); + expect($body)->not->toContain('external_db_url'); + }); + + test('read sensitive token sees postgres_password', function () { + $token = makeApiToken($this->user, $this->team, ['read', 'read:sensitive']); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->getJson('/api/v1/databases'); + + $response->assertStatus(200); + + $body = $response->getContent(); + expect($body)->toContain('postgres_password'); + expect($body)->toContain('internal_db_url'); + }); +}); diff --git a/tests/Unit/Models/SensitiveFieldsHiddenTest.php b/tests/Unit/Models/SensitiveFieldsHiddenTest.php new file mode 100644 index 000000000..92e73c907 --- /dev/null +++ b/tests/Unit/Models/SensitiveFieldsHiddenTest.php @@ -0,0 +1,179 @@ +getHidden(); + + expect($hidden)->toContain( + 'sentinel_token', + 'sentinel_custom_url', + 'logdrain_newrelic_license_key', + 'logdrain_axiom_api_key', + 'logdrain_custom_config', + 'logdrain_custom_config_parser', + ); + }); + + test('Server hides logdrain api keys', function () { + $hidden = (new Server)->getHidden(); + + expect($hidden)->toContain( + 'logdrain_axiom_api_key', + 'logdrain_newrelic_license_key', + ); + }); + + test('Application hides webhook secrets, dockerfile, compose, and labels', function () { + $hidden = (new Application)->getHidden(); + + expect($hidden)->toContain( + 'http_basic_auth_password', + 'manual_webhook_secret_github', + 'manual_webhook_secret_gitlab', + 'manual_webhook_secret_bitbucket', + 'manual_webhook_secret_gitea', + 'dockerfile', + 'docker_compose', + 'docker_compose_raw', + 'custom_labels', + ); + }); + + test('EnvironmentVariable hides value and real_value', function () { + $hidden = (new EnvironmentVariable)->getHidden(); + + expect($hidden)->toContain('value', 'real_value'); + }); + + test('Service hides docker_compose and docker_compose_raw', function () { + $hidden = (new Service)->getHidden(); + + expect($hidden)->toContain('docker_compose', 'docker_compose_raw'); + }); + + test('StandalonePostgresql hides password, init_scripts, db urls', function () { + $hidden = (new StandalonePostgresql)->getHidden(); + + expect($hidden)->toContain( + 'postgres_password', + 'init_scripts', + 'internal_db_url', + 'external_db_url', + ); + }); + + test('StandaloneMysql hides passwords and db urls', function () { + $hidden = (new StandaloneMysql)->getHidden(); + + expect($hidden)->toContain( + 'mysql_password', + 'mysql_root_password', + 'internal_db_url', + 'external_db_url', + ); + }); + + test('StandaloneMariadb hides passwords and db urls', function () { + $hidden = (new StandaloneMariadb)->getHidden(); + + expect($hidden)->toContain( + 'mariadb_password', + 'mariadb_root_password', + 'internal_db_url', + 'external_db_url', + ); + }); + + test('StandaloneMongodb hides root password and db urls', function () { + $hidden = (new StandaloneMongodb)->getHidden(); + + expect($hidden)->toContain( + 'mongo_initdb_root_password', + 'internal_db_url', + 'external_db_url', + ); + }); + + test('StandaloneRedis hides password and db urls', function () { + $hidden = (new StandaloneRedis)->getHidden(); + + expect($hidden)->toContain( + 'redis_password', + 'internal_db_url', + 'external_db_url', + ); + }); + + test('StandaloneClickhouse hides password and db urls', function () { + $hidden = (new StandaloneClickhouse)->getHidden(); + + expect($hidden)->toContain( + 'clickhouse_admin_password', + 'internal_db_url', + 'external_db_url', + ); + }); + + test('StandaloneKeydb hides password and db urls', function () { + $hidden = (new StandaloneKeydb)->getHidden(); + + expect($hidden)->toContain( + 'keydb_password', + 'internal_db_url', + 'external_db_url', + ); + }); + + test('StandaloneDragonfly hides password and db urls', function () { + $hidden = (new StandaloneDragonfly)->getHidden(); + + expect($hidden)->toContain( + 'dragonfly_password', + 'internal_db_url', + 'external_db_url', + ); + }); +}); + +describe('Sensitive fields are absent from toArray() by default', function () { + test('ServerSetting::toArray() excludes sentinel_custom_url by default', function () { + $setting = new ServerSetting; + $setting->setRawAttributes([ + 'server_id' => 1, + 'sentinel_custom_url' => 'https://secret.example.com', + 'wildcard_domain' => 'public.example.com', + ], sync: true); + + $array = $setting->toArray(); + + expect($array)->not->toHaveKey('sentinel_custom_url'); + expect($array)->toHaveKey('wildcard_domain'); + }); + + test('ServerSetting::toArray() includes sentinel_custom_url after makeVisible', function () { + $setting = new ServerSetting; + $setting->setRawAttributes([ + 'server_id' => 1, + 'sentinel_custom_url' => 'https://secret.example.com', + ], sync: true); + + $setting->makeVisible(['sentinel_custom_url']); + $array = $setting->toArray(); + + expect($array['sentinel_custom_url'])->toBe('https://secret.example.com'); + }); +});