diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 4588a5ae80..ad05837d08 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -44,7 +44,7 @@ from awx.main.fields import ImplicitRoleField from awx.main.utils import ( get_type_for_model, get_model_for_type, timestamp_apiformat, camelcase_to_underscore, getattrd, parse_yaml_or_json, - has_model_field_prefetched, extract_ansible_vars) + has_model_field_prefetched, extract_ansible_vars, encrypt_dict) from awx.main.utils.filters import SmartFilter from awx.main.redact import REPLACE_STR @@ -3140,7 +3140,6 @@ class LaunchConfigurationBaseSerializer(BaseSerializer): attrs['char_prompts'] = mock_obj.char_prompts # Insert survey_passwords to track redacted variables - # TODO: perform encryption on save if 'extra_data' in attrs: extra_data = parse_yaml_or_json(attrs.get('extra_data', {})) if hasattr(ujt, 'survey_password_variables'): @@ -3150,6 +3149,20 @@ class LaunchConfigurationBaseSerializer(BaseSerializer): password_dict[key] = REPLACE_STR if not self.instance or password_dict != self.instance.survey_passwords: attrs['survey_passwords'] = password_dict + if not isinstance(attrs['extra_data'], dict): + attrs['extra_data'] = parse_yaml_or_json(attrs['extra_data']) + encrypt_dict(attrs['extra_data'], password_dict.keys()) + if self.instance: + db_extra_data = parse_yaml_or_json(self.instance.extra_data) + else: + db_extra_data = {} + for key in password_dict.keys(): + if attrs['extra_data'].get(key, None) == REPLACE_STR: + if key not in db_extra_data: + raise serializers.ValidationError( + _('Provided variable {} has no database value to replace with.').format(key)) + else: + attrs['extra_data'][key] = db_extra_data[key] return attrs diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 1d2a4eb221..f1c934b9a3 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -34,7 +34,7 @@ from django_celery_results.models import TaskResult from awx.main.models.base import * # noqa from awx.main.models.mixins import ResourceMixin, TaskManagerUnifiedJobMixin from awx.main.utils import ( - encrypt_value, decrypt_field, _inventory_updates, + encrypt_dict, decrypt_field, _inventory_updates, copy_model_by_class, copy_m2m_relationships, get_type_for_model, parse_yaml_or_json ) @@ -349,11 +349,7 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio # automatically encrypt survey fields if hasattr(self, 'survey_spec') and getattr(self, 'survey_enabled', False): password_list = self.survey_password_variables() - for key in kwargs.get('extra_vars', {}): - if key in password_list: - kwargs['extra_vars'][key] = encrypt_value( - kwargs['extra_vars'][key] - ) + encrypt_dict(kwargs.get('extra_vars', {}), password_list) unified_job_class = self._get_unified_job_class() fields = self._get_unified_job_field_names() diff --git a/awx/main/tests/unit/api/serializers/test_workflow_serializers.py b/awx/main/tests/unit/api/serializers/test_workflow_serializers.py index 7e19f66420..712345a1b6 100644 --- a/awx/main/tests/unit/api/serializers/test_workflow_serializers.py +++ b/awx/main/tests/unit/api/serializers/test_workflow_serializers.py @@ -177,6 +177,8 @@ class TestWorkflowJobTemplateNodeSerializerSurveyPasswords(): }) assert 'survey_passwords' in attrs assert 'var1' in attrs['survey_passwords'] + assert attrs['extra_data']['var1'].startswith('$encrypted$') + assert len(attrs['extra_data']['var1']) > len('$encrypted$') def test_set_survey_passwords_modify(self, jt): serializer = WorkflowJobTemplateNodeSerializer() @@ -192,6 +194,25 @@ class TestWorkflowJobTemplateNodeSerializerSurveyPasswords(): }) assert 'survey_passwords' in attrs assert 'var1' in attrs['survey_passwords'] + assert attrs['extra_data']['var1'].startswith('$encrypted$') + assert len(attrs['extra_data']['var1']) > len('$encrypted$') + + def test_use_db_answer(self, jt): + serializer = WorkflowJobTemplateNodeSerializer() + wfjt = WorkflowJobTemplate(name='fake-wfjt') + serializer.instance = WorkflowJobTemplateNode( + workflow_job_template=wfjt, + unified_job_template=jt, + extra_data={'var1': '$encrypted$foooooo'} + ) + attrs = serializer.validate({ + 'unified_job_template': jt, + 'workflow_job_template': wfjt, + 'extra_data': {'var1': '$encrypted$'} + }) + assert 'survey_passwords' in attrs + assert 'var1' in attrs['survey_passwords'] + assert attrs['extra_data']['var1'] == '$encrypted$foooooo' @mock.patch('awx.api.serializers.WorkflowJobTemplateNodeSerializer.get_related', lambda x,y: {}) diff --git a/awx/main/utils/encryption.py b/awx/main/utils/encryption.py index c8c5b72afd..29aed4721e 100644 --- a/awx/main/utils/encryption.py +++ b/awx/main/utils/encryption.py @@ -9,8 +9,10 @@ from cryptography.hazmat.backends import default_backend from django.utils.encoding import smart_str -__all__ = ['get_encryption_key', 'encrypt_value', 'encrypt_field', - 'decrypt_field', 'decrypt_value'] +__all__ = ['get_encryption_key', + 'encrypt_field', 'decrypt_field', + 'encrypt_value', 'decrypt_value', + 'encrypt_dict'] logger = logging.getLogger('awx.main.utils.encryption') @@ -125,3 +127,13 @@ def decrypt_field(instance, field_name, subfield=None): exc_info=True ) raise + + +def encrypt_dict(data, fields): + ''' + Encrypts all of the dictionary values in `data` under the keys in `fields` + in-place operation on `data` + ''' + encrypt_fields = set(data.keys()).intersection(fields) + for key in encrypt_fields: + data[key] = encrypt_value(data[key])