diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb index 6f5475a4a787c17540b1e989661b96e79a01be8f..810801d420920d2b0b0fe03d22107dd173e9ec47 100644 --- a/app/controllers/admin/hooks_controller.rb +++ b/app/controllers/admin/hooks_controller.rb @@ -8,40 +8,6 @@ class Admin::HooksController < Admin::ApplicationController feature_category :integrations urgency :low, [:test] - def index - @hooks = SystemHook.all.load - @hook = SystemHook.new - end - - def create - @hook = SystemHook.new(hook_params.to_h) - - if @hook.save - redirect_to admin_hooks_path, notice: _('Hook was successfully created.') - else - @hooks = SystemHook.all - render :index - end - end - - def edit - end - - def update - if hook.update(hook_params) - flash[:notice] = _('System hook was successfully updated.') - redirect_to admin_hooks_path - else - render 'edit' - end - end - - def destroy - destroy_hook(hook) - - redirect_to admin_hooks_path, status: :found - end - def test result = TestHooks::SystemService.new(hook, current_user, params[:trigger]).execute @@ -52,6 +18,10 @@ def test private + def relation + SystemHook + end + def hook @hook ||= SystemHook.find(params[:id]) end @@ -60,12 +30,11 @@ def hook_logs @hook_logs ||= hook.web_hook_logs.recent.page(params[:page]) end - def hook_params - params.require(:hook).permit( - :enable_ssl_verification, - :token, - :url, - *SystemHook.triggers.values - ) + def hook_param_names + %i[enable_ssl_verification token url] + end + + def trigger_values + SystemHook.triggers.values end end diff --git a/app/controllers/concerns/integrations/hooks_execution.rb b/app/controllers/concerns/integrations/hooks_execution.rb index 6a9d3d51f9bf21904426755547a146fd4e994367..fb26840168f092d2e47c57723a97fa6b6f9ed5c3 100644 --- a/app/controllers/concerns/integrations/hooks_execution.rb +++ b/app/controllers/concerns/integrations/hooks_execution.rb @@ -3,8 +3,68 @@ module Integrations::HooksExecution extend ActiveSupport::Concern + included do + attr_writer :hooks, :hook + end + + def index + self.hooks = relation.select(&:persisted?) + self.hook = relation.new + end + + def create + self.hook = relation.new(hook_params) + hook.save + + unless hook.valid? + self.hooks = relation.select(&:persisted?) + flash[:alert] = hook.errors.full_messages.join.html_safe + end + + redirect_to action: :index + end + + def update + if hook.update(hook_params) + flash[:notice] = _('Hook was successfully updated.') + redirect_to action: :index + else + render 'edit' + end + end + + def destroy + destroy_hook(hook) + + redirect_to action: :index, status: :found + end + + def edit + redirect_to(action: :index) unless hook + end + private + def hook_params + permitted = hook_param_names + trigger_values + permitted << { url_variables: [:key, :value] } + + ps = params.require(:hook).permit(*permitted).to_h + + ps[:url_variables] = ps[:url_variables].to_h { [_1[:key], _1[:value].presence] } if ps.key?(:url_variables) + + if action_name == 'update' && ps.key?(:url_variables) + supplied = ps[:url_variables] + ps[:url_variables] = hook.url_variables.merge(supplied).compact + end + + ps + end + + def hook_param_names + %i[enable_ssl_verification token url push_events_branch_filter] + end + def destroy_hook(hook) result = WebHooks::DestroyService.new(current_user).execute(hook) diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 99eba32e00faf56bf7460f6dd35276a56cc574cd..50f388324f1696541eb898cd6d6620e959144d67 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -15,52 +15,21 @@ class Projects::HooksController < Projects::ApplicationController feature_category :integrations urgency :low, [:test] - def index - @hooks = @project.hooks.load - @hook = ProjectHook.new - end - - def create - @hook = @project.hooks.new(hook_params) - @hook.save - - unless @hook.valid? - @hooks = @project.hooks.select(&:persisted?) - flash[:alert] = @hook.errors.full_messages.join.html_safe - end - - redirect_to action: :index - end - - def edit - redirect_to(action: :index) unless hook - end - - def update - if hook.update(hook_params) - flash[:notice] = _('Hook was successfully updated.') - redirect_to action: :index - else - render 'edit' - end - end - def test - result = TestHooks::ProjectService.new(hook, current_user, params[:trigger]).execute + trigger = params.fetch(:trigger, ::ProjectHook.triggers.each_value.first.to_s) + result = TestHooks::ProjectService.new(hook, current_user, trigger).execute set_hook_execution_notice(result) redirect_back_or_default(default: { action: :index }) end - def destroy - destroy_hook(hook) + private - redirect_to action: :index, status: :found + def relation + @project.hooks end - private - def hook @hook ||= @project.hooks.find(params[:id]) end @@ -69,13 +38,7 @@ def hook_logs @hook_logs ||= hook.web_hook_logs.recent.page(params[:page]) end - def hook_params - params.require(:hook).permit( - :enable_ssl_verification, - :token, - :url, - :push_events_branch_filter, - *ProjectHook.triggers.values - ) + def trigger_values + ProjectHook.triggers.values end end diff --git a/ee/app/controllers/groups/hooks_controller.rb b/ee/app/controllers/groups/hooks_controller.rb index 58dd5dd6fcb5b343a441c1ecd0734ee8f4698be9..febc3b558bf8bdf281888991d6195fd2f5dbc2ed 100644 --- a/ee/app/controllers/groups/hooks_controller.rb +++ b/ee/app/controllers/groups/hooks_controller.rb @@ -7,7 +7,7 @@ class Groups::HooksController < Groups::ApplicationController before_action :group before_action :authorize_admin_group! before_action :check_group_webhooks_available! - before_action :set_hook, only: [:edit, :update, :test, :destroy] + before_action :hook, only: [:edit, :update, :test, :destroy] before_action -> { check_rate_limit!(:group_testing_hook, scope: [@group, current_user]) }, only: :test respond_to :html @@ -17,38 +17,9 @@ class Groups::HooksController < Groups::ApplicationController feature_category :integrations urgency :low, [:test] - def index - @hooks = @group.hooks.load - @hook = GroupHook.new - end - - def create - @hook = @group.hooks.new(hook_params) - @hook.save - - if @hook.valid? - redirect_to group_hooks_path(@group) - else - @hooks = @group.hooks.select(&:persisted?) - render :index - end - end - - def edit - end - - def update - if @hook.update(hook_params) - flash[:notice] = _('Hook was successfully updated.') - redirect_to group_hooks_path(@group) - else - render 'edit' - end - end - def test if @group.first_non_empty_project - service = TestHooks::ProjectService.new(@hook, current_user, params[:trigger] || 'push_events') + service = TestHooks::ProjectService.new(hook, current_user, params[:trigger] || 'push_events') service.project = @group.first_non_empty_project result = service.execute @@ -60,26 +31,18 @@ def test redirect_back_or_default(default: { action: 'index' }) end - def destroy - destroy_hook(@hook) + private - redirect_to group_hooks_path(@group), status: :found + def relation + @group.hooks end - private - - def set_hook + def hook @hook ||= @group.hooks.find(params[:id]) end - def hook_params - params.require(:hook).permit( - :enable_ssl_verification, - :token, - :url, - :push_events_branch_filter, - *GroupHook.triggers.values - ) + def trigger_values + GroupHook.triggers.values end def check_group_webhooks_available! diff --git a/ee/spec/controllers/groups/hooks_controller_spec.rb b/ee/spec/controllers/groups/hooks_controller_spec.rb index 368c8c90a49d174b9fd626ec7faab3c569be215f..4e9a0086ff22ed6bf88f51fe0f8239c6195c2a2f 100644 --- a/ee/spec/controllers/groups/hooks_controller_spec.rb +++ b/ee/spec/controllers/groups/hooks_controller_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' RSpec.describe Groups::HooksController do - let(:user) { create(:user) } - let(:group) { create(:group) } + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } before do group.add_owner(user) @@ -44,14 +44,30 @@ enable_ssl_verification: true, token: 'TEST TOKEN', url: 'http://example.com', - push_events_branch_filter: 'filter-branch' + push_events_branch_filter: 'filter-branch', + url_variables: [ + { key: 'token', value: 'shh-secret!' } + ] } post :create, params: { group_id: group.to_param, hook: hook_params } expect(response).to have_gitlab_http_status(:found) expect(group.hooks.size).to eq(1) - expect(group.hooks.first).to have_attributes(hook_params) + expect(group.hooks.first).to have_attributes(hook_params.except(:url_variables)) + expect(group.hooks.first.url_variables).to eq('token' => 'shh-secret!') + end + + it 'alerts the user if the new hook is invalid' do + hook_params = { + token: "TEST\nTOKEN", + url: "http://example.com" + } + + post :create, params: { group_id: group.to_param, hook: hook_params } + + expect(flash[:alert]).to be_present + expect(group.hooks.count).to eq(0) end end @@ -68,7 +84,7 @@ end describe 'PATCH #update' do - let(:hook) { create(:group_hook, group: group) } + let_it_be(:hook) { create(:group_hook, group: group) } context 'valid params' do let(:hook_params) do @@ -88,17 +104,28 @@ deployment_events: true, releases_events: true, member_events: true, - subgroup_events: true + subgroup_events: true, + url_variables: [ + { key: 'a', value: 'alpha' }, + { key: 'b', value: nil }, + { key: 'c', value: 'gamma' } + ] } end - it 'is successfull' do + it 'is successful' do + hook.update!(url_variables: { 'a' => 'x', 'b' => 'z' }) + patch :update, params: { group_id: group.to_param, id: hook, hook: hook_params } expect(response).to have_gitlab_http_status(:found) expect(response).to redirect_to(group_hooks_path(group)) expect(group.hooks.size).to eq(1) - expect(group.hooks.first).to have_attributes(hook_params) + expect(hook.reload).to have_attributes(hook_params.except(:url_variables)) + expect(hook.url_variables).to eq( + 'a' => 'alpha', + 'c' => 'gamma' + ) end end @@ -113,6 +140,7 @@ patch :update, params: { group_id: group.to_param, id: hook, hook: hook_params } expect(response).to have_gitlab_http_status(:ok) + expect(flash[:notice]).to be_nil expect(response).to render_template(:edit) expect(group.hooks.size).to eq(1) expect(group.hooks.first).not_to have_attributes(hook_params) @@ -120,7 +148,7 @@ end end - describe 'POST #test' do + describe 'POST #test', :clean_gitlab_redis_shared_state do let(:hook) { create(:group_hook, group: group) } context 'when group does not have a project' do diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9f19cfd563a24c4eb1886eca8c007529ab8aed37..b4b67cd21bf188e4001fbbf7c063ea0721ba2ccb 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -18989,9 +18989,6 @@ msgstr "" msgid "Hook execution failed. Ensure the group has a project with commits." msgstr "" -msgid "Hook was successfully created." -msgstr "" - msgid "Hook was successfully updated." msgstr "" @@ -37350,9 +37347,6 @@ msgstr "" msgid "System header and footer" msgstr "" -msgid "System hook was successfully updated." -msgstr "" - msgid "System hooks are triggered on sets of events like creating a project or adding an SSH key. You can also enable extra triggers, such as push events." msgstr "" diff --git a/spec/controllers/admin/hooks_controller_spec.rb b/spec/controllers/admin/hooks_controller_spec.rb index 17c4222530d8181357e4518240a26889997c8c89..14f4a2f40e7295d267d6248359f7beff7b25a4d5 100644 --- a/spec/controllers/admin/hooks_controller_spec.rb +++ b/spec/controllers/admin/hooks_controller_spec.rb @@ -17,16 +17,46 @@ url: "http://example.com", push_events: true, - tag_push_events: true, + tag_push_events: false, repository_update_events: true, - merge_requests_events: true + merge_requests_events: false, + url_variables: [{ key: 'token', value: 'some secret value' }] } post :create, params: { hook: hook_params } expect(response).to have_gitlab_http_status(:found) expect(SystemHook.all.size).to eq(1) - expect(SystemHook.first).to have_attributes(hook_params) + expect(SystemHook.first).to have_attributes(hook_params.except(:url_variables)) + expect(SystemHook.first).to have_attributes(url_variables: { 'token' => 'some secret value' }) + end + end + + describe 'POST #update' do + let!(:hook) { create(:system_hook) } + + it 'sets all parameters' do + hook.update!(url_variables: { 'foo' => 'bar', 'baz' => 'woo' }) + + hook_params = { + url: 'http://example.com/{baz}?token={token}', + enable_ssl_verification: false, + url_variables: [ + { key: 'token', value: 'some secret value' }, + { key: 'foo', value: nil } + ] + } + + put :update, params: { id: hook.id, hook: hook_params } + + hook.reload + + expect(response).to have_gitlab_http_status(:found) + expect(flash[:notice]).to include('successfully updated') + expect(hook).to have_attributes(hook_params.except(:url_variables)) + expect(hook).to have_attributes( + url_variables: { 'token' => 'some secret value', 'baz' => 'woo' } + ) end end diff --git a/spec/controllers/projects/hooks_controller_spec.rb b/spec/controllers/projects/hooks_controller_spec.rb index ebcf35a7ecd08eff23723b750ecefcf37580b923..a275bc2863151bb4a73d2ce658110b3300641d99 100644 --- a/spec/controllers/projects/hooks_controller_spec.rb +++ b/spec/controllers/projects/hooks_controller_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Projects::HooksController do + include AfterNextHelpers + let_it_be(:project) { create(:project) } let(:user) { project.first_owner } @@ -20,6 +22,36 @@ end end + describe '#update' do + let_it_be(:hook) { create(:project_hook, project: project) } + + let(:params) do + { namespace_id: project.namespace, project_id: project, id: hook.id } + end + + it 'adds, updates and deletes URL variables' do + hook.update!(url_variables: { 'a' => 'bar', 'b' => 'woo' }) + + params[:hook] = { + url_variables: [ + { key: 'a', value: 'updated' }, + { key: 'b', value: nil }, + { key: 'c', value: 'new' } + ] + } + + put :update, params: params + + expect(response).to have_gitlab_http_status(:found) + expect(flash[:notice]).to include('successfully updated') + + expect(hook.reload.url_variables).to eq( + 'a' => 'updated', + 'c' => 'new' + ) + end + end + describe '#edit' do let_it_be(:hook) { create(:project_hook, project: project) } @@ -87,14 +119,30 @@ def it_renders_correctly job_events: true, pipeline_events: true, wiki_page_events: true, - deployment_events: true + deployment_events: true, + + url_variables: [{ key: 'token', value: 'some secret value' }] } post :create, params: { namespace_id: project.namespace, project_id: project, hook: hook_params } expect(response).to have_gitlab_http_status(:found) - expect(ProjectHook.all.size).to eq(1) - expect(ProjectHook.first).to have_attributes(hook_params) + expect(flash[:alert]).to be_blank + expect(ProjectHook.count).to eq(1) + expect(ProjectHook.first).to have_attributes(hook_params.except(:url_variables)) + expect(ProjectHook.first).to have_attributes(url_variables: { 'token' => 'some secret value' }) + end + + it 'alerts the user if the new hook is invalid' do + hook_params = { + token: "TEST\nTOKEN", + url: "http://example.com" + } + + post :create, params: { namespace_id: project.namespace, project_id: project, hook: hook_params } + + expect(flash[:alert]).to be_present + expect(ProjectHook.count).to eq(0) end end @@ -109,6 +157,45 @@ def it_renders_correctly describe '#test' do let(:hook) { create(:project_hook, project: project) } + context 'when the hook executes successfully' do + before do + stub_request(:post, hook.url).to_return(status: 200) + end + + it 'informs the user' do + post :test, params: { namespace_id: project.namespace, project_id: project, id: hook } + + expect(flash[:notice]).to include('executed successfully') + expect(flash[:notice]).to include('HTTP 200') + end + end + + context 'when the hook runs, but fails' do + before do + stub_request(:post, hook.url).to_return(status: 400) + end + + it 'informs the user' do + post :test, params: { namespace_id: project.namespace, project_id: project, id: hook } + + expect(flash[:alert]).to include('executed successfully but') + expect(flash[:alert]).to include('HTTP 400') + end + end + + context 'when the hook fails completely' do + before do + allow_next(::TestHooks::ProjectService) + .to receive(:execute).and_return({ message: 'All is woe' }) + end + + it 'informs the user' do + post :test, params: { namespace_id: project.namespace, project_id: project, id: hook } + + expect(flash[:alert]).to include('failed: All is woe') + end + end + context 'when the endpoint receives requests above the limit', :freeze_time, :clean_gitlab_redis_rate_limiting do before do allow(Gitlab::ApplicationRateLimiter).to receive(:rate_limits)