Skip to content
Snippets Groups Projects
Commit d18fdf46 authored by Alex Kalderimis's avatar Alex Kalderimis Committed by Terri Chu
Browse files

Backend support for manipulating web-hook variables

Hook controllers accept new parameters to set url variables.

Changelog: changed
parent 4f59fdc6
No related branches found
No related tags found
1 merge request!89933Web Hook encrypted URL variables: Backend support (Web)
......@@ -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
......@@ -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)
......
......@@ -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
......@@ -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!
......
......@@ -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
......
......@@ -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 ""
 
......
......@@ -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
......
......@@ -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)
......
  • Toggle commit list
  • Zhiyuan Lu @luzhiyuan.deer ·
    Guest

    Hi Alex, I am Zhiyuan, I will be responsible for the development of an issue about webhook.

    I would like to first understand how webhooks work. Do you know where the documentation documents how it works, like what services are used, what interfaces are used?

    I tried to find some documentation, but they didn't help me much:

    1. Official documentation. There is only a description of the use of this function here, not the technical implementation principle
    2. Search for issue using keyword webhook. I get many, many results and it's a pain to find what I need
    3. Search for webhook in git log. Also got too many results

    Looking forward to your reply, thanks :pray:

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