diff --git a/app/Livewire/Security/PrivateKey/Show.php b/app/Livewire/Security/PrivateKey/Show.php index 664119bad..1b8f26ff2 100644 --- a/app/Livewire/Security/PrivateKey/Show.php +++ b/app/Livewire/Security/PrivateKey/Show.php @@ -76,7 +76,9 @@ private function syncData(bool $toModel = false): void // Sync FROM model (on load/refresh) $this->name = $this->private_key->name; $this->description = $this->private_key->description; - $this->privateKeyValue = $this->private_key->private_key; + $this->privateKeyValue = auth()->user()->can('update', $this->private_key) + ? $this->private_key->private_key + : ''; $this->isGitRelated = $this->private_key->is_git_related; } } diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 001d31c37..2dadd7366 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -122,13 +122,6 @@ public function updatedHtmlUrl(): void } } - public function boot() - { - if ($this->github_app) { - $this->github_app->makeVisible(['client_secret', 'webhook_secret']); - } - } - /** * Sync data between component properties and model * @@ -170,8 +163,9 @@ private function syncData(bool $toModel = false): void $this->appId = $this->github_app->app_id; $this->installationId = $this->github_app->installation_id; $this->clientId = $this->github_app->client_id; - $this->clientSecret = $this->github_app->client_secret; - $this->webhookSecret = $this->github_app->webhook_secret; + $canUpdate = auth()->user()->can('update', $this->github_app); + $this->clientSecret = $canUpdate ? $this->github_app->client_secret : null; + $this->webhookSecret = $canUpdate ? $this->github_app->webhook_secret : null; $this->isSystemWide = $this->github_app->is_system_wide; $this->privateKeyId = $this->github_app->private_key_id; $this->contents = $this->github_app->contents; @@ -231,7 +225,7 @@ public function checkPermissions() syncGithubAppName($this->github_app); GithubAppPermissionJob::dispatchSync($this->github_app); - $this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret'); + $this->github_app->refresh(); $this->syncData(false); $this->isConnected = $this->github_app->isConnected(); $this->name = str($this->github_app->name)->kebab(); @@ -305,7 +299,7 @@ public function mount() try { $github_app_uuid = request()->github_app_uuid; $this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail(); - $this->github_app->makeVisible(['client_secret', 'webhook_secret']); + $this->authorize('view', $this->github_app); $this->privateKeys = PrivateKey::ownedByCurrentTeamCached(); $this->applications = $this->github_app->applications; @@ -420,7 +414,6 @@ public function submit() try { $this->authorize('update', $this->github_app); - $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); $this->organization = normalizeGithubOrganization($this->organization); $this->apiUrl = filled($this->apiUrl) ? $this->apiUrl @@ -442,7 +435,6 @@ public function createGithubAppManually() { $this->authorize('update', $this->github_app); - $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); $this->github_app->app_id = 1234567890; $this->github_app->installation_id = 1234567890; $this->github_app->save(); @@ -457,8 +449,6 @@ public function instantSave() try { $this->authorize('update', $this->github_app); - $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); - $this->syncData(true); $this->github_app->save(); $this->isConnected = $this->github_app->isConnected(); @@ -475,7 +465,6 @@ public function delete() if ($this->github_app->applications->isNotEmpty()) { $this->dispatch('error', 'This source is being used by an application. Please delete all applications first.'); - $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); return; } diff --git a/tests/Feature/Authorization/LivewireStoredSecretAuthorizationTest.php b/tests/Feature/Authorization/LivewireStoredSecretAuthorizationTest.php new file mode 100644 index 000000000..68bb0ac8b --- /dev/null +++ b/tests/Feature/Authorization/LivewireStoredSecretAuthorizationTest.php @@ -0,0 +1,117 @@ + 0]); + Storage::fake('ssh-keys'); + + $this->team = Team::factory()->create(); + $this->owner = User::factory()->create(); + $this->member = User::factory()->create(); + $this->team->members()->attach($this->owner, ['role' => 'owner']); + $this->team->members()->attach($this->member, ['role' => 'member']); +}); + +it('does not serialize a private key for a member who cannot update it', function () { + $privateKeyValue = PrivateKey::generateNewKeyPair('ed25519')['private_key']; + $privateKey = PrivateKey::factory()->create([ + 'team_id' => $this->team->id, + 'private_key' => $privateKeyValue, + ]); + + $this->actingAs($this->member); + session(['currentTeam' => $this->team]); + + Livewire::test(PrivateKeyShow::class, ['private_key_uuid' => $privateKey->uuid]) + ->assertSuccessful() + ->assertSet('privateKeyValue', '') + ->assertDontSee($privateKeyValue); +}); + +it('keeps a private key available to an owner who can update it', function () { + $privateKeyValue = PrivateKey::generateNewKeyPair('ed25519')['private_key']; + $privateKey = PrivateKey::factory()->create([ + 'team_id' => $this->team->id, + 'private_key' => $privateKeyValue, + ]); + + $this->actingAs($this->owner); + session(['currentTeam' => $this->team]); + + Livewire::test(PrivateKeyShow::class, ['private_key_uuid' => $privateKey->uuid]) + ->assertSuccessful() + ->assertSet('privateKeyValue', $privateKeyValue); +}); + +it('does not serialize GitHub App secrets for a member who cannot update it', function () { + $githubApp = createGithubAppWithSecrets($this->team); + + $this->actingAs($this->member); + session(['currentTeam' => $this->team]); + + Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid]) + ->test(GithubAppChange::class) + ->assertSuccessful() + ->assertSet('clientSecret', null) + ->assertSet('webhookSecret', null) + ->assertDontSee('stored-client-secret') + ->assertDontSee('stored-webhook-secret'); +}); + +it('does not serialize secrets from a system-wide GitHub App for another team', function () { + $githubApp = createGithubAppWithSecrets($this->team, isSystemWide: true); + $otherTeam = Team::factory()->create(); + $otherUser = User::factory()->create(); + $otherTeam->members()->attach($otherUser, ['role' => 'owner']); + + $this->actingAs($otherUser); + session(['currentTeam' => $otherTeam]); + + Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid]) + ->test(GithubAppChange::class) + ->assertSuccessful() + ->assertSet('clientSecret', null) + ->assertSet('webhookSecret', null) + ->assertDontSee('stored-client-secret') + ->assertDontSee('stored-webhook-secret'); +}); + +it('keeps GitHub App secrets available to an owner who can update it', function () { + $githubApp = createGithubAppWithSecrets($this->team); + + $this->actingAs($this->owner); + session(['currentTeam' => $this->team]); + + Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid]) + ->test(GithubAppChange::class) + ->assertSuccessful() + ->assertSet('clientSecret', 'stored-client-secret') + ->assertSet('webhookSecret', 'stored-webhook-secret'); +}); + +function createGithubAppWithSecrets(Team $team, bool $isSystemWide = false): GithubApp +{ + return GithubApp::query()->create([ + 'team_id' => $team->id, + 'name' => 'security-test-app', + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'custom_user' => 'git', + 'custom_port' => 22, + 'client_secret' => 'stored-client-secret', + 'webhook_secret' => 'stored-webhook-secret', + 'is_system_wide' => $isSystemWide, + ]); +}