From 5973bb4d4f3c236d76ac25cb77c22e5317d5379f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:24:19 +0200 Subject: [PATCH] fix(security): hide notification secrets from non-admins Prevent users without update permission from reading notification credentials and manual webhook secrets in Livewire state or rendered forms. --- app/Livewire/Notifications/Discord.php | 4 +- app/Livewire/Notifications/Pushover.php | 9 ++- app/Livewire/Notifications/Slack.php | 4 +- app/Livewire/Notifications/Telegram.php | 9 ++- app/Livewire/Notifications/Webhook.php | 4 +- app/Livewire/Project/Shared/Webhooks.php | 19 +++-- .../livewire/notifications/discord.blade.php | 12 ++- .../livewire/notifications/pushover.blade.php | 21 ++++-- .../livewire/notifications/slack.blade.php | 12 ++- .../livewire/notifications/telegram.blade.php | 21 ++++-- .../livewire/notifications/webhook.blade.php | 12 ++- .../project/shared/webhooks.blade.php | 16 ++-- .../NotificationAuthorizationTest.php | 74 ++++++++++++++++++- .../SharedResourceAuthorizationTest.php | 47 +++++++++++- 14 files changed, 219 insertions(+), 45 deletions(-) diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php index ab3884320..59350a3e1 100644 --- a/app/Livewire/Notifications/Discord.php +++ b/app/Livewire/Notifications/Discord.php @@ -110,7 +110,9 @@ public function syncData(bool $toModel = false) refreshSession(); } else { $this->discordEnabled = $this->settings->discord_enabled; - $this->discordWebhookUrl = $this->settings->discord_webhook_url; + $this->discordWebhookUrl = auth()->user()->can('update', $this->settings) + ? $this->settings->discord_webhook_url + : null; $this->deploymentSuccessDiscordNotifications = $this->settings->deployment_success_discord_notifications; $this->deploymentFailureDiscordNotifications = $this->settings->deployment_failure_discord_notifications; diff --git a/app/Livewire/Notifications/Pushover.php b/app/Livewire/Notifications/Pushover.php index d79eea87b..f894c5005 100644 --- a/app/Livewire/Notifications/Pushover.php +++ b/app/Livewire/Notifications/Pushover.php @@ -113,8 +113,13 @@ public function syncData(bool $toModel = false) refreshSession(); } else { $this->pushoverEnabled = $this->settings->pushover_enabled; - $this->pushoverUserKey = $this->settings->pushover_user_key; - $this->pushoverApiToken = $this->settings->pushover_api_token; + if (auth()->user()->can('update', $this->settings)) { + $this->pushoverUserKey = $this->settings->pushover_user_key; + $this->pushoverApiToken = $this->settings->pushover_api_token; + } else { + $this->pushoverUserKey = null; + $this->pushoverApiToken = null; + } $this->deploymentSuccessPushoverNotifications = $this->settings->deployment_success_pushover_notifications; $this->deploymentFailurePushoverNotifications = $this->settings->deployment_failure_pushover_notifications; diff --git a/app/Livewire/Notifications/Slack.php b/app/Livewire/Notifications/Slack.php index f870b3986..58cab5494 100644 --- a/app/Livewire/Notifications/Slack.php +++ b/app/Livewire/Notifications/Slack.php @@ -110,7 +110,9 @@ public function syncData(bool $toModel = false) refreshSession(); } else { $this->slackEnabled = $this->settings->slack_enabled; - $this->slackWebhookUrl = $this->settings->slack_webhook_url; + $this->slackWebhookUrl = auth()->user()->can('update', $this->settings) + ? $this->settings->slack_webhook_url + : null; $this->deploymentSuccessSlackNotifications = $this->settings->deployment_success_slack_notifications; $this->deploymentFailureSlackNotifications = $this->settings->deployment_failure_slack_notifications; diff --git a/app/Livewire/Notifications/Telegram.php b/app/Livewire/Notifications/Telegram.php index fc3966cf6..78eb7ef9f 100644 --- a/app/Livewire/Notifications/Telegram.php +++ b/app/Livewire/Notifications/Telegram.php @@ -169,8 +169,13 @@ public function syncData(bool $toModel = false) $this->settings->save(); } else { $this->telegramEnabled = $this->settings->telegram_enabled; - $this->telegramToken = $this->settings->telegram_token; - $this->telegramChatId = $this->settings->telegram_chat_id; + if (auth()->user()->can('update', $this->settings)) { + $this->telegramToken = $this->settings->telegram_token; + $this->telegramChatId = $this->settings->telegram_chat_id; + } else { + $this->telegramToken = null; + $this->telegramChatId = null; + } $this->deploymentSuccessTelegramNotifications = $this->settings->deployment_success_telegram_notifications; $this->deploymentFailureTelegramNotifications = $this->settings->deployment_failure_telegram_notifications; diff --git a/app/Livewire/Notifications/Webhook.php b/app/Livewire/Notifications/Webhook.php index 630d422a9..4a67180ff 100644 --- a/app/Livewire/Notifications/Webhook.php +++ b/app/Livewire/Notifications/Webhook.php @@ -105,7 +105,9 @@ public function syncData(bool $toModel = false) refreshSession(); } else { $this->webhookEnabled = $this->settings->webhook_enabled; - $this->webhookUrl = $this->settings->webhook_url; + $this->webhookUrl = auth()->user()->can('update', $this->settings) + ? $this->settings->webhook_url + : null; $this->deploymentSuccessWebhookNotifications = $this->settings->deployment_success_webhook_notifications; $this->deploymentFailureWebhookNotifications = $this->settings->deployment_failure_webhook_notifications; diff --git a/app/Livewire/Project/Shared/Webhooks.php b/app/Livewire/Project/Shared/Webhooks.php index eafc653d5..eb90262e9 100644 --- a/app/Livewire/Project/Shared/Webhooks.php +++ b/app/Livewire/Project/Shared/Webhooks.php @@ -34,19 +34,24 @@ public function mount() { $this->deploywebhook = generateDeployWebhook($this->resource); - $this->githubManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_github'); + if ($this->canViewSecrets()) { + $this->githubManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_github'); + $this->gitlabManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitlab'); + $this->bitbucketManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_bitbucket'); + $this->giteaManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitea'); + } + $this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github'); - - $this->gitlabManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitlab'); $this->gitlabManualWebhook = generateGitManualWebhook($this->resource, 'gitlab'); - - $this->bitbucketManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_bitbucket'); $this->bitbucketManualWebhook = generateGitManualWebhook($this->resource, 'bitbucket'); - - $this->giteaManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitea'); $this->giteaManualWebhook = generateGitManualWebhook($this->resource, 'gitea'); } + public function canViewSecrets(): bool + { + return auth()->user()->can('update', $this->resource); + } + public function submit() { try { diff --git a/resources/views/livewire/notifications/discord.blade.php b/resources/views/livewire/notifications/discord.blade.php index 0e5406c78..64e445441 100644 --- a/resources/views/livewire/notifications/discord.blade.php +++ b/resources/views/livewire/notifications/discord.blade.php @@ -26,9 +26,15 @@ helper="If enabled, a ping (@here) will be sent to the notification when a critical event happens." label="Ping Enabled" /> - + @can('update', $settings) + + @else + + @endcan

Notification Settings

diff --git a/resources/views/livewire/notifications/pushover.blade.php b/resources/views/livewire/notifications/pushover.blade.php index 74cd9e8d2..4ea1a159c 100644 --- a/resources/views/livewire/notifications/pushover.blade.php +++ b/resources/views/livewire/notifications/pushover.blade.php @@ -24,12 +24,21 @@

- - + @can('update', $settings) + + + @else + + + @endcan

Notification Settings

diff --git a/resources/views/livewire/notifications/slack.blade.php b/resources/views/livewire/notifications/slack.blade.php index 14c7b3508..9ab752291 100644 --- a/resources/views/livewire/notifications/slack.blade.php +++ b/resources/views/livewire/notifications/slack.blade.php @@ -23,9 +23,15 @@
- + @can('update', $settings) + + @else + + @endcan

Notification Settings

diff --git a/resources/views/livewire/notifications/telegram.blade.php b/resources/views/livewire/notifications/telegram.blade.php index f87a13c37..98ec128d5 100644 --- a/resources/views/livewire/notifications/telegram.blade.php +++ b/resources/views/livewire/notifications/telegram.blade.php @@ -24,12 +24,21 @@

- - + @can('update', $settings) + + + @else + + + @endcan

Notification Settings

diff --git a/resources/views/livewire/notifications/webhook.blade.php b/resources/views/livewire/notifications/webhook.blade.php index 7c32311bf..4bacb6091 100644 --- a/resources/views/livewire/notifications/webhook.blade.php +++ b/resources/views/livewire/notifications/webhook.blade.php @@ -28,9 +28,15 @@ class="normal-case dark:text-white btn btn-xs no-animation btn-primary">
- + @can('update', $settings) + + @else + + @endcan

Notification Settings

diff --git a/resources/views/livewire/project/shared/webhooks.blade.php b/resources/views/livewire/project/shared/webhooks.blade.php index 24bba525a..bccc31383 100644 --- a/resources/views/livewire/project/shared/webhooks.blade.php +++ b/resources/views/livewire/project/shared/webhooks.blade.php @@ -22,9 +22,9 @@ helper="Need to set a secret to be able to use this webhook. It should match with the secret in GitHub." label="GitHub Webhook Secret" id="githubManualWebhookSecret">
@else - + label="GitHub Webhook Secret" value="Hidden (only admins can view)">
@endcan @@ -39,9 +39,9 @@ helper="Need to set a secret to be able to use this webhook. It should match with the secret in GitLab." label="GitLab Webhook Secret" id="gitlabManualWebhookSecret">
@else - + label="GitLab Webhook Secret" value="Hidden (only admins can view)">
@endcan
@@ -51,9 +51,9 @@ helper="Need to set a secret to be able to use this webhook. It should match with the secret in Bitbucket." label="Bitbucket Webhook Secret" id="bitbucketManualWebhookSecret"> @else - + label="Bitbucket Webhook Secret" value="Hidden (only admins can view)"> @endcan
@@ -63,9 +63,9 @@ helper="Need to set a secret to be able to use this webhook. It should match with the secret in Gitea." label="Gitea Webhook Secret" id="giteaManualWebhookSecret"> @else - + label="Gitea Webhook Secret" value="Hidden (only admins can view)"> @endcan
@can('update', $resource) diff --git a/tests/Feature/Authorization/NotificationAuthorizationTest.php b/tests/Feature/Authorization/NotificationAuthorizationTest.php index 7a7ac94aa..aa9d1cbdc 100644 --- a/tests/Feature/Authorization/NotificationAuthorizationTest.php +++ b/tests/Feature/Authorization/NotificationAuthorizationTest.php @@ -15,7 +15,7 @@ uses(RefreshDatabase::class); beforeEach(function () { - InstanceSettings::updateOrCreate(['id' => 0]); + InstanceSettings::unguarded(fn () => InstanceSettings::updateOrCreate(['id' => 0], ['id' => 0])); $this->team = Team::factory()->create(); @@ -275,3 +275,75 @@ expect($this->member->can('sendTest', $this->team->pushoverNotificationSettings))->toBeFalse(); expect($this->member->can('sendTest', $this->team->webhookNotificationSettings))->toBeFalse(); }); + +test('member cannot view notification secrets', function (string $component, string $settingsRelation, array $secrets) { + $settings = $this->team->{$settingsRelation}; + $settings->update($secrets); + + $this->actingAs($this->member); + session(['currentTeam' => $this->team]); + + $componentTest = Livewire::test($component); + + foreach ($secrets as $column => $value) { + $property = str($column)->camel()->toString(); + + $componentTest + ->assertSet($property, null) + ->assertDontSee($value); + } + + $componentTest->assertSee('Hidden (only admins can view)'); +})->with([ + 'discord webhook' => [DiscordNotification::class, 'discordNotificationSettings', [ + 'discord_webhook_url' => 'https://discord.com/api/webhooks/secret-member', + ]], + 'slack webhook' => [SlackNotification::class, 'slackNotificationSettings', [ + 'slack_webhook_url' => 'https://hooks.slack.com/services/secret-member', + ]], + 'telegram token and chat id' => [TelegramNotification::class, 'telegramNotificationSettings', [ + 'telegram_token' => 'telegram-secret-token', + 'telegram_chat_id' => 'telegram-secret-chat', + ]], + 'pushover credentials' => [PushoverNotification::class, 'pushoverNotificationSettings', [ + 'pushover_user_key' => 'pushover-secret-user', + 'pushover_api_token' => 'pushover-secret-token', + ]], + 'generic webhook' => [WebhookNotification::class, 'webhookNotificationSettings', [ + 'webhook_url' => 'https://example.com/secret-webhook', + ]], +]); + +test('admin can view notification secrets', function (string $component, string $settingsRelation, array $secrets) { + $settings = $this->team->{$settingsRelation}; + $settings->update($secrets); + + $this->actingAs($this->admin); + session(['currentTeam' => $this->team]); + + $componentTest = Livewire::test($component); + + foreach ($secrets as $column => $value) { + $property = str($column)->camel()->toString(); + + $componentTest->assertSet($property, $value); + } +})->with([ + 'discord webhook' => [DiscordNotification::class, 'discordNotificationSettings', [ + 'discord_webhook_url' => 'https://discord.com/api/webhooks/secret-admin', + ]], + 'slack webhook' => [SlackNotification::class, 'slackNotificationSettings', [ + 'slack_webhook_url' => 'https://hooks.slack.com/services/secret-admin', + ]], + 'telegram token and chat id' => [TelegramNotification::class, 'telegramNotificationSettings', [ + 'telegram_token' => 'telegram-admin-token', + 'telegram_chat_id' => 'telegram-admin-chat', + ]], + 'pushover credentials' => [PushoverNotification::class, 'pushoverNotificationSettings', [ + 'pushover_user_key' => 'pushover-admin-user', + 'pushover_api_token' => 'pushover-admin-token', + ]], + 'generic webhook' => [WebhookNotification::class, 'webhookNotificationSettings', [ + 'webhook_url' => 'https://example.com/admin-webhook', + ]], +]); diff --git a/tests/Feature/Authorization/SharedResourceAuthorizationTest.php b/tests/Feature/Authorization/SharedResourceAuthorizationTest.php index 23f134458..57e46c418 100644 --- a/tests/Feature/Authorization/SharedResourceAuthorizationTest.php +++ b/tests/Feature/Authorization/SharedResourceAuthorizationTest.php @@ -20,7 +20,7 @@ uses(RefreshDatabase::class); beforeEach(function () { - InstanceSettings::updateOrCreate(['id' => 0]); + InstanceSettings::unguarded(fn () => InstanceSettings::updateOrCreate(['id' => 0], ['id' => 0])); $this->team = Team::factory()->create(); @@ -167,6 +167,51 @@ expect($this->admin->can('update', $this->application))->toBeTrue(); }); +test('member cannot view application webhook secrets', function () { + $this->application->update([ + 'git_repository' => 'coollabsio/coolify', + 'git_branch' => 'main', + 'manual_webhook_secret_github' => 'github-secret-value', + 'manual_webhook_secret_gitlab' => 'gitlab-secret-value', + 'manual_webhook_secret_bitbucket' => 'bitbucket-secret-value', + 'manual_webhook_secret_gitea' => 'gitea-secret-value', + ]); + + $this->actingAs($this->member); + session(['currentTeam' => $this->team]); + + Livewire::test(Webhooks::class, ['resource' => $this->application->fresh()]) + ->assertSet('githubManualWebhookSecret', null) + ->assertSet('gitlabManualWebhookSecret', null) + ->assertSet('bitbucketManualWebhookSecret', null) + ->assertSet('giteaManualWebhookSecret', null) + ->assertSee('Hidden (only admins can view)') + ->assertDontSee('github-secret-value') + ->assertDontSee('gitlab-secret-value') + ->assertDontSee('bitbucket-secret-value') + ->assertDontSee('gitea-secret-value'); +}); + +test('admin can view application webhook secrets', function () { + $this->application->update([ + 'git_repository' => 'coollabsio/coolify', + 'git_branch' => 'main', + 'manual_webhook_secret_github' => 'github-secret-value', + 'manual_webhook_secret_gitlab' => 'gitlab-secret-value', + 'manual_webhook_secret_bitbucket' => 'bitbucket-secret-value', + 'manual_webhook_secret_gitea' => 'gitea-secret-value', + ]); + + $this->actingAs($this->admin); + session(['currentTeam' => $this->team]); + + Livewire::test(Webhooks::class, ['resource' => $this->application->fresh()]) + ->assertSet('githubManualWebhookSecret', 'github-secret-value') + ->assertSet('gitlabManualWebhookSecret', 'gitlab-secret-value') + ->assertSet('bitbucketManualWebhookSecret', 'bitbucket-secret-value') + ->assertSet('giteaManualWebhookSecret', 'gitea-secret-value'); +}); + // --- Resource Limits (policy checks, mount requires full resource data) --- test('member cannot update application resource limits', function () {