diff --git a/awx/main/migrations/0011_v322_encrypt_survey_passwords.py b/awx/main/migrations/0011_v322_encrypt_survey_passwords.py new file mode 100644 index 0000000000..344aa96bc1 --- /dev/null +++ b/awx/main/migrations/0011_v322_encrypt_survey_passwords.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations +from awx.main.migrations import ActivityStreamDisabledMigration +from awx.main.migrations import _reencrypt as reencrypt + + +class Migration(ActivityStreamDisabledMigration): + + dependencies = [ + ('main', '0010_v322_add_support_for_ovirt4_inventory'), + ] + + operations = [ + migrations.RunPython(reencrypt.encrypt_survey_passwords), + ] diff --git a/awx/main/migrations/_reencrypt.py b/awx/main/migrations/_reencrypt.py index c4e502f9a2..c1fad4aedf 100644 --- a/awx/main/migrations/_reencrypt.py +++ b/awx/main/migrations/_reencrypt.py @@ -1,5 +1,7 @@ import logging +import json from django.utils.translation import ugettext_lazy as _ +import six from awx.conf.migrations._reencrypt import ( decrypt_field, @@ -72,3 +74,38 @@ def _unified_jobs(apps): uj.start_args = decrypt_field(uj, 'start_args') uj.start_args = encrypt_field(uj, 'start_args') uj.save() + + +def encrypt_survey_passwords(apps, schema_editor): + _encrypt_survey_passwords( + apps.get_model('main', 'Job'), + apps.get_model('main', 'JobTemplate'), + ) + + +def _encrypt_survey_passwords(Job, JobTemplate): + from awx.main.utils.encryption import encrypt_value + for jt in JobTemplate.objects.exclude(survey_spec={}): + changed = False + if jt.survey_spec.get('spec', []): + for field in jt.survey_spec['spec']: + if field.get('type') == 'password' and field.get('default', ''): + if field['default'].startswith('$encrypted$'): + continue + field['default'] = encrypt_value(field['default'], pk=None) + changed = True + if changed: + jt.save() + + for job in Job.objects.defer('result_stdout_text').exclude(survey_passwords={}).iterator(): + changed = False + for key in job.survey_passwords: + if key in job.extra_vars: + extra_vars = json.loads(job.extra_vars) + if not extra_vars.get(key, '') or extra_vars[key].startswith('$encrypted$'): + continue + extra_vars[key] = encrypt_value(extra_vars[key], pk=None) + job.extra_vars = json.dumps(extra_vars) + changed = True + if changed: + job.save() diff --git a/awx/main/tests/functional/test_reencrypt_migration.py b/awx/main/tests/functional/test_reencrypt_migration.py index 3201866893..18aa2bc644 100644 --- a/awx/main/tests/functional/test_reencrypt_migration.py +++ b/awx/main/tests/functional/test_reencrypt_migration.py @@ -10,6 +10,8 @@ from django.apps import apps from awx.main.models import ( UnifiedJob, + Job, + JobTemplate, NotificationTemplate, Credential, ) @@ -20,9 +22,10 @@ from awx.main.migrations._reencrypt import ( _notification_templates, _credentials, _unified_jobs, + _encrypt_survey_passwords ) -from awx.main.utils import decrypt_field +from awx.main.utils import decrypt_field, get_encryption_key, decrypt_value @pytest.mark.django_db @@ -93,3 +96,54 @@ def test_unified_job_migration(old_enc, new_enc, value): # Exception if the encryption type of AESCBC is not properly skipped, ensures # our `startswith` calls don't have typos _unified_jobs(apps) + + +@pytest.mark.django_db +def test_survey_default_password_encryption(job_template_factory): + jt = job_template_factory('jt', organization='org1', project='prj', + inventory='inv', credential='cred').job_template + jt.survey_enabled = True + jt.survey_spec = { + 'description': 'A survey', + 'spec': [{ + 'index': 0, + 'question_name': 'What is your password?', + 'required': True, + 'variable': 'secret_value', + 'default': 'SUPERSECRET', + 'type': 'password' + }], + 'name': 'my survey' + } + jt.save() + + _encrypt_survey_passwords(Job, JobTemplate) + spec = JobTemplate.objects.get(pk=jt.pk).survey_spec['spec'] + assert decrypt_value(get_encryption_key('value', pk=None), spec[0]['default']) == 'SUPERSECRET' + + +@pytest.mark.django_db +def test_job_survey_vars_encryption(job_template_factory): + jt = job_template_factory('jt', organization='org1', project='prj', + inventory='inv', credential='cred').job_template + jt.survey_enabled = True + jt.survey_spec = { + 'description': 'A survey', + 'spec': [{ + 'index': 0, + 'question_name': 'What is your password?', + 'required': True, + 'variable': 'secret_value', + 'default': '', + 'type': 'password' + }], + 'name': 'my survey' + } + jt.save() + job = jt.create_unified_job() + job.extra_vars = json.dumps({'secret_value': 'SUPERSECRET'}) + job.save() + + _encrypt_survey_passwords(Job, JobTemplate) + job = Job.objects.get(pk=job.pk) + assert json.loads(job.decrypted_extra_vars()) == {'secret_value': 'SUPERSECRET'}