diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index ac30f04cd7..97b82025be 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -426,7 +426,9 @@ class General extends Component $this->customNginxConfiguration = $this->application->custom_nginx_configuration; $this->isHttpBasicAuthEnabled = $this->application->is_http_basic_auth_enabled; $this->httpBasicAuthUsername = $this->application->http_basic_auth_username; - $this->httpBasicAuthPassword = $this->application->http_basic_auth_password; + $this->httpBasicAuthPassword = auth()->user()->can('update', $this->application) + ? $this->application->http_basic_auth_password + : null; $this->watchPaths = $this->application->watch_paths; $this->redirect = $this->application->redirect; diff --git a/app/Livewire/Server/LogDrains.php b/app/Livewire/Server/LogDrains.php index c3f36f7a7e..cdc4162bba 100644 --- a/app/Livewire/Server/LogDrains.php +++ b/app/Livewire/Server/LogDrains.php @@ -60,7 +60,9 @@ class LogDrains extends Component $this->server->settings->logdrain_newrelic_base_uri = $this->logDrainNewRelicBaseUri; } else { $this->isLogDrainNewRelicEnabled = $this->server->settings->is_logdrain_newrelic_enabled; - $this->logDrainNewRelicLicenseKey = $this->server->settings->logdrain_newrelic_license_key; + $this->logDrainNewRelicLicenseKey = auth()->user()->can('update', $this->server) + ? $this->server->settings->logdrain_newrelic_license_key + : null; $this->logDrainNewRelicBaseUri = $this->server->settings->logdrain_newrelic_base_uri; } } @@ -74,7 +76,9 @@ class LogDrains extends Component } else { $this->isLogDrainAxiomEnabled = $this->server->settings->is_logdrain_axiom_enabled; $this->logDrainAxiomDatasetName = $this->server->settings->logdrain_axiom_dataset_name; - $this->logDrainAxiomApiKey = $this->server->settings->logdrain_axiom_api_key; + $this->logDrainAxiomApiKey = auth()->user()->can('update', $this->server) + ? $this->server->settings->logdrain_axiom_api_key + : null; } } @@ -86,8 +90,12 @@ class LogDrains extends Component $this->server->settings->logdrain_custom_config_parser = $this->logDrainCustomConfigParser; } else { $this->isLogDrainCustomEnabled = $this->server->settings->is_logdrain_custom_enabled; - $this->logDrainCustomConfig = $this->server->settings->logdrain_custom_config; - $this->logDrainCustomConfigParser = $this->server->settings->logdrain_custom_config_parser; + $this->logDrainCustomConfig = auth()->user()->can('update', $this->server) + ? $this->server->settings->logdrain_custom_config + : null; + $this->logDrainCustomConfigParser = auth()->user()->can('update', $this->server) + ? $this->server->settings->logdrain_custom_config_parser + : null; } } diff --git a/app/Livewire/Server/Sentinel.php b/app/Livewire/Server/Sentinel.php index 0ad55a9deb..4719033f92 100644 --- a/app/Livewire/Server/Sentinel.php +++ b/app/Livewire/Server/Sentinel.php @@ -43,6 +43,7 @@ class Sentinel extends Component public function mount() { + $this->authorize('viewSentinel', $this->server); $this->syncData(); } diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index 3dbf7f73bd..a0365f8056 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -286,11 +286,15 @@ class Show extends Component $this->isSwarmWorker = $this->server->settings->is_swarm_worker; $this->serverRole = $this->server->settings->effectiveServerRole()->value; $this->isMetricsEnabled = $this->server->settings->is_metrics_enabled; - $this->sentinelToken = $this->server->settings->sentinel_token; + $this->sentinelToken = auth()->user()->can('update', $this->server) + ? $this->server->settings->sentinel_token + : ''; $this->sentinelMetricsRefreshRateSeconds = $this->server->settings->sentinel_metrics_refresh_rate_seconds; $this->sentinelMetricsHistoryDays = $this->server->settings->sentinel_metrics_history_days; $this->sentinelPushIntervalSeconds = $this->server->settings->sentinel_push_interval_seconds; - $this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url; + $this->sentinelCustomUrl = auth()->user()->can('update', $this->server) + ? $this->server->settings->sentinel_custom_url + : null; $this->isSentinelDebugEnabled = $this->server->settings->is_sentinel_debug_enabled; $this->sentinelUpdatedAt = $this->server->sentinel_updated_at; $this->serverTimezone = $this->server->settings->server_timezone; diff --git a/tests/Feature/LivewireResourceDataVisibilityTest.php b/tests/Feature/LivewireResourceDataVisibilityTest.php new file mode 100644 index 0000000000..d32e036e7d --- /dev/null +++ b/tests/Feature/LivewireResourceDataVisibilityTest.php @@ -0,0 +1,135 @@ + InstanceSettings::firstOrCreate(['id' => 0])); + $this->team = Team::factory()->create(['show_boarding' => false]); + $this->member = User::factory()->create(); + $this->admin = User::factory()->create(); + $this->team->members()->attach($this->member, ['role' => 'member']); + $this->team->members()->attach($this->admin, ['role' => 'admin']); + $this->server = Server::factory()->create(['team_id' => $this->team->id]); + $this->server->settings->updateQuietly([ + 'sentinel_token' => 'SENTINEL-MARKER-SECRET', + 'sentinel_custom_url' => 'https://sentinel.example.test/MARKER-SECRET', + 'logdrain_newrelic_license_key' => 'NEWRELIC-MARKER-SECRET', + 'logdrain_axiom_api_key' => 'AXIOM-MARKER-SECRET', + 'logdrain_custom_config' => 'CUSTOM-CONFIG-MARKER-SECRET', + 'logdrain_custom_config_parser' => 'CUSTOM-PARSER-MARKER-SECRET', + ]); + $this->project = Project::factory()->create(['team_id' => $this->team->id]); + $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); + $destination = StandaloneDocker::query()->where('server_id', $this->server->id)->firstOrFail(); + $this->application = Application::factory()->create([ + 'environment_id' => $this->environment->id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + 'http_basic_auth_password' => 'BASIC-MARKER-SECRET', + 'is_http_basic_auth_enabled' => true, + 'http_basic_auth_username' => 'admin', + 'redirect' => 'no', + 'static_image' => 'nginx:alpine', + 'base_directory' => '/', + 'build_pack' => 'nixpacks', + ]); +}); + +function useSecretExposureUser(User $user, Team $team): void +{ + test()->actingAs($user); + session(['currentTeam' => $team]); +} + +test('server overview redacts the Sentinel token from a member snapshot', function () { + useSecretExposureUser($this->member, $this->team); + Livewire::test(Show::class, ['server_uuid' => $this->server->uuid]) + ->assertSuccessful()->assertSet('sentinelToken', '')->assertSet('sentinelCustomUrl', null) + ->call('refresh')->assertSet('sentinelToken', '')->assertSet('sentinelCustomUrl', null); + $this->get(route('server.show', ['server_uuid' => $this->server->uuid])) + ->assertSuccessful()->assertDontSee('SENTINEL-MARKER-SECRET')->assertDontSee('sentinel.example.test/MARKER-SECRET'); + + useSecretExposureUser($this->admin, $this->team); + Livewire::test(Show::class, ['server_uuid' => $this->server->uuid]) + ->assertSuccessful()->assertSet('sentinelToken', 'SENTINEL-MARKER-SECRET') + ->assertSet('sentinelCustomUrl', 'https://sentinel.example.test/MARKER-SECRET'); +}); + +test('log drain page redacts provider keys from a member snapshot', function () { + useSecretExposureUser($this->member, $this->team); + Livewire::test(LogDrains::class, ['server_uuid' => $this->server->uuid]) + ->assertSuccessful()->assertSet('logDrainNewRelicLicenseKey', null)->assertSet('logDrainAxiomApiKey', null) + ->assertSet('logDrainCustomConfig', null)->assertSet('logDrainCustomConfigParser', null); + $this->get(route('server.log-drains', ['server_uuid' => $this->server->uuid])) + ->assertSuccessful()->assertDontSee('NEWRELIC-MARKER-SECRET')->assertDontSee('AXIOM-MARKER-SECRET') + ->assertDontSee('CUSTOM-CONFIG-MARKER-SECRET')->assertDontSee('CUSTOM-PARSER-MARKER-SECRET'); + + useSecretExposureUser($this->admin, $this->team); + Livewire::test(LogDrains::class, ['server_uuid' => $this->server->uuid]) + ->assertSuccessful()->assertSet('logDrainNewRelicLicenseKey', 'NEWRELIC-MARKER-SECRET') + ->assertSet('logDrainAxiomApiKey', 'AXIOM-MARKER-SECRET') + ->assertSet('logDrainCustomConfig', 'CUSTOM-CONFIG-MARKER-SECRET') + ->assertSet('logDrainCustomConfigParser', 'CUSTOM-PARSER-MARKER-SECRET'); +}); + +test('application page redacts HTTP basic password from a member snapshot', function () { + useSecretExposureUser($this->member, $this->team); + Livewire::test(General::class, ['application' => $this->application]) + ->assertSuccessful()->assertSet('httpBasicAuthPassword', null); + $this->get(route('project.application.configuration', [ + 'project_uuid' => $this->project->uuid, + 'environment_uuid' => $this->environment->uuid, + 'application_uuid' => $this->application->uuid, + ]))->assertSuccessful()->assertDontSee('BASIC-MARKER-SECRET'); + + useSecretExposureUser($this->admin, $this->team); + Livewire::test(General::class, ['application' => $this->application]) + ->assertSuccessful()->assertSet('httpBasicAuthPassword', 'BASIC-MARKER-SECRET'); +}); + +test('the dedicated Sentinel page denies members and cross-team users', function () { + useSecretExposureUser($this->member, $this->team); + $this->get(route('server.sentinel', ['server_uuid' => $this->server->uuid]))->assertForbidden(); + Livewire::test(Sentinel::class, ['server' => $this->server])->assertForbidden(); + + useSecretExposureUser($this->admin, $this->team); + $this->get(route('server.sentinel', ['server_uuid' => $this->server->uuid]))->assertSuccessful(); + Livewire::test(Sentinel::class, ['server' => $this->server]) + ->assertSuccessful()->assertSet('sentinelToken', 'SENTINEL-MARKER-SECRET'); + + $outsider = User::factory()->create(); + $outsideTeam = $outsider->teams()->first(); + $outsideTeam->updateQuietly(['show_boarding' => false]); + useSecretExposureUser($outsider, $outsideTeam); + $this->get(route('server.show', ['server_uuid' => $this->server->uuid]))->assertNotFound(); + $this->get(route('server.log-drains', ['server_uuid' => $this->server->uuid]))->assertNotFound(); + $this->get(route('server.sentinel', ['server_uuid' => $this->server->uuid]))->assertNotFound(); + $this->get(route('project.application.configuration', [ + 'project_uuid' => $this->project->uuid, + 'environment_uuid' => $this->environment->uuid, + 'application_uuid' => $this->application->uuid, + ]))->assertNotFound(); +}); + +test('model serialization keeps the three secrets hidden', function () { + expect(json_encode($this->server->toArray())) + ->not->toContain('SENTINEL-MARKER-SECRET', 'NEWRELIC-MARKER-SECRET', 'AXIOM-MARKER-SECRET', 'CUSTOM-CONFIG-MARKER-SECRET', 'CUSTOM-PARSER-MARKER-SECRET', 'sentinel.example.test/MARKER-SECRET'); + expect(json_encode($this->server->settings->toArray())) + ->not->toContain('SENTINEL-MARKER-SECRET', 'NEWRELIC-MARKER-SECRET', 'AXIOM-MARKER-SECRET', 'CUSTOM-CONFIG-MARKER-SECRET', 'CUSTOM-PARSER-MARKER-SECRET', 'sentinel.example.test/MARKER-SECRET'); + expect(json_encode($this->application->toArray()))->not->toContain('BASIC-MARKER-SECRET'); +});