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:
commit
15bffdf861
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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',))
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user