From 6f030256f5c246651a59a72a4280ff0df863c3e4 Mon Sep 17 00:00:00 2001 From: beeankha Date: Mon, 17 Jun 2019 11:20:49 -0400 Subject: [PATCH 01/17] Add username and password fields to webhook backend --- awx/main/notifications/webhook_backend.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/awx/main/notifications/webhook_backend.py b/awx/main/notifications/webhook_backend.py index 7a68587621..277d82c4b8 100644 --- a/awx/main/notifications/webhook_backend.py +++ b/awx/main/notifications/webhook_backend.py @@ -16,13 +16,17 @@ class WebhookBackend(AWXBaseEmailBackend): init_parameters = {"url": {"label": "Target URL", "type": "string"}, "disable_ssl_verification": {"label": "Verify SSL", "type": "bool", "default": False}, + "username": {"label": "Username", "type": "string"}, + "password": {"label": "Password", "type": "password"}, "headers": {"label": "HTTP Headers", "type": "object"}} recipient_parameter = "url" sender_parameter = None - def __init__(self, headers, disable_ssl_verification=False, fail_silently=False, **kwargs): + def __init__(self, headers, username, password, disable_ssl_verification=False, fail_silently=False, **kwargs): self.disable_ssl_verification = disable_ssl_verification self.headers = headers + self.username = username + self.password = password if password != "" else None super(WebhookBackend, self).__init__(fail_silently=fail_silently) def format_body(self, body): From 5071e1c75fcfbb495781bcecd658185d81ba6a2e Mon Sep 17 00:00:00 2001 From: beeankha Date: Wed, 19 Jun 2019 16:10:50 -0400 Subject: [PATCH 02/17] Update webhook backend to take username/password --- awx/main/notifications/webhook_backend.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/awx/main/notifications/webhook_backend.py b/awx/main/notifications/webhook_backend.py index 277d82c4b8..4b804ee214 100644 --- a/awx/main/notifications/webhook_backend.py +++ b/awx/main/notifications/webhook_backend.py @@ -3,6 +3,7 @@ import logging import requests +import base64 from django.utils.encoding import smart_text from django.utils.translation import ugettext_lazy as _ @@ -16,13 +17,13 @@ class WebhookBackend(AWXBaseEmailBackend): init_parameters = {"url": {"label": "Target URL", "type": "string"}, "disable_ssl_verification": {"label": "Verify SSL", "type": "bool", "default": False}, - "username": {"label": "Username", "type": "string"}, - "password": {"label": "Password", "type": "password"}, + "username": {"label": "Username", "type": "string", "default": ""}, + "password": {"label": "Password", "type": "password", "default": ""}, "headers": {"label": "HTTP Headers", "type": "object"}} recipient_parameter = "url" sender_parameter = None - def __init__(self, headers, username, password, disable_ssl_verification=False, fail_silently=False, **kwargs): + def __init__(self, headers, disable_ssl_verification=False, fail_silently=False, username=None, password=None, **kwargs): self.disable_ssl_verification = disable_ssl_verification self.headers = headers self.username = username @@ -36,6 +37,7 @@ class WebhookBackend(AWXBaseEmailBackend): sent_messages = 0 if 'User-Agent' not in self.headers: self.headers['User-Agent'] = "Tower {}".format(get_awx_version()) + self.headers['Authorization'] = base64.b64encode("{}:{}".format(self.username, self.password).encode()) for m in messages: r = requests.post("{}".format(m.recipients()[0]), json=m.body, From fbb3fd2799b1379824867257820dfad07d4b814f Mon Sep 17 00:00:00 2001 From: beeankha Date: Fri, 21 Jun 2019 15:06:59 -0400 Subject: [PATCH 03/17] Add custom HTTP method --- awx/main/notifications/webhook_backend.py | 29 +++++++++++++---------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/awx/main/notifications/webhook_backend.py b/awx/main/notifications/webhook_backend.py index 4b804ee214..61242d2237 100644 --- a/awx/main/notifications/webhook_backend.py +++ b/awx/main/notifications/webhook_backend.py @@ -3,7 +3,6 @@ import logging import requests -import base64 from django.utils.encoding import smart_text from django.utils.translation import ugettext_lazy as _ @@ -16,6 +15,7 @@ logger = logging.getLogger('awx.main.notifications.webhook_backend') class WebhookBackend(AWXBaseEmailBackend): init_parameters = {"url": {"label": "Target URL", "type": "string"}, + "method": {"label": "HTTP Method", "type": "string", "default": "POST"}, "disable_ssl_verification": {"label": "Verify SSL", "type": "bool", "default": False}, "username": {"label": "Username", "type": "string", "default": ""}, "password": {"label": "Password", "type": "password", "default": ""}, @@ -23,7 +23,8 @@ class WebhookBackend(AWXBaseEmailBackend): recipient_parameter = "url" sender_parameter = None - def __init__(self, headers, disable_ssl_verification=False, fail_silently=False, username=None, password=None, **kwargs): + def __init__(self, headers, method, disable_ssl_verification=False, fail_silently=False, username=None, password=None, **kwargs): + self.method = method self.disable_ssl_verification = disable_ssl_verification self.headers = headers self.username = username @@ -37,15 +38,19 @@ class WebhookBackend(AWXBaseEmailBackend): sent_messages = 0 if 'User-Agent' not in self.headers: self.headers['User-Agent'] = "Tower {}".format(get_awx_version()) - self.headers['Authorization'] = base64.b64encode("{}:{}".format(self.username, self.password).encode()) + if self.method.lower() not in ('put', 'post'): + raise ValueError("Method must be either 'POST' or 'PUT'.") + chosen_method = getattr(requests, self.method.lower(), None) for m in messages: - r = requests.post("{}".format(m.recipients()[0]), - json=m.body, - headers=self.headers, - verify=(not self.disable_ssl_verification)) - if r.status_code >= 400: - logger.error(smart_text(_("Error sending notification webhook: {}").format(r.text))) - if not self.fail_silently: - raise Exception(smart_text(_("Error sending notification webhook: {}").format(r.text))) - sent_messages += 1 + if chosen_method is not None: + r = chosen_method("{}".format(m.recipients()[0]), + auth=(self.username, self.password), + json=m.body, + headers=self.headers, + verify=(not self.disable_ssl_verification)) + if r.status_code >= 400: + logger.error(smart_text(_("Error sending notification webhook: {}").format(r.text))) + if not self.fail_silently: + raise Exception(smart_text(_("Error sending notification webhook: {}").format(r.text))) + sent_messages += 1 return sent_messages From 52b01feafee1d6dd1084cd25906f62484a363d27 Mon Sep 17 00:00:00 2001 From: beeankha Date: Mon, 24 Jun 2019 11:15:00 -0400 Subject: [PATCH 04/17] Change init parameter name to 'http_method' to reduce ambiguity --- awx/main/notifications/webhook_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/notifications/webhook_backend.py b/awx/main/notifications/webhook_backend.py index 61242d2237..a10004ffc8 100644 --- a/awx/main/notifications/webhook_backend.py +++ b/awx/main/notifications/webhook_backend.py @@ -15,7 +15,7 @@ logger = logging.getLogger('awx.main.notifications.webhook_backend') class WebhookBackend(AWXBaseEmailBackend): init_parameters = {"url": {"label": "Target URL", "type": "string"}, - "method": {"label": "HTTP Method", "type": "string", "default": "POST"}, + "http_method": {"label": "HTTP Method", "type": "string", "default": "POST"}, "disable_ssl_verification": {"label": "Verify SSL", "type": "bool", "default": False}, "username": {"label": "Username", "type": "string", "default": ""}, "password": {"label": "Password", "type": "password", "default": ""}, From cc0310ccd4d70ff96103aa32086df560c1398c83 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Mon, 24 Jun 2019 11:59:26 -0400 Subject: [PATCH 05/17] add notification webhook fields --- .../src/notifications/add/add.controller.js | 9 +++++++++ .../src/notifications/edit/edit.controller.js | 10 ++++++++++ .../notificationTemplates.form.js | 19 +++++++++++++++++-- .../shared/type-change.service.js | 2 ++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/notifications/add/add.controller.js b/awx/ui/client/src/notifications/add/add.controller.js index 7093a80680..aff4ff0275 100644 --- a/awx/ui/client/src/notifications/add/add.controller.js +++ b/awx/ui/client/src/notifications/add/add.controller.js @@ -87,6 +87,15 @@ export default ['Rest', 'Wait', 'NotificationsFormObject', element: '#notification_template_color', multiple: false }); + + $scope.httpMethodChoices = [ + {'id': 'post', 'name': i18n._('POST')}, + {'id': 'put', 'name': i18n._('PUT')}, + ]; + CreateSelect2({ + element: '#notification_template_http_method', + multiple: false, + }); }); $scope.$watch('headers', function validate_headers(str) { diff --git a/awx/ui/client/src/notifications/edit/edit.controller.js b/awx/ui/client/src/notifications/edit/edit.controller.js index a90d025953..56ff081961 100644 --- a/awx/ui/client/src/notifications/edit/edit.controller.js +++ b/awx/ui/client/src/notifications/edit/edit.controller.js @@ -138,6 +138,16 @@ export default ['Rest', 'Wait', element: '#notification_template_color', multiple: false }); + + $scope.httpMethodChoices = [ + {'id': 'post', 'name': i18n._('POST')}, + {'id': 'put', 'name': i18n._('PUT')}, + ]; + CreateSelect2({ + element: '#notification_template_http_method', + multiple: false, + }); + NotificationsTypeChange.getDetailFields($scope.notification_type.value).forEach(function(field) { $scope[field[0]] = field[1]; }); diff --git a/awx/ui/client/src/notifications/notificationTemplates.form.js b/awx/ui/client/src/notifications/notificationTemplates.form.js index fa291d7654..704d7d8464 100644 --- a/awx/ui/client/src/notifications/notificationTemplates.form.js +++ b/awx/ui/client/src/notifications/notificationTemplates.form.js @@ -63,7 +63,7 @@ export default ['i18n', function(i18n) { username: { label: i18n._('Username'), type: 'text', - ngShow: "notification_type.value == 'email' ", + ngShow: "notification_type.value == 'email' || notification_type.value == 'webhook' ", subForm: 'typeSubForm', ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)' }, @@ -75,7 +75,7 @@ export default ['i18n', function(i18n) { reqExpression: "password_required" , init: "false" }, - ngShow: "notification_type.value == 'email' || notification_type.value == 'irc' ", + ngShow: "notification_type.value == 'email' || notification_type.value == 'irc' || notification_type.value == 'webhook' ", subForm: 'typeSubForm', ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)' }, @@ -423,6 +423,21 @@ export default ['i18n', function(i18n) { subForm: 'typeSubForm', ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)' }, + http_method: { + label: i18n._('HTTP Method'), + dataTitle: i18n._('HTTP Method'), + type: 'select', + ngOptions: 'choice.id as choice.name for choice in httpMethodChoices', + default: 'post', + awPopOver: i18n._('Specify an HTTP method for the webhook. Acceptable choices are: POST or PATCH'), + awRequiredWhen: { + reqExpression: "webhook_required", + init: "false" + }, + ngShow: "notification_type.value == 'webhook' ", + subForm: 'typeSubForm', + ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)' + }, mattermost_url: { label: i18n._('Target URL'), type: 'text', diff --git a/awx/ui/client/src/notifications/shared/type-change.service.js b/awx/ui/client/src/notifications/shared/type-change.service.js index 5695a4ca10..86e2baa35b 100644 --- a/awx/ui/client/src/notifications/shared/type-change.service.js +++ b/awx/ui/client/src/notifications/shared/type-change.service.js @@ -53,6 +53,8 @@ function (i18n) { break; case 'webhook': obj.webhook_required = true; + obj.passwordLabel = ' ' + i18n._('Basic Auth Password'); + obj.password_required = false; break; case 'mattermost': obj.mattermost_required = true; From 6e9f74eb17b5e7b546001ad2642c2c6ded685e8c Mon Sep 17 00:00:00 2001 From: beeankha Date: Mon, 24 Jun 2019 12:13:22 -0400 Subject: [PATCH 06/17] Updating tests, changing 'method' to 'http_method' --- awx/main/notifications/webhook_backend.py | 10 +++++----- awx/main/tests/factories/fixtures.py | 9 ++++----- awx/main/tests/functional/conftest.py | 4 +++- awx/main/tests/functional/test_notifications.py | 4 ++-- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/awx/main/notifications/webhook_backend.py b/awx/main/notifications/webhook_backend.py index a10004ffc8..f13f96f235 100644 --- a/awx/main/notifications/webhook_backend.py +++ b/awx/main/notifications/webhook_backend.py @@ -23,8 +23,8 @@ class WebhookBackend(AWXBaseEmailBackend): recipient_parameter = "url" sender_parameter = None - def __init__(self, headers, method, disable_ssl_verification=False, fail_silently=False, username=None, password=None, **kwargs): - self.method = method + def __init__(self, headers, http_method, disable_ssl_verification=False, fail_silently=False, username=None, password=None, **kwargs): + self.http_method = http_method self.disable_ssl_verification = disable_ssl_verification self.headers = headers self.username = username @@ -38,9 +38,9 @@ class WebhookBackend(AWXBaseEmailBackend): sent_messages = 0 if 'User-Agent' not in self.headers: self.headers['User-Agent'] = "Tower {}".format(get_awx_version()) - if self.method.lower() not in ('put', 'post'): - raise ValueError("Method must be either 'POST' or 'PUT'.") - chosen_method = getattr(requests, self.method.lower(), None) + if self.http_method.lower() not in ('put', 'post'): + raise ValueError("HTTP method must be either 'POST' or 'PUT'.") + chosen_method = getattr(requests, self.http_method.lower(), None) for m in messages: if chosen_method is not None: r = chosen_method("{}".format(m.recipients()[0]), diff --git a/awx/main/tests/factories/fixtures.py b/awx/main/tests/factories/fixtures.py index d4ad255e4a..fe61410908 100644 --- a/awx/main/tests/factories/fixtures.py +++ b/awx/main/tests/factories/fixtures.py @@ -117,7 +117,7 @@ def mk_credential(name, credential_type='ssh', persisted=True): def mk_notification_template(name, notification_type='webhook', configuration=None, organization=None, persisted=True): nt = NotificationTemplate(name=name) nt.notification_type = notification_type - nt.notification_configuration = configuration or dict(url="http://localhost", headers={"Test": "Header"}) + nt.notification_configuration = configuration or dict(url="http://localhost", username="", password="", headers={"Test": "Header"}) if organization is not None: nt.organization = organization @@ -216,7 +216,7 @@ def mk_workflow_job_template(name, extra_vars='', spec=None, organization=None, def mk_workflow_job_template_node(workflow_job_template=None, - unified_job_template=None, + unified_job_template=None, success_nodes=None, failure_nodes=None, always_nodes=None, @@ -231,11 +231,11 @@ def mk_workflow_job_template_node(workflow_job_template=None, return workflow_node -def mk_workflow_job_node(unified_job_template=None, +def mk_workflow_job_node(unified_job_template=None, success_nodes=None, failure_nodes=None, always_nodes=None, - workflow_job=None, + workflow_job=None, job=None, persisted=True): workflow_node = WorkflowJobNode(unified_job_template=unified_job_template, @@ -247,4 +247,3 @@ def mk_workflow_job_node(unified_job_template=None, if persisted: workflow_node.save() return workflow_node - diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index c924034bdd..314ef94713 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -385,7 +385,9 @@ def notification_template(organization): organization=organization, notification_type="webhook", notification_configuration=dict(url="http://localhost", - headers={"Test": "Header"})) + username="", + password="", + headers={"Test": "Header",})) @pytest.fixture diff --git a/awx/main/tests/functional/test_notifications.py b/awx/main/tests/functional/test_notifications.py index 576bc71a7a..308e52c2c7 100644 --- a/awx/main/tests/functional/test_notifications.py +++ b/awx/main/tests/functional/test_notifications.py @@ -92,7 +92,7 @@ def test_inherited_notification_templates(get, post, user, organization, project isrc.save() jt = JobTemplate.objects.create(name='test', inventory=i, project=project, playbook='debug.yml') jt.save() - + @pytest.mark.django_db def test_notification_template_simple_patch(patch, notification_template, admin): @@ -124,7 +124,7 @@ def test_custom_environment_injection(post, user, organization): organization=organization.id, notification_type="webhook", notification_configuration=dict(url="https://example.org", disable_ssl_verification=False, - headers={"Test": "Header"})), + http_method="POST", headers={"Test": "Header"})), u) assert response.status_code == 201 template = NotificationTemplate.objects.get(pk=response.data['id']) From 2b74b6f9b62b724d8e55b01e4b8426bc80bb9bec Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Mon, 24 Jun 2019 15:56:10 -0400 Subject: [PATCH 07/17] add tooling for basic testing of notification webhooks --- Makefile | 3 +++ docs/notification_system.md | 8 ++++++++ tools/docker-notifications-override.yml | 11 +++++++++++ 3 files changed, 22 insertions(+) create mode 100644 tools/docker-notifications-override.yml diff --git a/Makefile b/Makefile index 652ff2f971..7b1b7ca017 100644 --- a/Makefile +++ b/Makefile @@ -583,6 +583,9 @@ docker-compose-credential-plugins: docker-auth echo -e "\033[0;31mTo generate a CyberArk Conjur API key: docker exec -it tools_conjur_1 conjurctl account create quick-start\033[0m" CURRENT_UID=$(shell id -u) TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose.yml -f tools/docker-credential-plugins-override.yml up --no-recreate awx +docker-compose-notifications: docker-auth + CURRENT_UID=$(shell id -u) TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose.yml -f tools/docker-notifications-override.yml up --no-recreate awx + docker-compose-test: docker-auth cd tools && CURRENT_UID=$(shell id -u) OS="$(shell docker info | grep 'Operating System')" TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose run --rm --service-ports awx /bin/bash diff --git a/docs/notification_system.md b/docs/notification_system.md index 7ef17d2cef..40adb5c8af 100644 --- a/docs/notification_system.md +++ b/docs/notification_system.md @@ -218,6 +218,14 @@ https://gist.github.com/matburt/73bfbf85c2443f39d272 The link below shows how to define an endpoint and parse headers and json content. It doesn't show how to configure Flask for HTTPS, but is fairly straightforward: http://flask.pocoo.org/snippets/111/ +You can also link an httpbin service to the development environment for testing webhooks using: + +``` +make docker-compose-notifications +``` + +This will create an `httpbin` service reachable from the awx container at `http://httpbin/post`, `http://httpbin/put`, etc. Outside of the container, you can reach the service at `http://localhost:8204`. + ## Grafana diff --git a/tools/docker-notifications-override.yml b/tools/docker-notifications-override.yml new file mode 100644 index 0000000000..528ed7b353 --- /dev/null +++ b/tools/docker-notifications-override.yml @@ -0,0 +1,11 @@ +version: '2' +services: + # Primary Tower Development Container link + awx: + links: + - httpbin + httpbin: + image: kennethreitz/httpbin + container_name: tools_httpbin_1 + ports: + - '8204:80' From 0a0b09b394aaecf0a7ec6d8bb04b02b4c544568b Mon Sep 17 00:00:00 2001 From: beeankha Date: Tue, 25 Jun 2019 10:46:05 -0400 Subject: [PATCH 08/17] Update logic in send method to recognize password field in upgraded webhook notifications --- awx/main/models/notifications.py | 7 ++++--- docs/notification_system.md | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/awx/main/models/notifications.py b/awx/main/models/notifications.py index cdec14f953..fc99caf37c 100644 --- a/awx/main/models/notifications.py +++ b/awx/main/models/notifications.py @@ -118,9 +118,10 @@ class NotificationTemplate(CommonModelNameNotUnique): def send(self, subject, body): for field in filter(lambda x: self.notification_class.init_parameters[x]['type'] == "password", self.notification_class.init_parameters): - self.notification_configuration[field] = decrypt_field(self, - 'notification_configuration', - subfield=field) + if field in self.notification_configuration: + self.notification_configuration[field] = decrypt_field(self, + 'notification_configuration', + subfield=field) recipients = self.notification_configuration.pop(self.notification_class.recipient_parameter) if not isinstance(recipients, list): recipients = [recipients] diff --git a/docs/notification_system.md b/docs/notification_system.md index 40adb5c8af..cc7cc2a7e5 100644 --- a/docs/notification_system.md +++ b/docs/notification_system.md @@ -218,13 +218,13 @@ https://gist.github.com/matburt/73bfbf85c2443f39d272 The link below shows how to define an endpoint and parse headers and json content. It doesn't show how to configure Flask for HTTPS, but is fairly straightforward: http://flask.pocoo.org/snippets/111/ -You can also link an httpbin service to the development environment for testing webhooks using: +You can also link an `httpbin` service to the development environment for testing webhooks using: ``` make docker-compose-notifications ``` -This will create an `httpbin` service reachable from the awx container at `http://httpbin/post`, `http://httpbin/put`, etc. Outside of the container, you can reach the service at `http://localhost:8204`. +This will create an `httpbin` service reachable from the AWX container at `http://httpbin/post`, `http://httpbin/put`, etc. Outside of the container, you can reach the service at `http://localhost:8204`. ## Grafana From 99737937cd2bbd4d99aae9fcafc67c74cc4dbc56 Mon Sep 17 00:00:00 2001 From: beeankha Date: Tue, 25 Jun 2019 13:57:32 -0400 Subject: [PATCH 09/17] No auth header sent if username/password fields are blank --- awx/main/notifications/webhook_backend.py | 28 ++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/awx/main/notifications/webhook_backend.py b/awx/main/notifications/webhook_backend.py index f13f96f235..a6449bcab9 100644 --- a/awx/main/notifications/webhook_backend.py +++ b/awx/main/notifications/webhook_backend.py @@ -23,12 +23,12 @@ class WebhookBackend(AWXBaseEmailBackend): recipient_parameter = "url" sender_parameter = None - def __init__(self, headers, http_method, disable_ssl_verification=False, fail_silently=False, username=None, password=None, **kwargs): + def __init__(self, http_method, headers, disable_ssl_verification=False, fail_silently=False, username=None, password=None, **kwargs): self.http_method = http_method self.disable_ssl_verification = disable_ssl_verification self.headers = headers self.username = username - self.password = password if password != "" else None + self.password = password super(WebhookBackend, self).__init__(fail_silently=fail_silently) def format_body(self, body): @@ -42,15 +42,17 @@ class WebhookBackend(AWXBaseEmailBackend): raise ValueError("HTTP method must be either 'POST' or 'PUT'.") chosen_method = getattr(requests, self.http_method.lower(), None) for m in messages: - if chosen_method is not None: - r = chosen_method("{}".format(m.recipients()[0]), - auth=(self.username, self.password), - json=m.body, - headers=self.headers, - verify=(not self.disable_ssl_verification)) - if r.status_code >= 400: - logger.error(smart_text(_("Error sending notification webhook: {}").format(r.text))) - if not self.fail_silently: - raise Exception(smart_text(_("Error sending notification webhook: {}").format(r.text))) - sent_messages += 1 + auth = None + if self.username and self.password: + auth = (self.username, self.password) + r = chosen_method("{}".format(m.recipients()[0]), + auth=auth, + json=m.body, + headers=self.headers, + verify=(not self.disable_ssl_verification)) + if r.status_code >= 400: + logger.error(smart_text(_("Error sending notification webhook: {}").format(r.text))) + if not self.fail_silently: + raise Exception(smart_text(_("Error sending notification webhook: {}").format(r.text))) + sent_messages += 1 return sent_messages From d66106d3807a62f45296bba9ca0c7492f0b1b7b5 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 25 Jun 2019 15:04:02 -0400 Subject: [PATCH 10/17] rename docker-notifications to docker-httpbin --- Makefile | 4 ++-- docs/notification_system.md | 2 +- ...notifications-override.yml => docker-httpbin-override.yml} | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename tools/{docker-notifications-override.yml => docker-httpbin-override.yml} (100%) diff --git a/Makefile b/Makefile index 7b1b7ca017..9a555bb3e1 100644 --- a/Makefile +++ b/Makefile @@ -583,8 +583,8 @@ docker-compose-credential-plugins: docker-auth echo -e "\033[0;31mTo generate a CyberArk Conjur API key: docker exec -it tools_conjur_1 conjurctl account create quick-start\033[0m" CURRENT_UID=$(shell id -u) TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose.yml -f tools/docker-credential-plugins-override.yml up --no-recreate awx -docker-compose-notifications: docker-auth - CURRENT_UID=$(shell id -u) TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose.yml -f tools/docker-notifications-override.yml up --no-recreate awx +docker-compose-httpbin: docker-auth + CURRENT_UID=$(shell id -u) TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose.yml -f tools/docker-httpbin-override.yml up --no-recreate awx docker-compose-test: docker-auth cd tools && CURRENT_UID=$(shell id -u) OS="$(shell docker info | grep 'Operating System')" TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose run --rm --service-ports awx /bin/bash diff --git a/docs/notification_system.md b/docs/notification_system.md index cc7cc2a7e5..f76c880f50 100644 --- a/docs/notification_system.md +++ b/docs/notification_system.md @@ -221,7 +221,7 @@ http://flask.pocoo.org/snippets/111/ You can also link an `httpbin` service to the development environment for testing webhooks using: ``` -make docker-compose-notifications +make docker-compose-httpbin ``` This will create an `httpbin` service reachable from the AWX container at `http://httpbin/post`, `http://httpbin/put`, etc. Outside of the container, you can reach the service at `http://localhost:8204`. diff --git a/tools/docker-notifications-override.yml b/tools/docker-httpbin-override.yml similarity index 100% rename from tools/docker-notifications-override.yml rename to tools/docker-httpbin-override.yml From 6ef235dcd502a4f67ea5da59dfd525ad0cdfb4c9 Mon Sep 17 00:00:00 2001 From: beeankha Date: Mon, 22 Jul 2019 11:03:05 -0400 Subject: [PATCH 11/17] Enable auth header to send with just username field filled in --- awx/main/notifications/webhook_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/notifications/webhook_backend.py b/awx/main/notifications/webhook_backend.py index a6449bcab9..9ee73e92f0 100644 --- a/awx/main/notifications/webhook_backend.py +++ b/awx/main/notifications/webhook_backend.py @@ -43,7 +43,7 @@ class WebhookBackend(AWXBaseEmailBackend): chosen_method = getattr(requests, self.http_method.lower(), None) for m in messages: auth = None - if self.username and self.password: + if self.username: auth = (self.username, self.password) r = chosen_method("{}".format(m.recipients()[0]), auth=auth, From 04404c93db58f7d170fe023c2a1066b6552b3102 Mon Sep 17 00:00:00 2001 From: beeankha Date: Mon, 22 Jul 2019 16:57:10 -0400 Subject: [PATCH 12/17] Enforce http_method restrictions via API --- awx/api/serializers.py | 2 ++ awx/main/notifications/webhook_backend.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index b3d4e4c2b6..fdddc9ba22 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -4237,6 +4237,8 @@ class NotificationTemplateSerializer(BaseSerializer): continue if field_type == "password" and field_val == "$encrypted$" and object_actual is not None: attrs['notification_configuration'][field] = object_actual.notification_configuration[field] + if field == "http_method" and field_val.lower() not in ['put', 'post']: + error_list.append(_("HTTP method must be either 'POST' or 'PUT'.")) if missing_fields: error_list.append(_("Missing required fields for Notification Configuration: {}.").format(missing_fields)) if incorrect_type_fields: diff --git a/awx/main/notifications/webhook_backend.py b/awx/main/notifications/webhook_backend.py index 9ee73e92f0..bb9c869b12 100644 --- a/awx/main/notifications/webhook_backend.py +++ b/awx/main/notifications/webhook_backend.py @@ -38,7 +38,7 @@ class WebhookBackend(AWXBaseEmailBackend): sent_messages = 0 if 'User-Agent' not in self.headers: self.headers['User-Agent'] = "Tower {}".format(get_awx_version()) - if self.http_method.lower() not in ('put', 'post'): + if self.http_method.lower() not in ['put','post']: raise ValueError("HTTP method must be either 'POST' or 'PUT'.") chosen_method = getattr(requests, self.http_method.lower(), None) for m in messages: From 37e73acb62965c46587fb2a1d5398d56f5cd03da Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 25 Jun 2019 15:30:07 -0400 Subject: [PATCH 13/17] cleanup tooling --- Makefile | 3 --- docs/notification_system.md | 2 +- tools/docker-httpbin-override.yml | 11 ----------- 3 files changed, 1 insertion(+), 15 deletions(-) delete mode 100644 tools/docker-httpbin-override.yml diff --git a/Makefile b/Makefile index 9a555bb3e1..652ff2f971 100644 --- a/Makefile +++ b/Makefile @@ -583,9 +583,6 @@ docker-compose-credential-plugins: docker-auth echo -e "\033[0;31mTo generate a CyberArk Conjur API key: docker exec -it tools_conjur_1 conjurctl account create quick-start\033[0m" CURRENT_UID=$(shell id -u) TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose.yml -f tools/docker-credential-plugins-override.yml up --no-recreate awx -docker-compose-httpbin: docker-auth - CURRENT_UID=$(shell id -u) TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose.yml -f tools/docker-httpbin-override.yml up --no-recreate awx - docker-compose-test: docker-auth cd tools && CURRENT_UID=$(shell id -u) OS="$(shell docker info | grep 'Operating System')" TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose run --rm --service-ports awx /bin/bash diff --git a/docs/notification_system.md b/docs/notification_system.md index f76c880f50..65b05607b3 100644 --- a/docs/notification_system.md +++ b/docs/notification_system.md @@ -221,7 +221,7 @@ http://flask.pocoo.org/snippets/111/ You can also link an `httpbin` service to the development environment for testing webhooks using: ``` -make docker-compose-httpbin +docker run --network="tools_default" --name httpbin -p 8204:80 kennethreitz/httpbin ``` This will create an `httpbin` service reachable from the AWX container at `http://httpbin/post`, `http://httpbin/put`, etc. Outside of the container, you can reach the service at `http://localhost:8204`. diff --git a/tools/docker-httpbin-override.yml b/tools/docker-httpbin-override.yml deleted file mode 100644 index 528ed7b353..0000000000 --- a/tools/docker-httpbin-override.yml +++ /dev/null @@ -1,11 +0,0 @@ -version: '2' -services: - # Primary Tower Development Container link - awx: - links: - - httpbin - httpbin: - image: kennethreitz/httpbin - container_name: tools_httpbin_1 - ports: - - '8204:80' From 7580491f1a1f1d4be2642e45df68c34bc3b9ef7f Mon Sep 17 00:00:00 2001 From: beeankha Date: Tue, 23 Jul 2019 14:52:26 -0400 Subject: [PATCH 14/17] Add migration file to define http_method explicitly --- .../0082_v360_webhook_http_method.py | 26 +++++++++++++++++++ awx/main/notifications/webhook_backend.py | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 awx/main/migrations/0082_v360_webhook_http_method.py diff --git a/awx/main/migrations/0082_v360_webhook_http_method.py b/awx/main/migrations/0082_v360_webhook_http_method.py new file mode 100644 index 0000000000..3e81cfe4a9 --- /dev/null +++ b/awx/main/migrations/0082_v360_webhook_http_method.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + + +def add_webhook_notification_template_fields(apps, schema_editor): + # loop over all existing webhook notification templates + # and make sure they have the new "http_method", "username" + # and "password" fields + NotificationTemplate = apps.get_model('main', 'notificationtemplate') + webhooks = NotificationTemplate.objects.filter(notification_type='webhook') + for w in webhooks: + w.notification_configuration['http_method'] = 'POST' + w.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0081_v360_notify_on_start'), + ] + + operations = [ + migrations.RunPython(add_webhook_notification_template_fields, migrations.RunPython.noop), + ] diff --git a/awx/main/notifications/webhook_backend.py b/awx/main/notifications/webhook_backend.py index bb9c869b12..91a6f15118 100644 --- a/awx/main/notifications/webhook_backend.py +++ b/awx/main/notifications/webhook_backend.py @@ -43,7 +43,7 @@ class WebhookBackend(AWXBaseEmailBackend): chosen_method = getattr(requests, self.http_method.lower(), None) for m in messages: auth = None - if self.username: + if self.username or self.password: auth = (self.username, self.password) r = chosen_method("{}".format(m.recipients()[0]), auth=auth, From 1fe18dc5885316a3921d959b3db1d003684dde80 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 23 Jul 2019 19:24:00 -0400 Subject: [PATCH 15/17] normalize http method choice values --- awx/ui/client/src/notifications/add/add.controller.js | 4 ++-- awx/ui/client/src/notifications/edit/edit.controller.js | 4 ++-- awx/ui/client/src/notifications/main.js | 8 ++++++-- .../src/notifications/notificationTemplates.form.js | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/awx/ui/client/src/notifications/add/add.controller.js b/awx/ui/client/src/notifications/add/add.controller.js index aff4ff0275..bd7fc04e5f 100644 --- a/awx/ui/client/src/notifications/add/add.controller.js +++ b/awx/ui/client/src/notifications/add/add.controller.js @@ -89,8 +89,8 @@ export default ['Rest', 'Wait', 'NotificationsFormObject', }); $scope.httpMethodChoices = [ - {'id': 'post', 'name': i18n._('POST')}, - {'id': 'put', 'name': i18n._('PUT')}, + {'id': 'POST', 'name': i18n._('POST')}, + {'id': 'POST', 'name': i18n._('PUT')}, ]; CreateSelect2({ element: '#notification_template_http_method', diff --git a/awx/ui/client/src/notifications/edit/edit.controller.js b/awx/ui/client/src/notifications/edit/edit.controller.js index 56ff081961..b8470e3c22 100644 --- a/awx/ui/client/src/notifications/edit/edit.controller.js +++ b/awx/ui/client/src/notifications/edit/edit.controller.js @@ -140,8 +140,8 @@ export default ['Rest', 'Wait', }); $scope.httpMethodChoices = [ - {'id': 'post', 'name': i18n._('POST')}, - {'id': 'put', 'name': i18n._('PUT')}, + {'id': 'POST', 'name': i18n._('POST')}, + {'id': 'PUT', 'name': i18n._('PUT')}, ]; CreateSelect2({ element: '#notification_template_http_method', diff --git a/awx/ui/client/src/notifications/main.js b/awx/ui/client/src/notifications/main.js index c1a2a559be..138862bbef 100644 --- a/awx/ui/client/src/notifications/main.js +++ b/awx/ui/client/src/notifications/main.js @@ -68,8 +68,12 @@ angular.module('notifications', [ var url = getBasePath('notification_templates') + notificationTemplateId + '/'; rest.setUrl(url); return rest.get() - .then(function(data) { - return data.data; + .then(function(res) { + if (_.get(res, ['data', 'notification_type'] === 'webhook') && + _.get(res, ['data', 'notification_configuration', 'http_method'])) { + res.data.notification_configuration.http_method = res.data.notification_configuration.http_method.toUpperCase(); + } + return res.data; }).catch(function(response) { ProcessErrors(null, response.data, response.status, null, { hdr: 'Error!', diff --git a/awx/ui/client/src/notifications/notificationTemplates.form.js b/awx/ui/client/src/notifications/notificationTemplates.form.js index 704d7d8464..15b8629f1d 100644 --- a/awx/ui/client/src/notifications/notificationTemplates.form.js +++ b/awx/ui/client/src/notifications/notificationTemplates.form.js @@ -429,7 +429,7 @@ export default ['i18n', function(i18n) { type: 'select', ngOptions: 'choice.id as choice.name for choice in httpMethodChoices', default: 'post', - awPopOver: i18n._('Specify an HTTP method for the webhook. Acceptable choices are: POST or PATCH'), + awPopOver: i18n._('Specify an HTTP method for the webhook. Acceptable choices are: POST or PUT'), awRequiredWhen: { reqExpression: "webhook_required", init: "false" From f7502eed2fefb3a710bde3227d95791c2104fdbc Mon Sep 17 00:00:00 2001 From: beeankha Date: Wed, 24 Jul 2019 08:59:32 -0400 Subject: [PATCH 16/17] Correct the comment in migration file --- awx/main/migrations/0082_v360_webhook_http_method.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/awx/main/migrations/0082_v360_webhook_http_method.py b/awx/main/migrations/0082_v360_webhook_http_method.py index 3e81cfe4a9..3dc93ff970 100644 --- a/awx/main/migrations/0082_v360_webhook_http_method.py +++ b/awx/main/migrations/0082_v360_webhook_http_method.py @@ -5,9 +5,8 @@ from django.db import migrations def add_webhook_notification_template_fields(apps, schema_editor): - # loop over all existing webhook notification templates - # and make sure they have the new "http_method", "username" - # and "password" fields + # loop over all existing webhook notification templates and make + # sure they have the new "http_method" field filled in with "POST" NotificationTemplate = apps.get_model('main', 'notificationtemplate') webhooks = NotificationTemplate.objects.filter(notification_type='webhook') for w in webhooks: From 97f841057f8b8128db90481d8d7cb9f66f9f05ac Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Wed, 24 Jul 2019 15:49:26 -0400 Subject: [PATCH 17/17] fix method mapping for webhook notification add --- awx/ui/client/src/notifications/add/add.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/notifications/add/add.controller.js b/awx/ui/client/src/notifications/add/add.controller.js index bd7fc04e5f..3197dd310c 100644 --- a/awx/ui/client/src/notifications/add/add.controller.js +++ b/awx/ui/client/src/notifications/add/add.controller.js @@ -90,7 +90,7 @@ export default ['Rest', 'Wait', 'NotificationsFormObject', $scope.httpMethodChoices = [ {'id': 'POST', 'name': i18n._('POST')}, - {'id': 'POST', 'name': i18n._('PUT')}, + {'id': 'PUT', 'name': i18n._('PUT')}, ]; CreateSelect2({ element: '#notification_template_http_method',