From da8665330bc46ec8e13d650ab1dbc2bedf19b05a Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:24:53 +0200 Subject: [PATCH] feat(security): add integration token editing and rotation --- .../Security/IntegrationTokenEditor.php | 114 ++++++++++++++++++ app/Policies/IntegrationTokenPolicy.php | 10 ++ .../integration-token-editor.blade.php | 52 ++++++++ .../security/integration-tokens.blade.php | 70 ++++++----- .../Security/IntegrationTokenFormTest.php | 110 +++++++++++++++++ 5 files changed, 329 insertions(+), 27 deletions(-) create mode 100644 app/Livewire/Security/IntegrationTokenEditor.php create mode 100644 resources/views/livewire/security/integration-token-editor.blade.php diff --git a/app/Livewire/Security/IntegrationTokenEditor.php b/app/Livewire/Security/IntegrationTokenEditor.php new file mode 100644 index 0000000000..453a7e8ae8 --- /dev/null +++ b/app/Livewire/Security/IntegrationTokenEditor.php @@ -0,0 +1,114 @@ +integrationToken = IntegrationToken::ownedByCurrentTeam() + ->whereUuid($integration_token_uuid) + ->firstOrFail(); + + $this->authorize('view', $this->integrationToken); + + $this->name = $this->integrationToken->name; + $this->capabilities = $this->integrationToken->capabilities; + } + + protected function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'newToken' => ['nullable', 'string'], + 'capabilities' => ['required', 'array', 'min:1'], + 'capabilities.*' => ['required', 'in:dns'], + ]; + } + + protected function messages(): array + { + return [ + 'capabilities.required' => 'Select at least one capability.', + 'capabilities.min' => 'Select at least one capability.', + ]; + } + + public function save(CloudflareTokenValidator $validator): void + { + $this->authorize('update', $this->integrationToken); + $validated = $this->validate(); + $token = filled($validated['newToken']) ? $validated['newToken'] : $this->integrationToken->token; + $capabilitiesChanged = collect($validated['capabilities'])->sort()->values()->all() + !== collect($this->integrationToken->capabilities)->sort()->values()->all(); + + try { + if ((filled($validated['newToken']) || $capabilitiesChanged) + && ! $validator->validate($token, $validated['capabilities'])) { + $this->dispatch('error', 'The token could not access the selected Cloudflare capabilities. Check its permissions and zone resources.'); + + return; + } + + $updates = [ + 'name' => $validated['name'], + 'capabilities' => $validated['capabilities'], + ]; + + if (filled($validated['newToken'])) { + $updates['token'] = $validated['newToken']; + } + + $this->integrationToken->update($updates); + $this->newToken = ''; + + auditLog('ui.integration_token.updated', [ + 'team_id' => currentTeam()->id, + 'integration_token_uuid' => $this->integrationToken->uuid, + 'integration_token_name' => $this->integrationToken->name, + 'provider' => $this->integrationToken->provider, + 'rotated' => array_key_exists('token', $updates), + ]); + + $this->dispatch( + 'integration-token-updated', + uuid: $this->integrationToken->uuid, + name: $this->integrationToken->name, + capabilities: $this->integrationToken->capabilities, + ); + $this->dispatch('success', 'Integration token updated successfully.'); + } catch (\Throwable $e) { + handleError($e, $this); + } + } + + public function delete(string $password = ''): void + { + $this->authorize('delete', $this->integrationToken); + $this->integrationToken->delete(); + + $this->dispatch('integration-token-deleted', uuid: $this->integrationToken->uuid); + $this->dispatch('close-modal'); + $this->dispatch('success', 'Integration token deleted successfully.'); + } + + public function render() + { + return view('livewire.security.integration-token-editor'); + } +} diff --git a/app/Policies/IntegrationTokenPolicy.php b/app/Policies/IntegrationTokenPolicy.php index 8dfc98eb8d..309c8167f2 100644 --- a/app/Policies/IntegrationTokenPolicy.php +++ b/app/Policies/IntegrationTokenPolicy.php @@ -17,6 +17,16 @@ class IntegrationTokenPolicy return $user->isAdmin(); } + public function view(User $user, IntegrationToken $integrationToken): bool + { + return $user->isAdmin() && $integrationToken->team_id === currentTeam()->id; + } + + public function update(User $user, IntegrationToken $integrationToken): bool + { + return $user->isAdmin() && $integrationToken->team_id === currentTeam()->id; + } + public function delete(User $user, IntegrationToken $integrationToken): bool { return $user->isAdmin() && $integrationToken->team_id === currentTeam()->id; diff --git a/resources/views/livewire/security/integration-token-editor.blade.php b/resources/views/livewire/security/integration-token-editor.blade.php new file mode 100644 index 0000000000..b7e53dbc7c --- /dev/null +++ b/resources/views/livewire/security/integration-token-editor.blade.php @@ -0,0 +1,52 @@ +