1
0
mirror of https://github.com/ansible/awx.git synced 2024-11-01 08:21:15 +03:00

Merge pull request #4168 from cchurch/encrypt-that-ctit

Add support for encrypting settings that are passwords.
This commit is contained in:
Chris Church 2016-12-02 16:50:23 -05:00 committed by GitHub
commit 15bffdf861
8 changed files with 90 additions and 2 deletions

View File

@ -81,6 +81,9 @@ register(
# Optional; licensed feature required to be able to view or modify this
# setting.
feature_required='rebranding',
# Optional; field is stored encrypted in the database and only $encrypted$
# is returned via the API.
encrypted=True,
)
register(

View File

@ -10,6 +10,8 @@ from django.db import models
# Tower
from awx.main.models.base import CreatedModifiedModel
from awx.main.fields import JSONField
from awx.main.utils import encrypt_field
from awx.conf import settings_registry
__all__ = ['Setting']
@ -42,6 +44,30 @@ class Setting(CreatedModifiedModel):
else:
return u'{} = {}'.format(self.key, json_value)
def save(self, *args, **kwargs):
encrypted = settings_registry.is_setting_encrypted(self.key)
new_instance = not bool(self.pk)
# If update_fields has been specified, add our field names to it,
# if it hasn't been specified, then we're just doing a normal save.
update_fields = kwargs.get('update_fields', [])
# When first saving to the database, don't store any encrypted field
# value, but instead save it until after the instance is created.
# Otherwise, store encrypted value to the database.
if encrypted:
if new_instance:
self._saved_value = self.value
self.value = ''
else:
self.value = encrypt_field(self, 'value')
if 'value' not in update_fields:
update_fields.append('value')
super(Setting, self).save(*args, **kwargs)
# After saving a new instance for the first time, set the encrypted
# field and save again.
if encrypted and new_instance:
self.value = self._saved_value
self.save(update_fields=['value'])
@classmethod
def get_cache_key(self, key):
return key

View File

@ -90,6 +90,9 @@ class SettingsRegistry(object):
setting_names.append(setting)
return setting_names
def is_setting_encrypted(self, setting):
return bool(self._registry.get(setting, {}).get('encrypted', False))
def get_setting_field(self, setting, mixin_class=None, for_user=False, **kwargs):
from django.conf import settings
from rest_framework.fields import empty
@ -104,6 +107,7 @@ class SettingsRegistry(object):
depends_on = frozenset(field_kwargs.pop('depends_on', None) or [])
placeholder = field_kwargs.pop('placeholder', empty)
feature_required = field_kwargs.pop('feature_required', empty)
encrypted = bool(field_kwargs.pop('encrypted', False))
if getattr(field_kwargs.get('child', None), 'source', None) is not None:
field_kwargs['child'].source = None
field_instance = field_class(**field_kwargs)
@ -114,6 +118,7 @@ class SettingsRegistry(object):
field_instance.placeholder = placeholder
if feature_required is not empty:
field_instance.feature_required = feature_required
field_instance.encrypted = encrypted
original_field_instance = field_instance
if field_class != original_field_class:
original_field_instance = original_field_class(**field_kwargs)

View File

@ -45,6 +45,8 @@ class SettingFieldMixin(object):
"""Mixin to use a registered setting field class for API display/validation."""
def to_representation(self, obj):
if getattr(self, 'encrypted', False) and isinstance(obj, basestring) and obj:
return '$encrypted$'
return obj
def to_internal_value(self, value):

View File

@ -15,6 +15,7 @@ from django.db import ProgrammingError, OperationalError
from rest_framework.fields import empty, SkipField
# Tower
from awx.main.utils import decrypt_field
from awx.conf import settings_registry
from awx.conf.models import Setting
@ -121,7 +122,11 @@ class SettingsWrapper(UserSettingsHolder):
for setting in Setting.objects.filter(key__in=settings_to_cache.keys(), user__isnull=True).order_by('pk'):
if settings_to_cache[setting.key] != SETTING_CACHE_NOTSET:
continue
settings_to_cache[setting.key] = self._get_cache_value(setting.value)
if settings_registry.is_setting_encrypted(setting.key):
value = decrypt_field(setting, 'value')
else:
value = setting.value
settings_to_cache[setting.key] = self._get_cache_value(value)
# Load field default value for any settings not found in the database.
if SETTING_CACHE_DEFAULTS:
for key, value in settings_to_cache.items():
@ -159,7 +164,10 @@ class SettingsWrapper(UserSettingsHolder):
if not field.read_only:
setting = Setting.objects.filter(key=name, user__isnull=True).order_by('pk').first()
if setting:
value = setting.value
if getattr(field, 'encrypted', False):
value = decrypt_field(setting, 'value')
else:
value = setting.value
else:
value = SETTING_CACHE_NOTSET
if SETTING_CACHE_DEFAULTS:

View File

@ -103,6 +103,8 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
for key, value in serializer.validated_data.items():
if key == 'LICENSE':
continue
if settings_registry.is_setting_encrypted(key) and isinstance(value, basestring) and value.startswith('$encrypted$'):
continue
setattr(serializer.instance, key, value)
setting = settings_qs.filter(key=key).order_by('pk').first()
if not setting:

View File

@ -65,6 +65,40 @@ def test_ldap_settings(get, put, patch, delete, admin, enterprise_license):
patch(url, user=admin, data={'AUTH_LDAP_SERVER_URI': 'ldap://ldap.example.com, ldap://ldap2.example.com'}, expect=200)
@pytest.mark.django_db
def test_radius_settings(get, put, patch, delete, admin, enterprise_license, settings):
url = reverse('api:setting_singleton_detail', args=('radius',))
get(url, user=admin, expect=404)
Setting.objects.create(key='LICENSE', value=enterprise_license)
response = get(url, user=admin, expect=200)
put(url, user=admin, data=response.data, expect=200)
# Set secret via the API.
patch(url, user=admin, data={'RADIUS_SECRET': 'mysecret'}, expect=200)
response = get(url, user=admin, expect=200)
assert response.data['RADIUS_SECRET'] == '$encrypted$'
assert Setting.objects.filter(key='RADIUS_SECRET').first().value.startswith('$encrypted$')
assert settings.RADIUS_SECRET == 'mysecret'
# Set secret via settings wrapper.
settings_wrapper = settings._awx_conf_settings
settings_wrapper.RADIUS_SECRET = 'mysecret2'
response = get(url, user=admin, expect=200)
assert response.data['RADIUS_SECRET'] == '$encrypted$'
assert Setting.objects.filter(key='RADIUS_SECRET').first().value.startswith('$encrypted$')
assert settings.RADIUS_SECRET == 'mysecret2'
# If we send back $encrypted$, the setting is not updated.
patch(url, user=admin, data={'RADIUS_SECRET': '$encrypted$'}, expect=200)
response = get(url, user=admin, expect=200)
assert response.data['RADIUS_SECRET'] == '$encrypted$'
assert Setting.objects.filter(key='RADIUS_SECRET').first().value.startswith('$encrypted$')
assert settings.RADIUS_SECRET == 'mysecret2'
# If we send an empty string, the setting is also set to an empty string.
patch(url, user=admin, data={'RADIUS_SECRET': ''}, expect=200)
response = get(url, user=admin, expect=200)
assert response.data['RADIUS_SECRET'] == ''
assert Setting.objects.filter(key='RADIUS_SECRET').first().value == ''
assert settings.RADIUS_SECRET == ''
@pytest.mark.django_db
def test_ui_settings(get, put, patch, delete, admin, enterprise_license):
url = reverse('api:setting_singleton_detail', args=('ui',))

View File

@ -211,6 +211,7 @@ register(
category=_('LDAP'),
category_slug='ldap',
feature_required='ldap',
encrypted=True,
)
register(
@ -511,6 +512,7 @@ register(
category=_('RADIUS'),
category_slug='radius',
feature_required='enterprise_auth',
encrypted=True,
)
###############################################################################
@ -552,6 +554,7 @@ register(
category=_('Google OAuth2'),
category_slug='google-oauth2',
placeholder='q2fMVCmEregbg-drvebPp8OW',
encrypted=True,
)
register(
@ -641,6 +644,7 @@ register(
help_text=_('The OAuth2 secret (Client Secret) from your GitHub developer application.'),
category=_('GitHub OAuth2'),
category_slug='github',
encrypted=True,
)
register(
@ -704,6 +708,7 @@ register(
help_text=_('The OAuth2 secret (Client Secret) from your GitHub organization application.'),
category=_('GitHub Organization OAuth2'),
category_slug='github-org',
encrypted=True,
)
register(
@ -778,6 +783,7 @@ register(
help_text=_('The OAuth2 secret (Client Secret) from your GitHub organization application.'),
category=_('GitHub Team OAuth2'),
category_slug='github-team',
encrypted=True,
)
register(
@ -852,6 +858,7 @@ register(
help_text=_('The OAuth2 secret (Client Secret) from your Azure AD application.'),
category=_('Azure AD OAuth2'),
category_slug='azuread-oauth2',
encrypted=True,
)
register(
@ -955,6 +962,7 @@ register(
category=_('SAML'),
category_slug='saml',
feature_required='enterprise_auth',
encrypted=True,
)
register(