From 67ff8a9bee937e9f28d66cab95c319df155454f9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C4=93teris=20Caune?=
Date: Fri, 29 Jan 2021 11:16:11 +0200
Subject: [PATCH] Add the WEBHOOKS_ENABLED setting
---
CHANGELOG.md | 1 +
docker/.env | 1 +
hc/api/tests/test_notify.py | 305 +---------------
hc/api/tests/test_notify_webhook.py | 344 ++++++++++++++++++
hc/api/transports.py | 3 +
hc/front/tests/test_add_webhook.py | 7 +
hc/front/views.py | 3 +
hc/settings.py | 3 +
templates/docs/self_hosted_configuration.html | 5 +-
templates/docs/self_hosted_configuration.md | 8 +-
templates/front/channels.html | 2 +
templates/front/welcome.html | 2 +
12 files changed, 378 insertions(+), 306 deletions(-)
create mode 100644 hc/api/tests/test_notify_webhook.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef2a1a10..781e807a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file.
- Add a section in Docs about running self-hosted instances
- Add experimental Dockerfile and docker-compose.yml
- Add rate limiting for Pushover notifications (6 notifications / user / minute)
+- Add the WEBHOOKS_ENABLED setting (#471)
## Bug Fixes
- Fix unwanted HTML escaping in SMS and WhatsApp notifications
diff --git a/docker/.env b/docker/.env
index 161796e6..d96a3e17 100644
--- a/docker/.env
+++ b/docker/.env
@@ -53,3 +53,4 @@ TWILIO_AUTH=
TWILIO_FROM=
TWILIO_USE_WHATSAPP=False
USE_PAYMENTS=False
+WEBHOOKS_ENABLED=True
\ No newline at end of file
diff --git a/hc/api/tests/test_notify.py b/hc/api/tests/test_notify.py
index 847928e9..3a22c228 100644
--- a/hc/api/tests/test_notify.py
+++ b/hc/api/tests/test_notify.py
@@ -8,7 +8,7 @@ from django.core import mail
from django.utils.timezone import now
from hc.api.models import Channel, Check, Notification, TokenBucket
from hc.test import BaseTestCase
-from requests.exceptions import ConnectionError, Timeout
+from requests.exceptions import Timeout
from django.test.utils import override_settings
@@ -26,309 +26,6 @@ class NotifyTestCase(BaseTestCase):
self.channel.save()
self.channel.checks.add(self.check)
- @patch("hc.api.transports.requests.request")
- def test_webhook(self, mock_get):
- definition = {
- "method_down": "GET",
- "url_down": "http://example",
- "body_down": "",
- "headers_down": {},
- }
-
- self._setup_data("webhook", json.dumps(definition))
- mock_get.return_value.status_code = 200
-
- self.channel.notify(self.check)
- mock_get.assert_called_with(
- "get",
- "http://example",
- headers={"User-Agent": "healthchecks.io"},
- timeout=5,
- )
-
- @patch("hc.api.transports.requests.request", side_effect=Timeout)
- def test_webhooks_handle_timeouts(self, mock_get):
- definition = {
- "method_down": "GET",
- "url_down": "http://example",
- "body_down": "",
- "headers_down": {},
- }
-
- self._setup_data("webhook", json.dumps(definition))
- self.channel.notify(self.check)
-
- # The transport should have retried 3 times
- self.assertEqual(mock_get.call_count, 3)
-
- n = Notification.objects.get()
- self.assertEqual(n.error, "Connection timed out")
-
- self.channel.refresh_from_db()
- self.assertEqual(self.channel.last_error, "Connection timed out")
-
- @patch("hc.api.transports.requests.request", side_effect=ConnectionError)
- def test_webhooks_handle_connection_errors(self, mock_get):
- definition = {
- "method_down": "GET",
- "url_down": "http://example",
- "body_down": "",
- "headers_down": {},
- }
- self._setup_data("webhook", json.dumps(definition))
- self.channel.notify(self.check)
-
- # The transport should have retried 3 times
- self.assertEqual(mock_get.call_count, 3)
-
- n = Notification.objects.get()
- self.assertEqual(n.error, "Connection failed")
-
- @patch("hc.api.transports.requests.request")
- def test_webhooks_handle_500(self, mock_get):
- definition = {
- "method_down": "GET",
- "url_down": "http://example",
- "body_down": "",
- "headers_down": {},
- }
-
- self._setup_data("webhook", json.dumps(definition))
- mock_get.return_value.status_code = 500
-
- self.channel.notify(self.check)
-
- # The transport should have retried 3 times
- self.assertEqual(mock_get.call_count, 3)
-
- n = Notification.objects.get()
- self.assertEqual(n.error, "Received status code 500")
-
- @patch("hc.api.transports.requests.request", side_effect=Timeout)
- def test_webhooks_dont_retry_when_sending_test_notifications(self, mock_get):
- definition = {
- "method_down": "GET",
- "url_down": "http://example",
- "body_down": "",
- "headers_down": {},
- }
-
- self._setup_data("webhook", json.dumps(definition))
- self.channel.notify(self.check, is_test=True)
-
- # is_test flag is set, the transport should not retry:
- self.assertEqual(mock_get.call_count, 1)
-
- n = Notification.objects.get()
- self.assertEqual(n.error, "Connection timed out")
-
- @patch("hc.api.transports.requests.request")
- def test_webhooks_support_variables(self, mock_get):
- definition = {
- "method_down": "GET",
- "url_down": "http://host/$CODE/$STATUS/$TAG1/$TAG2/?name=$NAME",
- "body_down": "",
- "headers_down": {},
- }
-
- self._setup_data("webhook", json.dumps(definition))
- self.check.name = "Hello World"
- self.check.tags = "foo bar"
- self.check.save()
-
- self.channel.notify(self.check)
-
- url = "http://host/%s/down/foo/bar/?name=Hello%%20World" % self.check.code
-
- args, kwargs = mock_get.call_args
- self.assertEqual(args[0], "get")
- self.assertEqual(args[1], url)
- self.assertEqual(kwargs["headers"], {"User-Agent": "healthchecks.io"})
- self.assertEqual(kwargs["timeout"], 5)
-
- @patch("hc.api.transports.requests.request")
- def test_webhooks_handle_variable_variables(self, mock_get):
- definition = {
- "method_down": "GET",
- "url_down": "http://host/$$NAMETAG1",
- "body_down": "",
- "headers_down": {},
- }
-
- self._setup_data("webhook", json.dumps(definition))
- self.check.tags = "foo bar"
- self.check.save()
-
- self.channel.notify(self.check)
-
- # $$NAMETAG1 should *not* get transformed to "foo"
- args, kwargs = mock_get.call_args
- self.assertEqual(args[1], "http://host/$TAG1")
-
- @patch("hc.api.transports.requests.request")
- def test_webhooks_support_post(self, mock_request):
- definition = {
- "method_down": "POST",
- "url_down": "http://example.com",
- "body_down": "The Time Is $NOW",
- "headers_down": {},
- }
-
- self._setup_data("webhook", json.dumps(definition))
- self.check.save()
-
- self.channel.notify(self.check)
- args, kwargs = mock_request.call_args
- self.assertEqual(args[0], "post")
- self.assertEqual(args[1], "http://example.com")
-
- # spaces should not have been urlencoded:
- payload = kwargs["data"].decode()
- self.assertTrue(payload.startswith("The Time Is 2"))
-
- @patch("hc.api.transports.requests.request")
- def test_webhooks_dollarsign_escaping(self, mock_get):
- # If name or tag contains what looks like a variable reference,
- # that should be left alone:
- definition = {
- "method_down": "GET",
- "url_down": "http://host/$NAME",
- "body_down": "",
- "headers_down": {},
- }
-
- self._setup_data("webhook", json.dumps(definition))
- self.check.name = "$TAG1"
- self.check.tags = "foo"
- self.check.save()
-
- self.channel.notify(self.check)
-
- url = "http://host/%24TAG1"
- mock_get.assert_called_with(
- "get", url, headers={"User-Agent": "healthchecks.io"}, timeout=5
- )
-
- @patch("hc.api.transports.requests.request")
- def test_webhooks_handle_up_events(self, mock_get):
- definition = {
- "method_up": "GET",
- "url_up": "http://bar",
- "body_up": "",
- "headers_up": {},
- }
- self._setup_data("webhook", json.dumps(definition), status="up")
-
- self.channel.notify(self.check)
-
- mock_get.assert_called_with(
- "get", "http://bar", headers={"User-Agent": "healthchecks.io"}, timeout=5
- )
-
- @patch("hc.api.transports.requests.request")
- def test_webhooks_handle_noop_up_events(self, mock_get):
- definition = {
- "method_up": "GET",
- "url_up": "",
- "body_up": "",
- "headers_up": {},
- }
-
- self._setup_data("webhook", json.dumps(definition), status="up")
- self.channel.notify(self.check)
-
- self.assertFalse(mock_get.called)
- self.assertEqual(Notification.objects.count(), 0)
-
- @patch("hc.api.transports.requests.request")
- def test_webhooks_handle_unicode_post_body(self, mock_request):
- definition = {
- "method_down": "POST",
- "url_down": "http://foo.com",
- "body_down": "(╯°□°)╯︵ ┻━┻",
- "headers_down": {},
- }
-
- self._setup_data("webhook", json.dumps(definition))
- self.check.save()
-
- self.channel.notify(self.check)
- args, kwargs = mock_request.call_args
-
- # unicode should be encoded into utf-8
- self.assertIsInstance(kwargs["data"], bytes)
-
- @patch("hc.api.transports.requests.request")
- def test_webhooks_handle_post_headers(self, mock_request):
- definition = {
- "method_down": "POST",
- "url_down": "http://foo.com",
- "body_down": "data",
- "headers_down": {"Content-Type": "application/json"},
- }
-
- self._setup_data("webhook", json.dumps(definition))
- self.channel.notify(self.check)
-
- headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"}
- mock_request.assert_called_with(
- "post", "http://foo.com", data=b"data", headers=headers, timeout=5
- )
-
- @patch("hc.api.transports.requests.request")
- def test_webhooks_handle_get_headers(self, mock_request):
- definition = {
- "method_down": "GET",
- "url_down": "http://foo.com",
- "body_down": "",
- "headers_down": {"Content-Type": "application/json"},
- }
-
- self._setup_data("webhook", json.dumps(definition))
- self.channel.notify(self.check)
-
- headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"}
- mock_request.assert_called_with(
- "get", "http://foo.com", headers=headers, timeout=5
- )
-
- @patch("hc.api.transports.requests.request")
- def test_webhooks_allow_user_agent_override(self, mock_request):
- definition = {
- "method_down": "GET",
- "url_down": "http://foo.com",
- "body_down": "",
- "headers_down": {"User-Agent": "My-Agent"},
- }
-
- self._setup_data("webhook", json.dumps(definition))
- self.channel.notify(self.check)
-
- headers = {"User-Agent": "My-Agent"}
- mock_request.assert_called_with(
- "get", "http://foo.com", headers=headers, timeout=5
- )
-
- @patch("hc.api.transports.requests.request")
- def test_webhooks_support_variables_in_headers(self, mock_request):
- definition = {
- "method_down": "GET",
- "url_down": "http://foo.com",
- "body_down": "",
- "headers_down": {"X-Message": "$NAME is DOWN"},
- }
-
- self._setup_data("webhook", json.dumps(definition))
- self.check.name = "Foo"
- self.check.save()
-
- self.channel.notify(self.check)
-
- headers = {"User-Agent": "healthchecks.io", "X-Message": "Foo is DOWN"}
- mock_request.assert_called_with(
- "get", "http://foo.com", headers=headers, timeout=5
- )
-
@patch("hc.api.transports.requests.request")
def test_pd(self, mock_post):
self._setup_data("pd", "123")
diff --git a/hc/api/tests/test_notify_webhook.py b/hc/api/tests/test_notify_webhook.py
new file mode 100644
index 00000000..6836e911
--- /dev/null
+++ b/hc/api/tests/test_notify_webhook.py
@@ -0,0 +1,344 @@
+# coding: utf-8
+
+from datetime import timedelta as td
+import json
+from unittest.mock import patch
+
+from django.utils.timezone import now
+from hc.api.models import Channel, Check, Notification
+from hc.test import BaseTestCase
+from requests.exceptions import ConnectionError, Timeout
+from django.test.utils import override_settings
+
+
+class NotifyWebhookTestCase(BaseTestCase):
+ def _setup_data(self, value, status="down", email_verified=True):
+ self.check = Check(project=self.project)
+ self.check.status = status
+ self.check.last_ping = now() - td(minutes=61)
+ self.check.save()
+
+ self.channel = Channel(project=self.project)
+ self.channel.kind = "webhook"
+ self.channel.value = value
+ self.channel.email_verified = email_verified
+ self.channel.save()
+ self.channel.checks.add(self.check)
+
+ @patch("hc.api.transports.requests.request")
+ def test_webhook(self, mock_get):
+ definition = {
+ "method_down": "GET",
+ "url_down": "http://example",
+ "body_down": "",
+ "headers_down": {},
+ }
+
+ self._setup_data(json.dumps(definition))
+ mock_get.return_value.status_code = 200
+
+ self.channel.notify(self.check)
+ mock_get.assert_called_with(
+ "get",
+ "http://example",
+ headers={"User-Agent": "healthchecks.io"},
+ timeout=5,
+ )
+
+ @patch("hc.api.transports.requests.request", side_effect=Timeout)
+ def test_webhooks_handle_timeouts(self, mock_get):
+ definition = {
+ "method_down": "GET",
+ "url_down": "http://example",
+ "body_down": "",
+ "headers_down": {},
+ }
+
+ self._setup_data(json.dumps(definition))
+ self.channel.notify(self.check)
+
+ # The transport should have retried 3 times
+ self.assertEqual(mock_get.call_count, 3)
+
+ n = Notification.objects.get()
+ self.assertEqual(n.error, "Connection timed out")
+
+ self.channel.refresh_from_db()
+ self.assertEqual(self.channel.last_error, "Connection timed out")
+
+ @patch("hc.api.transports.requests.request", side_effect=ConnectionError)
+ def test_webhooks_handle_connection_errors(self, mock_get):
+ definition = {
+ "method_down": "GET",
+ "url_down": "http://example",
+ "body_down": "",
+ "headers_down": {},
+ }
+ self._setup_data(json.dumps(definition))
+ self.channel.notify(self.check)
+
+ # The transport should have retried 3 times
+ self.assertEqual(mock_get.call_count, 3)
+
+ n = Notification.objects.get()
+ self.assertEqual(n.error, "Connection failed")
+
+ @patch("hc.api.transports.requests.request")
+ def test_webhooks_handle_500(self, mock_get):
+ definition = {
+ "method_down": "GET",
+ "url_down": "http://example",
+ "body_down": "",
+ "headers_down": {},
+ }
+
+ self._setup_data(json.dumps(definition))
+ mock_get.return_value.status_code = 500
+
+ self.channel.notify(self.check)
+
+ # The transport should have retried 3 times
+ self.assertEqual(mock_get.call_count, 3)
+
+ n = Notification.objects.get()
+ self.assertEqual(n.error, "Received status code 500")
+
+ @patch("hc.api.transports.requests.request", side_effect=Timeout)
+ def test_webhooks_dont_retry_when_sending_test_notifications(self, mock_get):
+ definition = {
+ "method_down": "GET",
+ "url_down": "http://example",
+ "body_down": "",
+ "headers_down": {},
+ }
+
+ self._setup_data(json.dumps(definition))
+ self.channel.notify(self.check, is_test=True)
+
+ # is_test flag is set, the transport should not retry:
+ self.assertEqual(mock_get.call_count, 1)
+
+ n = Notification.objects.get()
+ self.assertEqual(n.error, "Connection timed out")
+
+ @patch("hc.api.transports.requests.request")
+ def test_webhooks_support_variables(self, mock_get):
+ definition = {
+ "method_down": "GET",
+ "url_down": "http://host/$CODE/$STATUS/$TAG1/$TAG2/?name=$NAME",
+ "body_down": "",
+ "headers_down": {},
+ }
+
+ self._setup_data(json.dumps(definition))
+ self.check.name = "Hello World"
+ self.check.tags = "foo bar"
+ self.check.save()
+
+ self.channel.notify(self.check)
+
+ url = "http://host/%s/down/foo/bar/?name=Hello%%20World" % self.check.code
+
+ args, kwargs = mock_get.call_args
+ self.assertEqual(args[0], "get")
+ self.assertEqual(args[1], url)
+ self.assertEqual(kwargs["headers"], {"User-Agent": "healthchecks.io"})
+ self.assertEqual(kwargs["timeout"], 5)
+
+ @patch("hc.api.transports.requests.request")
+ def test_webhooks_handle_variable_variables(self, mock_get):
+ definition = {
+ "method_down": "GET",
+ "url_down": "http://host/$$NAMETAG1",
+ "body_down": "",
+ "headers_down": {},
+ }
+
+ self._setup_data(json.dumps(definition))
+ self.check.tags = "foo bar"
+ self.check.save()
+
+ self.channel.notify(self.check)
+
+ # $$NAMETAG1 should *not* get transformed to "foo"
+ args, kwargs = mock_get.call_args
+ self.assertEqual(args[1], "http://host/$TAG1")
+
+ @patch("hc.api.transports.requests.request")
+ def test_webhooks_support_post(self, mock_request):
+ definition = {
+ "method_down": "POST",
+ "url_down": "http://example.com",
+ "body_down": "The Time Is $NOW",
+ "headers_down": {},
+ }
+
+ self._setup_data(json.dumps(definition))
+ self.check.save()
+
+ self.channel.notify(self.check)
+ args, kwargs = mock_request.call_args
+ self.assertEqual(args[0], "post")
+ self.assertEqual(args[1], "http://example.com")
+
+ # spaces should not have been urlencoded:
+ payload = kwargs["data"].decode()
+ self.assertTrue(payload.startswith("The Time Is 2"))
+
+ @patch("hc.api.transports.requests.request")
+ def test_webhooks_dollarsign_escaping(self, mock_get):
+ # If name or tag contains what looks like a variable reference,
+ # that should be left alone:
+ definition = {
+ "method_down": "GET",
+ "url_down": "http://host/$NAME",
+ "body_down": "",
+ "headers_down": {},
+ }
+
+ self._setup_data(json.dumps(definition))
+ self.check.name = "$TAG1"
+ self.check.tags = "foo"
+ self.check.save()
+
+ self.channel.notify(self.check)
+
+ url = "http://host/%24TAG1"
+ mock_get.assert_called_with(
+ "get", url, headers={"User-Agent": "healthchecks.io"}, timeout=5
+ )
+
+ @patch("hc.api.transports.requests.request")
+ def test_webhooks_handle_up_events(self, mock_get):
+ definition = {
+ "method_up": "GET",
+ "url_up": "http://bar",
+ "body_up": "",
+ "headers_up": {},
+ }
+ self._setup_data(json.dumps(definition), status="up")
+
+ self.channel.notify(self.check)
+
+ mock_get.assert_called_with(
+ "get", "http://bar", headers={"User-Agent": "healthchecks.io"}, timeout=5
+ )
+
+ @patch("hc.api.transports.requests.request")
+ def test_webhooks_handle_noop_up_events(self, mock_get):
+ definition = {
+ "method_up": "GET",
+ "url_up": "",
+ "body_up": "",
+ "headers_up": {},
+ }
+
+ self._setup_data(json.dumps(definition), status="up")
+ self.channel.notify(self.check)
+
+ self.assertFalse(mock_get.called)
+ self.assertEqual(Notification.objects.count(), 0)
+
+ @patch("hc.api.transports.requests.request")
+ def test_webhooks_handle_unicode_post_body(self, mock_request):
+ definition = {
+ "method_down": "POST",
+ "url_down": "http://foo.com",
+ "body_down": "(╯°□°)╯︵ ┻━┻",
+ "headers_down": {},
+ }
+
+ self._setup_data(json.dumps(definition))
+ self.check.save()
+
+ self.channel.notify(self.check)
+ args, kwargs = mock_request.call_args
+
+ # unicode should be encoded into utf-8
+ self.assertIsInstance(kwargs["data"], bytes)
+
+ @patch("hc.api.transports.requests.request")
+ def test_webhooks_handle_post_headers(self, mock_request):
+ definition = {
+ "method_down": "POST",
+ "url_down": "http://foo.com",
+ "body_down": "data",
+ "headers_down": {"Content-Type": "application/json"},
+ }
+
+ self._setup_data(json.dumps(definition))
+ self.channel.notify(self.check)
+
+ headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"}
+ mock_request.assert_called_with(
+ "post", "http://foo.com", data=b"data", headers=headers, timeout=5
+ )
+
+ @patch("hc.api.transports.requests.request")
+ def test_webhooks_handle_get_headers(self, mock_request):
+ definition = {
+ "method_down": "GET",
+ "url_down": "http://foo.com",
+ "body_down": "",
+ "headers_down": {"Content-Type": "application/json"},
+ }
+
+ self._setup_data(json.dumps(definition))
+ self.channel.notify(self.check)
+
+ headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"}
+ mock_request.assert_called_with(
+ "get", "http://foo.com", headers=headers, timeout=5
+ )
+
+ @patch("hc.api.transports.requests.request")
+ def test_webhooks_allow_user_agent_override(self, mock_request):
+ definition = {
+ "method_down": "GET",
+ "url_down": "http://foo.com",
+ "body_down": "",
+ "headers_down": {"User-Agent": "My-Agent"},
+ }
+
+ self._setup_data(json.dumps(definition))
+ self.channel.notify(self.check)
+
+ headers = {"User-Agent": "My-Agent"}
+ mock_request.assert_called_with(
+ "get", "http://foo.com", headers=headers, timeout=5
+ )
+
+ @patch("hc.api.transports.requests.request")
+ def test_webhooks_support_variables_in_headers(self, mock_request):
+ definition = {
+ "method_down": "GET",
+ "url_down": "http://foo.com",
+ "body_down": "",
+ "headers_down": {"X-Message": "$NAME is DOWN"},
+ }
+
+ self._setup_data(json.dumps(definition))
+ self.check.name = "Foo"
+ self.check.save()
+
+ self.channel.notify(self.check)
+
+ headers = {"User-Agent": "healthchecks.io", "X-Message": "Foo is DOWN"}
+ mock_request.assert_called_with(
+ "get", "http://foo.com", headers=headers, timeout=5
+ )
+
+ @override_settings(WEBHOOKS_ENABLED=False)
+ def test_it_requires_webhooks_enabled(self):
+ definition = {
+ "method_down": "GET",
+ "url_down": "http://example",
+ "body_down": "",
+ "headers_down": {},
+ }
+
+ self._setup_data(json.dumps(definition))
+ self.channel.notify(self.check)
+
+ n = Notification.objects.get()
+ self.assertEqual(n.error, "Webhook notifications are not enabled.")
diff --git a/hc/api/transports.py b/hc/api/transports.py
index c0db179f..854c376f 100644
--- a/hc/api/transports.py
+++ b/hc/api/transports.py
@@ -232,6 +232,9 @@ class Webhook(HttpTransport):
return False
def notify(self, check):
+ if not settings.WEBHOOKS_ENABLED:
+ return "Webhook notifications are not enabled."
+
spec = self.channel.webhook_spec(check.status)
if not spec["url"]:
return "Empty webhook URL"
diff --git a/hc/front/tests/test_add_webhook.py b/hc/front/tests/test_add_webhook.py
index 0d4d5630..9118793d 100644
--- a/hc/front/tests/test_add_webhook.py
+++ b/hc/front/tests/test_add_webhook.py
@@ -1,3 +1,4 @@
+from django.test.utils import override_settings
from hc.api.models import Channel
from hc.test import BaseTestCase
@@ -185,3 +186,9 @@ class AddWebhookTestCase(BaseTestCase):
self.client.login(username="bob@example.org", password="password")
r = self.client.get(self.url)
self.assertEqual(r.status_code, 403)
+
+ @override_settings(WEBHOOKS_ENABLED=False)
+ def test_it_handles_disabled_integration(self):
+ self.client.login(username="alice@example.org", password="password")
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 404)
diff --git a/hc/front/views.py b/hc/front/views.py
index f9cd2857..e78ad461 100644
--- a/hc/front/views.py
+++ b/hc/front/views.py
@@ -304,6 +304,7 @@ def index(request):
"enable_sms": settings.TWILIO_AUTH is not None,
"enable_telegram": settings.TELEGRAM_TOKEN is not None,
"enable_trello": settings.TRELLO_APP_KEY is not None,
+ "enable_webhooks": settings.WEBHOOKS_ENABLED is True,
"enable_whatsapp": settings.TWILIO_USE_WHATSAPP,
"registration_open": settings.REGISTRATION_OPEN,
}
@@ -770,6 +771,7 @@ def channels(request, code):
"enable_sms": settings.TWILIO_AUTH is not None,
"enable_telegram": settings.TELEGRAM_TOKEN is not None,
"enable_trello": settings.TRELLO_APP_KEY is not None,
+ "enable_webhooks": settings.WEBHOOKS_ENABLED is True,
"enable_whatsapp": settings.TWILIO_USE_WHATSAPP,
"use_payments": settings.USE_PAYMENTS,
}
@@ -931,6 +933,7 @@ def add_email(request, code):
return render(request, "integrations/add_email.html", ctx)
+@require_setting("WEBHOOKS_ENABLED")
@login_required
def add_webhook(request, code):
project = _get_rw_project_for_user(request, code)
diff --git a/hc/settings.py b/hc/settings.py
index a8427596..66f01388 100644
--- a/hc/settings.py
+++ b/hc/settings.py
@@ -235,6 +235,9 @@ TWILIO_USE_WHATSAPP = envbool("TWILIO_USE_WHATSAPP", "False")
# Trello
TRELLO_APP_KEY = os.getenv("TRELLO_APP_KEY")
+# Webhooks
+WEBHOOKS_ENABLED = envbool("WEBHOOKS_ENABLED", "True")
+
# Read additional configuration from hc/local_settings.py if it exists
if os.path.exists(os.path.join(BASE_DIR, "hc/local_settings.py")):
from .local_settings import *
diff --git a/templates/docs/self_hosted_configuration.html b/templates/docs/self_hosted_configuration.html
index b7f78507..0b240f63 100644
--- a/templates/docs/self_hosted_configuration.html
+++ b/templates/docs/self_hosted_configuration.html
@@ -321,4 +321,7 @@ scheme.
Default: False
USE_PAYMENTS
Default: False
-A boolean that turns on/off billing features.
\ No newline at end of file
+A boolean that turns on/off billing features.
+WEBHOOKS_ENABLED
+Default: True
+A boolean that turns on/off the Webhooks integration. Enabled by default.
\ No newline at end of file
diff --git a/templates/docs/self_hosted_configuration.md b/templates/docs/self_hosted_configuration.md
index b2f2b71e..5aab1258 100644
--- a/templates/docs/self_hosted_configuration.md
+++ b/templates/docs/self_hosted_configuration.md
@@ -508,4 +508,10 @@ Default: `False`
Default: `False`
-A boolean that turns on/off billing features.
\ No newline at end of file
+A boolean that turns on/off billing features.
+
+## `WEBHOOKS_ENABLED` {: #WEBHOOKS_ENABLED }
+
+Default: `True`
+
+A boolean that turns on/off the Webhooks integration. Enabled by default.
diff --git a/templates/front/channels.html b/templates/front/channels.html
index 8535680e..54e25421 100644
--- a/templates/front/channels.html
+++ b/templates/front/channels.html
@@ -207,6 +207,7 @@
Add Integration
+ {% if enable_webhooks %}
@@ -215,6 +216,7 @@
Receive an HTTP callback when a check goes down.
Add Integration
+ {% endif %}
{% if enable_apprise %}
diff --git a/templates/front/welcome.html b/templates/front/welcome.html
index 011f1b31..9e76c7ea 100644
--- a/templates/front/welcome.html
+++ b/templates/front/welcome.html
@@ -381,6 +381,7 @@
+ {% if enable_webhooks %}
@@ -390,6 +391,7 @@
+ {% endif %}
{% if enable_slack_btn %}