diff --git a/awx/api/serializers.py b/awx/api/serializers.py index d901184ee0..04f5e241c3 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2059,8 +2059,20 @@ class NotifierSerializer(BaseSerializer): model = Notifier fields = ('*', 'organization', 'notification_type', 'notification_configuration') - type_map = {"string": str, "int": int, "bool": bool, "list": list, - "password": str, "object": dict} + type_map = {"string": (str, unicode), + "int": (int,), + "bool": (bool,), + "list": (list,), + "password": (str, unicode), + "object": (dict,)} + + def to_representation(self, obj): + ret = super(NotifierSerializer, self).to_representation(obj) + for field in obj.notification_class.init_parameters: + if field in ret['notification_configuration'] and \ + force_text(ret['notification_configuration'][field]).startswith('$encrypted$'): + ret['notification_configuration'][field] = '$encrypted$' + return ret def get_related(self, obj): res = super(NotifierSerializer, self).get_related(obj) @@ -2076,20 +2088,28 @@ class NotifierSerializer(BaseSerializer): notification_class = Notifier.CLASS_FOR_NOTIFICATION_TYPE[attrs['notification_type']] missing_fields = [] incorrect_type_fields = [] + if 'notification_configuration' not in attrs: + return attrs for field in notification_class.init_parameters: if field not in attrs['notification_configuration']: missing_fields.append(field) continue field_val = attrs['notification_configuration'][field] field_type = notification_class.init_parameters[field]['type'] - expected_type = self.type_map[field_type] - if not isinstance(field_val, expected_type): + expected_types = self.type_map[field_type] + if not type(field_val) in expected_types: incorrect_type_fields.append((field, field_type)) + continue + if field_type == "password" and field_val.startswith('$encrypted$'): + missing_fields.append(field) + error_list = [] if missing_fields: - error_list = ["Missing required fields for Notification Configuration: {}".format(missing_fields)] + error_list.append("Missing required fields for Notification Configuration: {}".format(missing_fields)) + if incorrect_type_fields: for type_field_error in incorrect_type_fields: - error_list.append("Configuration field {} incorrect type, expected {}".format(type_field_error[0], + error_list.append("Configuration field '{}' incorrect type, expected {}".format(type_field_error[0], type_field_error[1])) + if error_list: raise serializers.ValidationError(error_list) return attrs diff --git a/awx/main/models/notifications.py b/awx/main/models/notifications.py index 2e1bb9df2f..d6fc9d31b8 100644 --- a/awx/main/models/notifications.py +++ b/awx/main/models/notifications.py @@ -10,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import smart_str from awx.main.models.base import * # noqa +from awx.main.utils import encrypt_field, decrypt_field from awx.main.notifications.email_backend import CustomEmailBackend from awx.main.notifications.slack_backend import SlackBackend from awx.main.notifications.twilio_backend import TwilioBackend @@ -62,6 +63,31 @@ class Notifier(CommonModel): def notification_class(self): return self.CLASS_FOR_NOTIFICATION_TYPE[self.notification_type] + def save(self, *args, **kwargs): + new_instance = not bool(self.pk) + update_fields = kwargs.get('update_fields', []) + for field in filter(lambda x: self.notification_class.init_parameters[x]['type'] == "password", + self.notification_class.init_parameters): + if new_instance: + value = getattr(self.notification_configuration, field, '') + setattr(self, '_saved_{}'.format(field), value) + self.notification_configuration[field] = '' + else: + encrypted = encrypt_field(self, 'notification_configuration', subfield=field) + self.notification_configuration[field] = encrypted + if 'notification_configuration' not in update_fields: + update_fields.append('notification_configuration') + super(Notifier, self).save(*args, **kwargs) + if new_instance: + update_fields = [] + for field in filter(lambda x: self.notification_class.init_parameters[x]['type'] == "password", + self.notification_class.init_parameters): + saved_value = getattr(self, '_saved_{}'.format(field), '') + setattr(self.notification_configuration, field, saved_value) + if 'notification_configuration' not in update_fields: + update_fields.append('notification_configuration') + self.save(update_fields=update_fields) + @property def recipients(self): return self.notification_configuration[self.notification_class.recipient_parameter] @@ -76,6 +102,11 @@ class Notifier(CommonModel): return notification 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) recipients = self.notification_configuration.pop(self.notification_class.recipient_parameter) if not isinstance(recipients, list): recipients = [recipients] diff --git a/awx/main/utils.py b/awx/main/utils.py index 5bd00c2da6..a561648f95 100644 --- a/awx/main/utils.py +++ b/awx/main/utils.py @@ -139,12 +139,13 @@ def get_encryption_key(instance, field_name): h.update(field_name) return h.digest()[:16] - -def encrypt_field(instance, field_name, ask=False): +def encrypt_field(instance, field_name, ask=False, subfield=None): ''' Return content of the given instance and field name encrypted. ''' value = getattr(instance, field_name) + if isinstance(value, dict) and subfield is not None: + value = value[subfield] if not value or value.startswith('$encrypted$') or (ask and value == 'ASK'): return value value = smart_str(value) @@ -157,11 +158,13 @@ def encrypt_field(instance, field_name, ask=False): return '$encrypted$%s$%s' % ('AES', b64data) -def decrypt_field(instance, field_name): +def decrypt_field(instance, field_name, subfield=None): ''' Return content of the given instance and field name decrypted. ''' value = getattr(instance, field_name) + if isinstance(value, dict) and subfield is not None: + value = value[subfield] if not value or not value.startswith('$encrypted$'): return value algo, b64data = value[len('$encrypted$'):].split('$', 1)