diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb
index 98ebe07fa..3713eea77 100644
--- a/app/controllers/webhooks_controller.rb
+++ b/app/controllers/webhooks_controller.rb
@@ -4,6 +4,7 @@ class WebhooksController < ApplicationController
self.main_menu = false
before_action :require_login
+ before_action :check_enabled
before_action :authorize
before_action :find_webhook, only: [:edit, :update, :destroy]
@@ -62,4 +63,8 @@ class WebhooksController < ApplicationController
def authorize
deny_access unless User.current.allowed_to?(:use_webhooks, nil, global: true)
end
+
+ def check_enabled
+ render_404 unless Webhook.enabled?
+ end
end
diff --git a/app/models/webhook.rb b/app/models/webhook.rb
index dd18f579e..cb0a3383d 100644
--- a/app/models/webhook.rb
+++ b/app/models/webhook.rb
@@ -42,9 +42,15 @@ class Webhook < ApplicationRecord
before_validation ->(hook){ hook.projects = hook.projects.to_a & hook.setable_projects }
+ def self.enabled?
+ Redmine::Configuration['webhooks']
+ end
+
# Triggers the given event for the given object, scheduling qualifying hooks
# to be called.
def self.trigger(event, object)
+ return unless enabled?
+
hooks_for(event, object).each do |hook|
payload = hook.payload(event, object)
WebhookJob.perform_later(hook.id, payload.to_json)
diff --git a/app/views/my/account.html.erb b/app/views/my/account.html.erb
index b516dd842..4073a3a0c 100644
--- a/app/views/my/account.html.erb
+++ b/app/views/my/account.html.erb
@@ -1,7 +1,7 @@
<%= additional_emails_link(@user) %>
<%= link_to(sprite_icon('key', l(:button_change_password)), { :action => 'password'}, :class => 'icon icon-passwd') if @user.change_password_allowed? %>
-<%= link_to sprite_icon('webhook', l(:label_webhook_plural)), webhooks_path, class: 'icon icon-webhook' if @user.allowed_to?(:use_webhooks, nil, global: true) %>
+<%= link_to sprite_icon('webhook', l(:label_webhook_plural)), webhooks_path, class: 'icon icon-webhook' if Webhook.enabled? && @user.allowed_to?(:use_webhooks, nil, global: true) %>
<%= link_to(sprite_icon('apps', l('label_oauth_authorized_application_plural')), oauth_authorized_applications_path, :class => 'icon icon-applications') if Setting.rest_api_enabled? %>
<%= call_hook(:view_my_account_contextual, :user => @user)%>
diff --git a/config/configuration.yml.example b/config/configuration.yml.example
index 4e8444ca9..13733a860 100644
--- a/config/configuration.yml.example
+++ b/config/configuration.yml.example
@@ -226,6 +226,11 @@ default:
# Webhooks
#
+ # Enables or disables the Webhooks feature (enabled by default).
+ # When disabled, all related UI will be hidden, and no webhooks will be triggered,
+ # but existing webhook data will be retained.
+ # webhooks: true
+ #
# An optional list of hosts and/or IP addresses and/or IP networks which
# should NOT be valid as webhook targets. You can add your internal IPs and
# hostnames here to avoid possible SSRF attacks.
diff --git a/lib/redmine/configuration.rb b/lib/redmine/configuration.rb
index 1c7fdb3a5..d139f3a3e 100644
--- a/lib/redmine/configuration.rb
+++ b/lib/redmine/configuration.rb
@@ -27,7 +27,8 @@ module Redmine
'email_delivery' => nil,
'max_concurrent_ajax_uploads' => 2,
'common_mark_enable_hardbreaks' => true,
- 'thumbnails_generation_timeout' => 10
+ 'thumbnails_generation_timeout' => 10,
+ 'webhooks' => true
}
@config = nil
diff --git a/lib/redmine/preparation.rb b/lib/redmine/preparation.rb
index fec55eb46..be63833a8 100644
--- a/lib/redmine/preparation.rb
+++ b/lib/redmine/preparation.rb
@@ -50,7 +50,7 @@ module Redmine
map.permission :save_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin
# Webhooks
- map.permission :use_webhooks, {}, :require => :member
+ map.permission :use_webhooks, {}, :require => :member if Redmine::Configuration['webhooks']
map.project_module :issue_tracking do |map|
# Issues
diff --git a/test/functional/my_controller_test.rb b/test/functional/my_controller_test.rb
index 3066f68e0..788ddd54b 100644
--- a/test/functional/my_controller_test.rb
+++ b/test/functional/my_controller_test.rb
@@ -398,6 +398,20 @@ class MyControllerTest < Redmine::ControllerTest
assert_select 'select[name=?]', 'user[language]'
end
+ def test_my_account_should_toggle_webhook_link_with_configuration
+ User.find(2).roles.first.add_permission!(:use_webhooks)
+
+ get :account
+ assert_response :success
+ assert_select 'a.icon-webhook', 1
+
+ Redmine::Configuration.with 'webhooks' => false do
+ get :account
+ assert_response :success
+ assert_select 'a.icon-webhook', 0
+ end
+ end
+
def test_my_account_with_avatar_enabled_should_link_to_edit_avatar
with_settings :gravatar_enabled => '1' do
Redmine::Configuration.with 'avatar_server_url' => 'https://gravatar.com' do
diff --git a/test/functional/webhooks_controller_test.rb b/test/functional/webhooks_controller_test.rb
index 220636b2e..1e617e1ac 100644
--- a/test/functional/webhooks_controller_test.rb
+++ b/test/functional/webhooks_controller_test.rb
@@ -27,6 +27,16 @@ class WebhooksControllerTest < Redmine::ControllerTest
assert_select 'td', text: @other_hook.url, count: 0
end
+ test "should return not found when disabled" do
+ Redmine::Configuration.with 'webhooks' => false do
+ get :index
+ assert_response :not_found
+
+ get :new
+ assert_response :not_found
+ end
+ end
+
test "should get new" do
get :new
assert_response :success
diff --git a/test/unit/webhook_test.rb b/test/unit/webhook_test.rb
index df0ed2240..4b92d672a 100644
--- a/test/unit/webhook_test.rb
+++ b/test/unit/webhook_test.rb
@@ -168,6 +168,28 @@ class WebhookTest < ActiveSupport::TestCase
end
end
+ test "enabled? should follow configuration flag" do
+ assert Webhook.enabled?
+
+ Redmine::Configuration.with 'webhooks' => false do
+ assert_not Webhook.enabled?
+ end
+
+ Redmine::Configuration.with 'webhooks' => true do
+ assert Webhook.enabled?
+ end
+ end
+
+ test "trigger should not enqueue jobs when disabled" do
+ create_hook
+
+ Redmine::Configuration.with 'webhooks' => false do
+ assert_no_enqueued_jobs do
+ Webhook.trigger('issue.created', @issue)
+ end
+ end
+ end
+
test "should compute payload" do
hook = create_hook
payload = hook.payload('issue.created', @issue)