Skip to content
Snippets Groups Projects
Verified Commit 743ff486 authored by Phawin Khongkhasawan's avatar Phawin Khongkhasawan Committed by GitLab
Browse files

Add resend hook event api

Changelog: added
parent b24bed7a
No related branches found
No related tags found
2 merge requests!164749Enable parallel in test-on-omnibus,!151130Add retry web hook request API
Showing
with 290 additions and 6 deletions
......@@ -20,11 +20,11 @@ def show
end
def retry
if hook_log.url_current?
execute_hook
result = execute_hook
if result.success?
redirect_to after_retry_redirect_path
else
flash[:warning] = _('The hook URL has changed, and this log entry cannot be retried')
flash[:warning] = result.message
redirect_back(fallback_location: after_retry_redirect_path)
end
end
......@@ -38,8 +38,9 @@ def hook_log
# rubocop:enable Gitlab/ModuleWithInstanceVariables
def execute_hook
result = hook.execute(hook_log.request_data, hook_log.trigger, idempotency_key: hook_log.idempotency_key)
result = WebHooks::Events::ResendService.new(hook_log, current_user: current_user).execute
set_hook_execution_notice(result)
result
end
def hide_search_settings
......
# frozen_string_literal: true
module WebHooks
module Events
class ResendService
def initialize(web_hook_log, current_user:)
@web_hook_log = web_hook_log
@current_user = current_user
end
def execute
return unauthorized_response unless authorized?
return url_changed_response unless web_hook_log.url_current?
web_hook_log.web_hook.execute(web_hook_log.request_data, web_hook_log.trigger,
idempotency_key: web_hook_log.idempotency_key)
end
private
def authorized?
case web_hook_log.web_hook.type
when 'ServiceHook'
current_user.can?(:admin_integrations, web_hook_log.web_hook.integration)
else
current_user.can?(:admin_web_hook, web_hook_log.web_hook)
end
end
def unauthorized_response
ServiceResponse.error(message: s_('WebHooks|The current user is not authorized to resend a hook event'))
end
def url_changed_response
ServiceResponse.error(
message: _('The hook URL has changed, and this log entry cannot be retried')
)
end
attr_reader :web_hook_log, :current_user
end
end
end
---
name: web_hook_event_resend_api_endpoint_rate_limit
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/372826
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/151130
rollout_issue_url: # No rollout: This is an ops flag
milestone: '17.4'
group: group::import and integrate
type: ops
default_enabled: true
......@@ -2227,6 +2227,31 @@ GET /groups/:id/hooks/:hook_id/events
]
```
### Resend group hook event
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/151130) in GitLab 17.4.
Resends a specific hook event.
This endpoint has a rate limit of five requests per minute for each hook and authenticated user.
To disable this limit on self-managed GitLab and GitLab Dedicated, an administrator can
[disable the feature flag](../administration/feature_flags.md) named `web_hook_event_resend_api_endpoint_rate_limit`.
```plaintext
POST /groups/:id/hooks/:hook_id/events/:hook_event_id/resend
```
| Attribute | Type | Required | Description |
|-----------|------------------|----------|-------------------------|
| `hook_id` | integer | Yes | The ID of a group hook. |
| `hook_event_id` | integer | Yes | The ID of a hook event. |
```json
{
"response_status": 200
}
```
### Add group hook
Adds a hook to a specified group.
......
......@@ -3226,6 +3226,31 @@ GET /projects/:id/hooks/:hook_id/events
]
```
### Resend project hook event
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/151130) in GitLab 17.4.
Resends a specific hook event.
This endpoint has a rate limit of five requests per minute for each hook and authenticated user.
To disable this limit on self-managed GitLab and GitLab Dedicated, an administrator can
[disable the feature flag](../administration/feature_flags.md) named `web_hook_event_resend_api_endpoint_rate_limit`.
```plaintext
POST /projects/:id/hooks/:hook_id/events/:hook_event_id/resend
```
| Attribute | Type | Required | Description |
|-----------|------------------|----------|--------------------------|
| `hook_id` | integer | Yes | The ID of a project hook. |
| `hook_event_id` | integer | Yes | The ID of a hook event. |
```json
{
"response_status": 200
}
```
### Add project hook
Adds a hook to a specified project.
......
......@@ -225,6 +225,7 @@ To inspect the request and response details of a webhook event:
To send the request again with the same data and the same [`Idempotency-Key` header](#delivery-headers)), select **Resend Request**.
If the webhook URL has changed, you cannot resend the request.
For programmatic resends, please refer to our [API documentation](../../../api/projects.md#resend-project-hook-event).
## Webhook receiver requirements
......
......@@ -158,6 +158,7 @@ def hook_scope
mount ::API::Hooks::TriggerTest, with: {
entity: GroupHook
}
mount ::API::Hooks::ResendHook
end
end
end
......
......@@ -78,6 +78,10 @@ def event_names
let_it_be(:project) { create(:project, :repository, group: group, creator_id: user.id) }
it_behaves_like 'test web-hook endpoint'
it_behaves_like 'resend web-hook event endpoint' do
let(:unauthorized_user) { user3 }
end
it_behaves_like 'get web-hook event endpoint' do
let(:unauthorized_user) { non_admin_user }
end
......
# frozen_string_literal: true
module API
module Entities
class RetryWebhookEvent < Grape::Entity
expose :response_status, documentation: { type: 'integer', example: 200 } do |event|
event.payload[:http_status]
end
end
end
end
# frozen_string_literal: true
module API
module Hooks
# rubocop: disable API/Base -- re-usable module
class ResendHook < ::Grape::API
desc 'Resend a webhook event' do
detail 'Resend a webhook event'
success code: 201
failure [
{ code: 422, message: 'Unprocessable entity' },
{ code: 404, message: 'Not found' },
{ code: 429, message: 'Too many requests' }
]
end
post ":hook_id/events/:hook_log_id/resend" do
hook = find_hook
if Feature.enabled?(:web_hook_event_resend_api_endpoint_rate_limit, Feature.current_request)
check_rate_limit!(:web_hook_event_resend, scope: [hook.parent, current_user])
end
web_hook_log = hook.web_hook_logs.find(params[:hook_log_id])
result = WebHooks::Events::ResendService.new(web_hook_log, current_user: current_user).execute
if result.success?
present result, with: Entities::RetryWebhookEvent
else
render_api_error!(result.message, 422)
end
end
end
# rubocop: enable API/Base
end
end
......@@ -159,6 +159,7 @@ def hook_scope
mount ::API::Hooks::TriggerTest, with: {
entity: ProjectHook
}
mount ::API::Hooks::ResendHook
end
end
end
......
......@@ -44,6 +44,7 @@ def rate_limits # rubocop:disable Metrics/AbcSize
web_hook_calls_mid: { interval: 1.minute },
web_hook_calls_low: { interval: 1.minute },
web_hook_test: { threshold: 5, interval: 1.minute },
web_hook_event_resend: { threshold: 5, interval: 1.minute },
users_get_by_id: { threshold: -> { application_settings.users_get_by_id_limit }, interval: 10.minutes },
username_exists: { threshold: 20, interval: 1.minute },
user_followers: { threshold: 100, interval: 1.minute },
......
......@@ -59812,6 +59812,9 @@ msgstr ""
msgid "WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details."
msgstr ""
 
msgid "WebHooks|The current user is not authorized to resend a hook event"
msgstr ""
msgid "WebIDE|Fork project"
msgstr ""
 
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Projects::Settings::IntegrationHookLogsController do
RSpec.describe Projects::Settings::IntegrationHookLogsController, feature_category: :webhooks do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:integration) { create(:drone_ci_integration, project: project) }
......@@ -42,7 +42,8 @@
subject { post :retry, params: log_params }
it 'executes the hook and redirects to the service form' do
expect_any_instance_of(ServiceHook).to receive(:execute)
expect_any_instance_of(WebHooks::Events::ResendService).to receive(:execute).and_return(instance_double(
ServiceResponse, success?: true))
expect_any_instance_of(described_class).to receive(:set_hook_execution_notice)
expect(subject).to redirect_to(edit_project_settings_integration_path(project, integration))
......
......@@ -69,6 +69,9 @@ def event_names
it_behaves_like 'test web-hook endpoint'
it_behaves_like 'POST webhook API endpoints with a branch filter', '/projects/:id'
it_behaves_like 'PUT webhook API endpoints with a branch filter', '/projects/:id'
it_behaves_like 'resend web-hook event endpoint' do
let(:unauthorized_user) { user3 }
end
it_behaves_like 'get web-hook event endpoint' do
let(:unauthorized_user) { user3 }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WebHooks::Events::ResendService, feature_category: :webhooks do
include StubRequests
let_it_be(:hook) { create(:project_hook) }
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:log) { create(:web_hook_log, web_hook: hook) }
subject(:service) { described_class.new(log, current_user: user) }
describe '#execute' do
context 'when user is authorized' do
before do
hook.project.add_owner(user)
end
context 'when the hook URL has changed' do
before do
allow(log).to receive(:url_current?).and_return(false)
end
it 'returns error' do
service_result = service.execute
expect(service_result).to be_error
expect(service_result.message).to eq("The hook URL has changed, and this log entry cannot be retried")
end
end
context 'when the hook URL has not changed' do
before do
allow(log).to receive(:url_current?).and_return(true)
end
it 'executes successfully' do
stub_full_request(log.web_hook.url, method: :post)
expect(log.web_hook).to receive(:execute).with(log.request_data, log.trigger,
{ idempotency_key: log.idempotency_key }).and_call_original
expect(service.execute).to be_success
end
end
end
context 'when user is unauthorized' do
before do
hook.project.add_developer(user)
end
it 'returns error' do
service_result = service.execute
expect(service_result).to be_error
expect(service_result.message).to eq("The current user is not authorized to resend a hook event")
end
end
end
end
......@@ -835,6 +835,66 @@ def request
end
end
RSpec.shared_examples 'resend web-hook event endpoint' do
include StubRequests
before do
stub_full_request(hook.url, method: :post).to_return(status: 200)
end
let_it_be(:log) { create(:web_hook_log, web_hook: hook, response_status: '404') }
it_behaves_like 'rate limited endpoint', rate_limit_key: :web_hook_event_resend do
let(:current_user) { user }
def request
post api("#{hook_uri}/events/#{log.id}/resend", current_user, admin_mode: current_user.admin?), params: {}
end
context 'when ops flag is disabled' do
before do
stub_feature_flags(web_hook_event_resend_api_endpoint_rate_limit: false)
end
it 'does not block the request' do
request
expect(response).to have_gitlab_http_status(:created)
end
end
end
it 'successfully posts a hook' do
post api("#{hook_uri}/events/#{log.id}/resend", user, admin_mode: user.admin?), params: {}
expect(response).to have_gitlab_http_status(:created)
expect(json_response['response_status']).to eq(200)
end
it "returns a 404 error when web hook log not found" do
post api("#{hook_uri}/events/#{non_existing_record_id}/resend", user, admin_mode: user.admin?), params: {}
expect(response).to have_gitlab_http_status(:not_found)
end
it "return 403 when current user is not authorized" do
post api("#{hook_uri}/events/#{log.id}/resend", unauthorized_user, admin_mode: false), params: {}
expect(response).to have_gitlab_http_status(:forbidden)
end
context 'when hook URL has changed' do
let_it_be(:log) { create(:web_hook_log, web_hook: hook, response_status: '404', url_hash: 'new_hash') }
it "returns 422" do
post api("#{hook_uri}/events/#{log.id}/resend", user, admin_mode: user.admin?), params: {}
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['message']).to eq('The hook URL has changed, and this log entry cannot be retried')
end
end
end
RSpec.shared_examples 'get web-hook event endpoint' do
describe 'hooks events' do
let_it_be(:log_200) { create(:web_hook_log, web_hook: hook) }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment