mirror of
https://github.com/ansible/awx.git
synced 2026-03-01 00:38:45 -03:30
Merge pull request #6601 from wwitzel3/devel
Fix re-encryption migration for credentials.
This commit is contained in:
@@ -9,7 +9,7 @@ from awx.conf import settings_registry
|
|||||||
|
|
||||||
|
|
||||||
__all__ = ['replace_aesecb_fernet', 'get_encryption_key', 'encrypt_field',
|
__all__ = ['replace_aesecb_fernet', 'get_encryption_key', 'encrypt_field',
|
||||||
'decrypt_value', 'decrypt_value']
|
'decrypt_value', 'decrypt_value', 'should_decrypt_field']
|
||||||
|
|
||||||
|
|
||||||
def replace_aesecb_fernet(apps, schema_editor):
|
def replace_aesecb_fernet(apps, schema_editor):
|
||||||
@@ -17,9 +17,8 @@ def replace_aesecb_fernet(apps, schema_editor):
|
|||||||
|
|
||||||
for setting in Setting.objects.filter().order_by('pk'):
|
for setting in Setting.objects.filter().order_by('pk'):
|
||||||
if settings_registry.is_setting_encrypted(setting.key):
|
if settings_registry.is_setting_encrypted(setting.key):
|
||||||
if setting.value.startswith('$encrypted$AESCBC$'):
|
if should_decrypt_field(setting.value):
|
||||||
continue
|
setting.value = decrypt_field(setting, 'value')
|
||||||
setting.value = decrypt_field(setting, 'value')
|
|
||||||
setting.save()
|
setting.save()
|
||||||
|
|
||||||
|
|
||||||
@@ -100,3 +99,7 @@ def encrypt_field(instance, field_name, ask=False, subfield=None, skip_utf8=Fals
|
|||||||
# know to decode the data when it's decrypted later
|
# know to decode the data when it's decrypted later
|
||||||
tokens.insert(1, 'UTF8')
|
tokens.insert(1, 'UTF8')
|
||||||
return '$'.join(tokens)
|
return '$'.join(tokens)
|
||||||
|
|
||||||
|
|
||||||
|
def should_decrypt_field(value):
|
||||||
|
return value.startswith('$encrypted$') and '$AESCBC$' not in value
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2017 Ansible, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
import pytest
|
import pytest
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
@@ -12,17 +16,21 @@ from awx.main.utils import decrypt_field as new_decrypt_field
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_settings():
|
@pytest.mark.parametrize("old_enc, new_enc, value", [
|
||||||
|
('$encrypted$UTF8$AES', '$encrypted$UTF8$AESCBC$', u'Iñtërnâtiônàlizætiøn'),
|
||||||
|
('$encrypted$AES$', '$encrypted$AESCBC$', 'test'),
|
||||||
|
])
|
||||||
|
def test_settings(old_enc, new_enc, value):
|
||||||
with mock.patch('awx.conf.models.encrypt_field', encrypt_field):
|
with mock.patch('awx.conf.models.encrypt_field', encrypt_field):
|
||||||
with mock.patch('awx.conf.settings.decrypt_field', decrypt_field):
|
with mock.patch('awx.conf.settings.decrypt_field', decrypt_field):
|
||||||
setting = Setting.objects.create(key='SOCIAL_AUTH_GITHUB_SECRET', value='test')
|
setting = Setting.objects.create(key='SOCIAL_AUTH_GITHUB_SECRET', value=value)
|
||||||
assert setting.value.startswith('$encrypted$AES$')
|
assert setting.value.startswith(old_enc)
|
||||||
|
|
||||||
replace_aesecb_fernet(apps, None)
|
replace_aesecb_fernet(apps, None)
|
||||||
setting.refresh_from_db()
|
setting.refresh_from_db()
|
||||||
|
|
||||||
assert setting.value.startswith('$encrypted$AESCBC$')
|
assert setting.value.startswith(new_enc)
|
||||||
assert new_decrypt_field(setting, 'value') == 'test'
|
assert new_decrypt_field(setting, 'value') == value
|
||||||
|
|
||||||
# This is here for a side-effect.
|
# This is here for a side-effect.
|
||||||
# Exception if the encryption type of AESCBC is not properly skipped, ensures
|
# Exception if the encryption type of AESCBC is not properly skipped, ensures
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from psycopg2.extensions import AsIs
|
|||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.migrations import _reencrypt as reencrypt
|
|
||||||
import awx.main.fields
|
import awx.main.fields
|
||||||
from awx.main.models import Host
|
from awx.main.models import Host
|
||||||
|
|
||||||
@@ -277,7 +276,6 @@ class Migration(migrations.Migration):
|
|||||||
name='kind',
|
name='kind',
|
||||||
field=models.CharField(default=b'', help_text='Kind of inventory being represented.', max_length=32, blank=True, choices=[(b'', 'Hosts have a direct link to this inventory.'), (b'smart', 'Hosts for inventory generated using the host_filter property.')]),
|
field=models.CharField(default=b'', help_text='Kind of inventory being represented.', max_length=32, blank=True, choices=[(b'', 'Hosts have a direct link to this inventory.'), (b'smart', 'Hosts for inventory generated using the host_filter property.')]),
|
||||||
),
|
),
|
||||||
migrations.RunPython(reencrypt.replace_aesecb_fernet),
|
|
||||||
|
|
||||||
# Timeout help text update
|
# Timeout help text update
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from awx.main import utils
|
from awx.main import utils
|
||||||
from awx.main.models import CredentialType
|
from awx.main.models import CredentialType
|
||||||
from awx.main.utils import encrypt_field, decrypt_field
|
from awx.conf.migrations._reencrypt import encrypt_field, decrypt_field
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
from awx.conf.migrations._reencrypt import decrypt_field
|
from awx.main import utils
|
||||||
|
from awx.conf.migrations._reencrypt import (
|
||||||
|
decrypt_field,
|
||||||
|
should_decrypt_field,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['replace_aesecb_fernet']
|
__all__ = ['replace_aesecb_fernet']
|
||||||
@@ -15,32 +19,35 @@ def _notification_templates(apps):
|
|||||||
for nt in NotificationTemplate.objects.all():
|
for nt in NotificationTemplate.objects.all():
|
||||||
for field in filter(lambda x: nt.notification_class.init_parameters[x]['type'] == "password",
|
for field in filter(lambda x: nt.notification_class.init_parameters[x]['type'] == "password",
|
||||||
nt.notification_class.init_parameters):
|
nt.notification_class.init_parameters):
|
||||||
if nt.notification_configuration[field].startswith('$encrypted$AESCBC$'):
|
if should_decrypt_field(nt.notification_configuration[field]):
|
||||||
continue
|
value = decrypt_field(nt, 'notification_configuration', subfield=field)
|
||||||
value = decrypt_field(nt, 'notification_configuration', subfield=field)
|
nt.notification_configuration[field] = value
|
||||||
nt.notification_configuration[field] = value
|
|
||||||
nt.save()
|
nt.save()
|
||||||
|
|
||||||
|
|
||||||
def _credentials(apps):
|
def _credentials(apps):
|
||||||
Credential = apps.get_model('main', 'Credential')
|
# this monkey-patch is necessary to make the implicit role generation save
|
||||||
for credential in Credential.objects.all():
|
# signal use the correct Role model (the version active at this point in
|
||||||
for field_name, value in credential.inputs.items():
|
# migration, not the one at HEAD)
|
||||||
if field_name in credential.credential_type.secret_fields:
|
orig_current_apps = utils.get_current_apps
|
||||||
value = getattr(credential, field_name)
|
try:
|
||||||
if value.startswith('$encrypted$AESCBC$'):
|
utils.get_current_apps = lambda: apps
|
||||||
continue
|
for credential in apps.get_model('main', 'Credential').objects.all():
|
||||||
value = decrypt_field(credential, field_name)
|
for field_name, value in credential.inputs.items():
|
||||||
credential.inputs[field_name] = value
|
if should_decrypt_field(value):
|
||||||
credential.save()
|
value = decrypt_field(credential, field_name)
|
||||||
|
credential.inputs[field_name] = value
|
||||||
|
credential.save()
|
||||||
|
finally:
|
||||||
|
utils.get_current_apps = orig_current_apps
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _unified_jobs(apps):
|
def _unified_jobs(apps):
|
||||||
UnifiedJob = apps.get_model('main', 'UnifiedJob')
|
UnifiedJob = apps.get_model('main', 'UnifiedJob')
|
||||||
for uj in UnifiedJob.objects.all():
|
for uj in UnifiedJob.objects.all():
|
||||||
if uj.start_args is not None:
|
if uj.start_args is not None:
|
||||||
if uj.start_args.startswith('$encrypted$AESCBC$'):
|
if should_decrypt_field(uj.start_args):
|
||||||
continue
|
start_args = decrypt_field(uj, 'start_args')
|
||||||
start_args = decrypt_field(uj, 'start_args')
|
uj.start_args = start_args
|
||||||
uj.start_args = start_args
|
uj.save()
|
||||||
uj.save()
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2017 Ansible, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
import json
|
import json
|
||||||
import pytest
|
import pytest
|
||||||
import mock
|
import mock
|
||||||
@@ -23,6 +27,7 @@ from awx.main.utils import decrypt_field
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_notification_template_migration():
|
def test_notification_template_migration():
|
||||||
|
# Doesn't get tagged as UTF8 because the the internal save call explicitly sets skip_utf8=True
|
||||||
with mock.patch('awx.main.models.notifications.encrypt_field', encrypt_field):
|
with mock.patch('awx.main.models.notifications.encrypt_field', encrypt_field):
|
||||||
nt = NotificationTemplate.objects.create(notification_type='slack', notification_configuration=dict(token='test'))
|
nt = NotificationTemplate.objects.create(notification_type='slack', notification_configuration=dict(token='test'))
|
||||||
|
|
||||||
@@ -42,20 +47,24 @@ def test_notification_template_migration():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_credential_migration():
|
@pytest.mark.parametrize("old_enc, new_enc, value", [
|
||||||
|
('$encrypted$UTF8$AES', '$encrypted$UTF8$AESCBC$', u'Iñtërnâtiônàlizætiøn'),
|
||||||
|
('$encrypted$AES$', '$encrypted$AESCBC$', 'test'),
|
||||||
|
])
|
||||||
|
def test_credential_migration(old_enc, new_enc, value):
|
||||||
with mock.patch('awx.main.models.credential.encrypt_field', encrypt_field):
|
with mock.patch('awx.main.models.credential.encrypt_field', encrypt_field):
|
||||||
cred_type = ssh()
|
cred_type = ssh()
|
||||||
cred_type.save()
|
cred_type.save()
|
||||||
|
|
||||||
cred = Credential.objects.create(credential_type=cred_type, inputs=dict(password='test'))
|
cred = Credential.objects.create(credential_type=cred_type, inputs=dict(password=value))
|
||||||
|
|
||||||
assert cred.password.startswith('$encrypted$AES$')
|
assert cred.password.startswith(old_enc)
|
||||||
|
|
||||||
_credentials(apps)
|
_credentials(apps)
|
||||||
cred.refresh_from_db()
|
cred.refresh_from_db()
|
||||||
|
|
||||||
assert cred.password.startswith('$encrypted$AESCBC$')
|
assert cred.password.startswith(new_enc)
|
||||||
assert decrypt_field(cred, 'password') == 'test'
|
assert decrypt_field(cred, 'password') == value
|
||||||
|
|
||||||
# This is here for a side-effect.
|
# This is here for a side-effect.
|
||||||
# Exception if the encryption type of AESCBC is not properly skipped, ensures
|
# Exception if the encryption type of AESCBC is not properly skipped, ensures
|
||||||
@@ -64,17 +73,21 @@ def test_credential_migration():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_unified_job_migration():
|
@pytest.mark.parametrize("old_enc, new_enc, value", [
|
||||||
|
('$encrypted$AES$', '$encrypted$AESCBC$', u'Iñtërnâtiônàlizætiøn'),
|
||||||
|
('$encrypted$AES$', '$encrypted$AESCBC$', 'test'),
|
||||||
|
])
|
||||||
|
def test_unified_job_migration(old_enc, new_enc, value):
|
||||||
with mock.patch('awx.main.models.base.encrypt_field', encrypt_field):
|
with mock.patch('awx.main.models.base.encrypt_field', encrypt_field):
|
||||||
uj = UnifiedJob.objects.create(launch_type='manual', start_args=json.dumps({'test':'value'}))
|
uj = UnifiedJob.objects.create(launch_type='manual', start_args=json.dumps({'test':value}))
|
||||||
|
|
||||||
assert uj.start_args.startswith('$encrypted$AES$')
|
assert uj.start_args.startswith(old_enc)
|
||||||
|
|
||||||
_unified_jobs(apps)
|
_unified_jobs(apps)
|
||||||
uj.refresh_from_db()
|
uj.refresh_from_db()
|
||||||
|
|
||||||
assert uj.start_args.startswith('$encrypted$AESCBC$')
|
assert uj.start_args.startswith(new_enc)
|
||||||
assert json.loads(decrypt_field(uj, 'start_args')) == {'test':'value'}
|
assert json.loads(decrypt_field(uj, 'start_args')) == {'test':value}
|
||||||
|
|
||||||
# This is here for a side-effect.
|
# This is here for a side-effect.
|
||||||
# Exception if the encryption type of AESCBC is not properly skipped, ensures
|
# Exception if the encryption type of AESCBC is not properly skipped, ensures
|
||||||
|
|||||||
Reference in New Issue
Block a user