def test_setting_singleton_update_dont_change_encripted_mark(api_request, dummy_setting): with dummy_setting( 'FOO_BAR', field_class=fields.CharField, encrypted=True, category='FooBar', category_slug='foobar' ), mock.patch('awx.conf.views.handle_setting_changes'): api_request( 'patch', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}), data={'FOO_BAR': 'password'} ) assert Setting.objects.get(key='FOO_BAR').value.startswith('$encrypted$') response = api_request( 'get', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}) ) assert response.data['FOO_BAR'] == '$encrypted$' api_request( 'patch', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}), data={'FOO_BAR': '$encrypted$'} ) assert decrypt_field(Setting.objects.get(key='FOO_BAR'), 'value') == 'password' api_request( 'patch', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}), data={'FOO_BAR': 'new_pw'} ) assert decrypt_field(Setting.objects.get(key='FOO_BAR'), 'value') == 'new_pw'
def test_encrypt_subfield(): field = Setting(value={'name': 'ANSIBLE'}) encrypted = field.value = encryption.encrypt_field(field, 'value', subfield='name') assert encryption.decrypt_field(field, 'value', subfield='name') == 'ANSIBLE' assert encrypted.startswith('$encrypted$UTF8$AESCBC$')
def test_encrypt_field_force_disable_unicode(): value = u"NothingSpecial" field = Setting(value=value) encrypted = field.value = encryption.encrypt_field(field, 'value', skip_utf8=True) assert "UTF8" not in encrypted assert encryption.decrypt_field(field, 'value') == value
def _credentials(self): for credential in Credential.objects.iterator(): for field_name in credential.credential_type.secret_fields: if field_name in credential.inputs: credential.inputs[field_name] = decrypt_field( credential, field_name, secret_key=self.old_key) credential.inputs[field_name] = encrypt_field( credential, field_name, secret_key=self.new_key) credential.save()
def _unified_jobs(self): for uj in UnifiedJob.objects.iterator(): if uj.start_args: uj.start_args = decrypt_field(uj, 'start_args', secret_key=self.old_key) uj.start_args = encrypt_field(uj, 'start_args', secret_key=self.new_key) uj.save()
def _settings(self): # don't update memcached, the *actual* value isn't changing post_save.disconnect(on_post_save_setting, sender=Setting) for setting in Setting.objects.filter().order_by('pk'): if settings_registry.is_setting_encrypted(setting.key): setting.value = decrypt_field(setting, 'value', secret_key=self.old_key) setting.value = encrypt_field(setting, 'value', secret_key=self.new_key) setting.save()
def test_job_start_args(self, job_factory): # test basic decryption job = job_factory() job.start_args = json.dumps({'foo': 'bar'}) job.start_args = encrypt_field(job, field_name='start_args') job.save() assert job.start_args.startswith(PREFIX) # re-key the start_args new_key = regenerate_secret_key.Command().handle() new_job = models.Job.objects.get(pk=job.pk) assert new_job.start_args != job.start_args # verify that the old SECRET_KEY doesn't work with pytest.raises(InvalidToken): decrypt_field(new_job, field_name='start_args') # verify that the new SECRET_KEY *does* work with override_settings(SECRET_KEY=new_key): assert json.loads( decrypt_field(new_job, field_name='start_args') ) == {'foo': 'bar'}
def _migrate_setting(apps, old_key, new_key, encrypted=False): Setting = apps.get_model('conf', 'Setting') if not Setting.objects.filter(key=old_key).exists(): return new_setting = Setting.objects.create(key=new_key, created=now(), modified=now()) if encrypted: new_setting.value = decrypt_field( Setting.objects.filter(key=old_key).first(), 'value') new_setting.value = encrypt_field(new_setting, 'value') else: new_setting.value = getattr( Setting.objects.filter(key=old_key).first(), 'value') new_setting.save()
def _notification_templates(self): for nt in NotificationTemplate.objects.iterator(): CLASS_FOR_NOTIFICATION_TYPE = dict([ (x[0], x[2]) for x in NotificationTemplate.NOTIFICATION_TYPES ]) notification_class = CLASS_FOR_NOTIFICATION_TYPE[ nt.notification_type] for field in filter( lambda x: notification_class.init_parameters[x]['type'] == "password", notification_class.init_parameters): nt.notification_configuration[field] = decrypt_field( nt, 'notification_configuration', subfield=field, secret_key=self.old_key) nt.notification_configuration[field] = encrypt_field( nt, 'notification_configuration', subfield=field, secret_key=self.new_key) nt.save()
def migrate_galaxy_settings(apps, schema_editor): Organization = apps.get_model('main', 'Organization') if Organization.objects.count() == 0: # nothing to migrate return set_current_apps(apps) ModernCredentialType.setup_tower_managed_defaults(apps) CredentialType = apps.get_model('main', 'CredentialType') Credential = apps.get_model('main', 'Credential') Setting = apps.get_model('conf', 'Setting') galaxy_type = CredentialType.objects.get(kind='galaxy') private_galaxy_url = Setting.objects.filter( key='PRIMARY_GALAXY_URL').first() # by default, prior versions of AWX automatically pulled content # from galaxy.ansible.com public_galaxy_enabled = True public_galaxy_setting = Setting.objects.filter( key='PUBLIC_GALAXY_ENABLED').first() if public_galaxy_setting and public_galaxy_setting.value is False: # ...UNLESS this behavior was explicitly disabled via this setting public_galaxy_enabled = False try: # Needed for old migrations public_galaxy_credential = Credential( created=now(), modified=now(), name='Ansible Galaxy', managed_by_tower=True, credential_type=galaxy_type, inputs={'url': 'https://galaxy.ansible.com/'}, ) except: # Needed for new migrations, tests public_galaxy_credential = Credential( created=now(), modified=now(), name='Ansible Galaxy', managed=True, credential_type=galaxy_type, inputs={'url': 'https://galaxy.ansible.com/'}) public_galaxy_credential.save() for org in Organization.objects.all(): if private_galaxy_url and private_galaxy_url.value: # If a setting exists for a private Galaxy URL, make a credential for it username = Setting.objects.filter( key='PRIMARY_GALAXY_USERNAME').first() password = Setting.objects.filter( key='PRIMARY_GALAXY_PASSWORD').first() if (username and username.value) or (password and password.value): logger.error( f'Specifying HTTP basic auth for the Ansible Galaxy API ' f'({private_galaxy_url.value}) is no longer supported. ' 'Please provide an API token instead after your upgrade ' 'has completed', ) inputs = {'url': private_galaxy_url.value} token = Setting.objects.filter(key='PRIMARY_GALAXY_TOKEN').first() if token and token.value: inputs['token'] = decrypt_field(token, 'value') auth_url = Setting.objects.filter( key='PRIMARY_GALAXY_AUTH_URL').first() if auth_url and auth_url.value: inputs['auth_url'] = auth_url.value name = f'Private Galaxy ({private_galaxy_url.value})' if 'cloud.redhat.com' in inputs['url']: name = f'Ansible Automation Hub ({private_galaxy_url.value})' cred = Credential(created=now(), modified=now(), name=name, organization=org, credential_type=galaxy_type, inputs=inputs) cred.save() if token and token.value: # encrypt based on the primary key from the prior save cred.inputs['token'] = encrypt_field(cred, 'token') cred.save() org.galaxy_credentials.add(cred) fallback_servers = getattr(settings, 'FALLBACK_GALAXY_SERVERS', []) for fallback in fallback_servers: url = fallback.get('url', None) auth_url = fallback.get('auth_url', None) username = fallback.get('username', None) password = fallback.get('password', None) token = fallback.get('token', None) if username or password: logger.error( f'Specifying HTTP basic auth for the Ansible Galaxy API ' f'({url}) is no longer supported. ' 'Please provide an API token instead after your upgrade ' 'has completed', ) inputs = {'url': url} if token: inputs['token'] = token if auth_url: inputs['auth_url'] = auth_url cred = Credential(created=now(), modified=now(), name=f'Ansible Galaxy ({url})', organization=org, credential_type=galaxy_type, inputs=inputs) cred.save() if token: # encrypt based on the primary key from the prior save cred.inputs['token'] = encrypt_field(cred, 'token') cred.save() org.galaxy_credentials.add(cred) if public_galaxy_enabled: # If public Galaxy was enabled, associate it to the org org.galaxy_credentials.add(public_galaxy_credential)
def migrate_to_v2_credentials(apps, schema_editor): CredentialType.setup_tower_managed_defaults() deprecated_cred = _generate_deprecated_cred_types() # this monkey-patch is necessary to make the implicit role generation save # signal use the correct Role model (the version active at this point in # migration, not the one at HEAD) orig_current_apps = utils.get_current_apps try: utils.get_current_apps = lambda: apps for cred in apps.get_model('main', 'Credential').objects.all(): job_templates = cred.jobtemplates.all() jobs = cred.jobs.all() data = {} if getattr(cred, 'vault_password', None): data['vault_password'] = cred.vault_password if _is_insights_scm(apps, cred): _disassociate_non_insights_projects(apps, cred) credential_type = _get_insights_credential_type() else: credential_type = _populate_deprecated_cred_types( deprecated_cred, cred.kind) or CredentialType.from_v1_kind( cred.kind, data) defined_fields = credential_type.defined_fields cred.credential_type = apps.get_model( 'main', 'CredentialType').objects.get(pk=credential_type.pk) for field in defined_fields: if getattr(cred, field, None): cred.inputs[field] = getattr(cred, field) if cred.vault_password: for jt in job_templates: jt.credential = None jt.vault_credential = cred jt.save() for job in jobs: job.credential = None job.vault_credential = cred job.save() if data.get('is_insights', False): cred.kind = 'insights' cred.save() # # If the credential contains a vault password, create a new # *additional* credential for the ssh details # if cred.vault_password: # We need to make an ssh credential, too ssh_type = CredentialType.from_v1_kind('ssh') new_cred = apps.get_model('main', 'Credential').objects.get(pk=cred.pk) new_cred.pk = None new_cred.vault_password = '' new_cred.credential_type = apps.get_model( 'main', 'CredentialType').objects.get(pk=ssh_type.pk) if 'vault_password' in new_cred.inputs: del new_cred.inputs['vault_password'] # unset these attributes so that new roles are properly created # at save time new_cred.read_role = None new_cred.admin_role = None new_cred.use_role = None if any([ getattr(cred, field) for field in ssh_type.defined_fields ]): new_cred.save(force_insert=True) # copy rbac roles for role_type in ('read_role', 'admin_role', 'use_role'): for member in getattr(cred, role_type).members.all(): getattr(new_cred, role_type).members.add(member) for role in getattr(cred, role_type).parents.all(): getattr(new_cred, role_type).parents.add(role) for jt in job_templates: jt.credential = new_cred jt.save() for job in jobs: job.credential = new_cred job.save() # passwords must be decrypted and re-encrypted, because # their encryption is based on the Credential's primary key # (which has changed) for field in ssh_type.defined_fields: if field in ssh_type.secret_fields: value = decrypt_field(cred, field) if value: setattr(new_cred, field, value) new_cred.inputs[field] = encrypt_field( new_cred, field) setattr(new_cred, field, '') elif getattr(cred, field): new_cred.inputs[field] = getattr(cred, field) new_cred.save() finally: utils.get_current_apps = orig_current_apps
def test_encrypt_field(): field = Setting(pk=123, value='ANSIBLE') encrypted = field.value = encryption.encrypt_field(field, 'value') assert encryption.decrypt_field(field, 'value') == 'ANSIBLE' assert encrypted.startswith('$encrypted$UTF8$AESCBC$')
def test_encrypt_field_with_unicode_string(): value = u'Iñtërnâtiônàlizætiøn' field = Setting(value=value) encrypted = field.value = encryption.encrypt_field(field, 'value') assert encryption.decrypt_field(field, 'value') == value assert encrypted.startswith('$encrypted$UTF8$AESCBC$')
def test_decrypt_field_with_undefined_attr_raises_expected_exception(): with pytest.raises(AttributeError): encryption.decrypt_field({}, 'undefined_attr')
def test_encrypt_field_without_pk(): field = Setting(value='ANSIBLE') encrypted = field.value = encryption.encrypt_field(field, 'value') assert encryption.decrypt_field(field, 'value') == 'ANSIBLE' assert encrypted.startswith('$encrypted$AESCBC$')