Commit ec2710d0 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'feature/backport-safewebhooks' into 'master'

Backported minimal safewebhook implementation to GitLab CE

This brings a minimal implementation for gitlab-org/gitlab-ce#13478
backported from EE (gitlab-org/gitlab-ee!334).

Also added UI to configure Secret Token

Fixes #15365.

See merge request !3940
parents 15a2c558 00ced598
...@@ -39,6 +39,12 @@ class Admin::HooksController < Admin::ApplicationController ...@@ -39,6 +39,12 @@ class Admin::HooksController < Admin::ApplicationController
end end
def hook_params def hook_params
params.require(:hook).permit(:url, :enable_ssl_verification, :push_events, :tag_push_events) params.require(:hook).permit(
:enable_ssl_verification,
:push_events,
:tag_push_events,
:token,
:url
)
end end
end end
...@@ -52,8 +52,16 @@ class Projects::HooksController < Projects::ApplicationController ...@@ -52,8 +52,16 @@ class Projects::HooksController < Projects::ApplicationController
end end
def hook_params def hook_params
params.require(:hook).permit(:url, :push_events, :issues_events, params.require(:hook).permit(
:merge_requests_events, :tag_push_events, :note_events, :build_events,
:build_events, :enable_ssl_verification) :enable_ssl_verification,
:issues_events,
:merge_requests_events,
:note_events,
:push_events,
:tag_push_events,
:token,
:url
)
end end
end end
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
# note_events :boolean default(FALSE), not null # note_events :boolean default(FALSE), not null
# enable_ssl_verification :boolean default(TRUE) # enable_ssl_verification :boolean default(TRUE)
# build_events :boolean default(FALSE), not null # build_events :boolean default(FALSE), not null
# token :string
# #
class ProjectHook < WebHook class ProjectHook < WebHook
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
# note_events :boolean default(FALSE), not null # note_events :boolean default(FALSE), not null
# enable_ssl_verification :boolean default(TRUE) # enable_ssl_verification :boolean default(TRUE)
# build_events :boolean default(FALSE), not null # build_events :boolean default(FALSE), not null
# token :string
# #
class ServiceHook < WebHook class ServiceHook < WebHook
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
# note_events :boolean default(FALSE), not null # note_events :boolean default(FALSE), not null
# enable_ssl_verification :boolean default(TRUE) # enable_ssl_verification :boolean default(TRUE)
# build_events :boolean default(FALSE), not null # build_events :boolean default(FALSE), not null
# token :string
# #
class SystemHook < WebHook class SystemHook < WebHook
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
# note_events :boolean default(FALSE), not null # note_events :boolean default(FALSE), not null
# enable_ssl_verification :boolean default(TRUE) # enable_ssl_verification :boolean default(TRUE)
# build_events :boolean default(FALSE), not null # build_events :boolean default(FALSE), not null
# token :string
# #
class WebHook < ActiveRecord::Base class WebHook < ActiveRecord::Base
...@@ -43,23 +44,17 @@ class WebHook < ActiveRecord::Base ...@@ -43,23 +44,17 @@ class WebHook < ActiveRecord::Base
if parsed_url.userinfo.blank? if parsed_url.userinfo.blank?
response = WebHook.post(url, response = WebHook.post(url,
body: data.to_json, body: data.to_json,
headers: { headers: build_headers(hook_name),
"Content-Type" => "application/json",
"X-Gitlab-Event" => hook_name.singularize.titleize
},
verify: enable_ssl_verification) verify: enable_ssl_verification)
else else
post_url = url.gsub("#{parsed_url.userinfo}@", "") post_url = url.gsub("#{parsed_url.userinfo}@", '')
auth = { auth = {
username: CGI.unescape(parsed_url.user), username: CGI.unescape(parsed_url.user),
password: CGI.unescape(parsed_url.password), password: CGI.unescape(parsed_url.password),
} }
response = WebHook.post(post_url, response = WebHook.post(post_url,
body: data.to_json, body: data.to_json,
headers: { headers: build_headers(hook_name),
"Content-Type" => "application/json",
"X-Gitlab-Event" => hook_name.singularize.titleize
},
verify: enable_ssl_verification, verify: enable_ssl_verification,
basic_auth: auth) basic_auth: auth)
end end
...@@ -73,4 +68,15 @@ class WebHook < ActiveRecord::Base ...@@ -73,4 +68,15 @@ class WebHook < ActiveRecord::Base
def async_execute(data, hook_name) def async_execute(data, hook_name)
Sidekiq::Client.enqueue(ProjectWebHookWorker, id, data, hook_name) Sidekiq::Client.enqueue(ProjectWebHookWorker, id, data, hook_name)
end end
private
def build_headers(hook_name)
headers = {
'Content-Type' => 'application/json',
'X-Gitlab-Event' => hook_name.singularize.titleize
}
headers['X-Gitlab-Token'] = token if token.present?
headers
end
end end
...@@ -13,9 +13,15 @@ ...@@ -13,9 +13,15 @@
= form_errors(@hook) = form_errors(@hook)
.form-group .form-group
= f.label :url, "URL:", class: 'control-label' = f.label :url, 'URL', class: 'control-label'
.col-sm-10 .col-sm-10
= f.text_field :url, class: "form-control" = f.text_field :url, class: 'form-control'
.form-group
= f.label :token, 'Secret Token', class: 'control-label'
.col-sm-10
= f.text_field :token, class: 'form-control'
%p.help-block
Use this token to validate received payloads
.form-group .form-group
= f.label :url, "Trigger", class: 'control-label' = f.label :url, "Trigger", class: 'control-label'
.col-sm-10.prepend-top-10 .col-sm-10.prepend-top-10
......
...@@ -15,6 +15,11 @@ ...@@ -15,6 +15,11 @@
.form-group .form-group
= f.label :url, "URL", class: "label-light" = f.label :url, "URL", class: "label-light"
= f.text_field :url, class: "form-control", placeholder: "http://example.com/trigger-ci.json" = f.text_field :url, class: "form-control", placeholder: "http://example.com/trigger-ci.json"
.form-group
= f.label :token, "Secret Token", class: 'label-light'
= f.text_field :token, class: "form-control", placeholder: ''
%p.help-block
Use this token to validate received payloads
.form-group .form-group
= f.label :url, "Trigger", class: "label-light" = f.label :url, "Trigger", class: "label-light"
%div %div
......
class AddTokenToWebHooks < ActiveRecord::Migration
def change
add_column :web_hooks, :token, :string
end
end
...@@ -1025,6 +1025,7 @@ ActiveRecord::Schema.define(version: 20160421130527) do ...@@ -1025,6 +1025,7 @@ ActiveRecord::Schema.define(version: 20160421130527) do
t.boolean "enable_ssl_verification", default: true t.boolean "enable_ssl_verification", default: true
t.boolean "build_events", default: false, null: false t.boolean "build_events", default: false, null: false
t.boolean "wiki_page_events", default: false, null: false t.boolean "wiki_page_events", default: false, null: false
t.string "token"
end end
add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree
......
FactoryGirl.define do FactoryGirl.define do
factory :project_hook do factory :project_hook do
url { FFaker::Internet.uri('http') } url { FFaker::Internet.uri('http') }
trait :token do
token { SecureRandom.hex(10) }
end
end end
end end
...@@ -43,51 +43,65 @@ describe WebHook, models: true do ...@@ -43,51 +43,65 @@ describe WebHook, models: true do
end end
describe "execute" do describe "execute" do
let(:project) { create(:project) }
let(:project_hook) { create(:project_hook) }
before(:each) do before(:each) do
@project_hook = create(:project_hook) project.hooks << [project_hook]
@project = create(:project)
@project.hooks << [@project_hook]
@data = { before: 'oldrev', after: 'newrev', ref: 'ref' } @data = { before: 'oldrev', after: 'newrev', ref: 'ref' }
WebMock.stub_request(:post, @project_hook.url) WebMock.stub_request(:post, project_hook.url)
end
context 'when token is defined' do
let(:project_hook) { create(:project_hook, :token) }
it 'POSTs to the webhook URL' do
project_hook.execute(@data, 'push_hooks')
expect(WebMock).to have_requested(:post, project_hook.url).with(
headers: { 'Content-Type' => 'application/json',
'X-Gitlab-Event' => 'Push Hook',
'X-Gitlab-Token' => project_hook.token }
).once
end
end end
it "POSTs to the webhook URL" do it "POSTs to the webhook URL" do
@project_hook.execute(@data, 'push_hooks') project_hook.execute(@data, 'push_hooks')
expect(WebMock).to have_requested(:post, @project_hook.url).with( expect(WebMock).to have_requested(:post, project_hook.url).with(
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Push Hook' } headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Push Hook' }
).once ).once
end end
it "POSTs the data as JSON" do it "POSTs the data as JSON" do
@project_hook.execute(@data, 'push_hooks') project_hook.execute(@data, 'push_hooks')
expect(WebMock).to have_requested(:post, @project_hook.url).with( expect(WebMock).to have_requested(:post, project_hook.url).with(
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Push Hook' } headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Push Hook' }
).once ).once
end end
it "catches exceptions" do it "catches exceptions" do
expect(WebHook).to receive(:post).and_raise("Some HTTP Post error") expect(WebHook).to receive(:post).and_raise("Some HTTP Post error")
expect { @project_hook.execute(@data, 'push_hooks') }.to raise_error(RuntimeError) expect { project_hook.execute(@data, 'push_hooks') }.to raise_error(RuntimeError)
end end
it "handles SSL exceptions" do it "handles SSL exceptions" do
expect(WebHook).to receive(:post).and_raise(OpenSSL::SSL::SSLError.new('SSL error')) expect(WebHook).to receive(:post).and_raise(OpenSSL::SSL::SSLError.new('SSL error'))
expect(@project_hook.execute(@data, 'push_hooks')).to eq([false, 'SSL error']) expect(project_hook.execute(@data, 'push_hooks')).to eq([false, 'SSL error'])
end end
it "handles 200 status code" do it "handles 200 status code" do
WebMock.stub_request(:post, @project_hook.url).to_return(status: 200, body: "Success") WebMock.stub_request(:post, project_hook.url).to_return(status: 200, body: "Success")
expect(@project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success']) expect(project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success'])
end end
it "handles 2xx status codes" do it "handles 2xx status codes" do
WebMock.stub_request(:post, @project_hook.url).to_return(status: 201, body: "Success") WebMock.stub_request(:post, project_hook.url).to_return(status: 201, body: "Success")
expect(@project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success']) expect(project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success'])
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment