mirror of
https://github.com/ansible/awx.git
synced 2026-02-16 02:30:01 -03:30
Add support for encrypting settings that are passwords.
This commit is contained in:
@@ -81,6 +81,9 @@ register(
|
|||||||
# Optional; licensed feature required to be able to view or modify this
|
# Optional; licensed feature required to be able to view or modify this
|
||||||
# setting.
|
# setting.
|
||||||
feature_required='rebranding',
|
feature_required='rebranding',
|
||||||
|
# Optional; field is stored encrypted in the database and only $encrypted$
|
||||||
|
# is returned via the API.
|
||||||
|
encrypted=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
register(
|
register(
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ from django.db import models
|
|||||||
# Tower
|
# Tower
|
||||||
from awx.main.models.base import CreatedModifiedModel
|
from awx.main.models.base import CreatedModifiedModel
|
||||||
from awx.main.fields import JSONField
|
from awx.main.fields import JSONField
|
||||||
|
from awx.main.utils import encrypt_field
|
||||||
|
from awx.conf import settings_registry
|
||||||
|
|
||||||
__all__ = ['Setting']
|
__all__ = ['Setting']
|
||||||
|
|
||||||
@@ -42,6 +44,30 @@ class Setting(CreatedModifiedModel):
|
|||||||
else:
|
else:
|
||||||
return u'{} = {}'.format(self.key, json_value)
|
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
|
@classmethod
|
||||||
def get_cache_key(self, key):
|
def get_cache_key(self, key):
|
||||||
return key
|
return key
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ class SettingsRegistry(object):
|
|||||||
setting_names.append(setting)
|
setting_names.append(setting)
|
||||||
return setting_names
|
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):
|
def get_setting_field(self, setting, mixin_class=None, for_user=False, **kwargs):
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from rest_framework.fields import empty
|
from rest_framework.fields import empty
|
||||||
@@ -104,6 +107,7 @@ class SettingsRegistry(object):
|
|||||||
depends_on = frozenset(field_kwargs.pop('depends_on', None) or [])
|
depends_on = frozenset(field_kwargs.pop('depends_on', None) or [])
|
||||||
placeholder = field_kwargs.pop('placeholder', empty)
|
placeholder = field_kwargs.pop('placeholder', empty)
|
||||||
feature_required = field_kwargs.pop('feature_required', 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:
|
if getattr(field_kwargs.get('child', None), 'source', None) is not None:
|
||||||
field_kwargs['child'].source = None
|
field_kwargs['child'].source = None
|
||||||
field_instance = field_class(**field_kwargs)
|
field_instance = field_class(**field_kwargs)
|
||||||
@@ -114,6 +118,7 @@ class SettingsRegistry(object):
|
|||||||
field_instance.placeholder = placeholder
|
field_instance.placeholder = placeholder
|
||||||
if feature_required is not empty:
|
if feature_required is not empty:
|
||||||
field_instance.feature_required = feature_required
|
field_instance.feature_required = feature_required
|
||||||
|
field_instance.encrypted = encrypted
|
||||||
original_field_instance = field_instance
|
original_field_instance = field_instance
|
||||||
if field_class != original_field_class:
|
if field_class != original_field_class:
|
||||||
original_field_instance = original_field_class(**field_kwargs)
|
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."""
|
"""Mixin to use a registered setting field class for API display/validation."""
|
||||||
|
|
||||||
def to_representation(self, obj):
|
def to_representation(self, obj):
|
||||||
|
if getattr(self, 'encrypted', False) and isinstance(obj, basestring) and obj:
|
||||||
|
return '$encrypted$'
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def to_internal_value(self, value):
|
def to_internal_value(self, value):
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from django.db import ProgrammingError, OperationalError
|
|||||||
from rest_framework.fields import empty, SkipField
|
from rest_framework.fields import empty, SkipField
|
||||||
|
|
||||||
# Tower
|
# Tower
|
||||||
|
from awx.main.utils import decrypt_field
|
||||||
from awx.conf import settings_registry
|
from awx.conf import settings_registry
|
||||||
from awx.conf.models import Setting
|
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'):
|
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:
|
if settings_to_cache[setting.key] != SETTING_CACHE_NOTSET:
|
||||||
continue
|
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.
|
# Load field default value for any settings not found in the database.
|
||||||
if SETTING_CACHE_DEFAULTS:
|
if SETTING_CACHE_DEFAULTS:
|
||||||
for key, value in settings_to_cache.items():
|
for key, value in settings_to_cache.items():
|
||||||
@@ -159,7 +164,10 @@ class SettingsWrapper(UserSettingsHolder):
|
|||||||
if not field.read_only:
|
if not field.read_only:
|
||||||
setting = Setting.objects.filter(key=name, user__isnull=True).order_by('pk').first()
|
setting = Setting.objects.filter(key=name, user__isnull=True).order_by('pk').first()
|
||||||
if setting:
|
if setting:
|
||||||
value = setting.value
|
if getattr(field, 'encrypted', False):
|
||||||
|
value = decrypt_field(setting, 'value')
|
||||||
|
else:
|
||||||
|
value = setting.value
|
||||||
else:
|
else:
|
||||||
value = SETTING_CACHE_NOTSET
|
value = SETTING_CACHE_NOTSET
|
||||||
if SETTING_CACHE_DEFAULTS:
|
if SETTING_CACHE_DEFAULTS:
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
|
|||||||
for key, value in serializer.validated_data.items():
|
for key, value in serializer.validated_data.items():
|
||||||
if key == 'LICENSE':
|
if key == 'LICENSE':
|
||||||
continue
|
continue
|
||||||
|
if settings_registry.is_setting_encrypted(key) and isinstance(value, basestring) and value.startswith('$encrypted$'):
|
||||||
|
continue
|
||||||
setattr(serializer.instance, key, value)
|
setattr(serializer.instance, key, value)
|
||||||
setting = settings_qs.filter(key=key).order_by('pk').first()
|
setting = settings_qs.filter(key=key).order_by('pk').first()
|
||||||
if not setting:
|
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)
|
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
|
@pytest.mark.django_db
|
||||||
def test_ui_settings(get, put, patch, delete, admin, enterprise_license):
|
def test_ui_settings(get, put, patch, delete, admin, enterprise_license):
|
||||||
url = reverse('api:setting_singleton_detail', args=('ui',))
|
url = reverse('api:setting_singleton_detail', args=('ui',))
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ register(
|
|||||||
category=_('LDAP'),
|
category=_('LDAP'),
|
||||||
category_slug='ldap',
|
category_slug='ldap',
|
||||||
feature_required='ldap',
|
feature_required='ldap',
|
||||||
|
encrypted=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
register(
|
register(
|
||||||
@@ -511,6 +512,7 @@ register(
|
|||||||
category=_('RADIUS'),
|
category=_('RADIUS'),
|
||||||
category_slug='radius',
|
category_slug='radius',
|
||||||
feature_required='enterprise_auth',
|
feature_required='enterprise_auth',
|
||||||
|
encrypted=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
@@ -552,6 +554,7 @@ register(
|
|||||||
category=_('Google OAuth2'),
|
category=_('Google OAuth2'),
|
||||||
category_slug='google-oauth2',
|
category_slug='google-oauth2',
|
||||||
placeholder='q2fMVCmEregbg-drvebPp8OW',
|
placeholder='q2fMVCmEregbg-drvebPp8OW',
|
||||||
|
encrypted=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
register(
|
register(
|
||||||
@@ -641,6 +644,7 @@ register(
|
|||||||
help_text=_('The OAuth2 secret (Client Secret) from your GitHub developer application.'),
|
help_text=_('The OAuth2 secret (Client Secret) from your GitHub developer application.'),
|
||||||
category=_('GitHub OAuth2'),
|
category=_('GitHub OAuth2'),
|
||||||
category_slug='github',
|
category_slug='github',
|
||||||
|
encrypted=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
register(
|
register(
|
||||||
@@ -704,6 +708,7 @@ register(
|
|||||||
help_text=_('The OAuth2 secret (Client Secret) from your GitHub organization application.'),
|
help_text=_('The OAuth2 secret (Client Secret) from your GitHub organization application.'),
|
||||||
category=_('GitHub Organization OAuth2'),
|
category=_('GitHub Organization OAuth2'),
|
||||||
category_slug='github-org',
|
category_slug='github-org',
|
||||||
|
encrypted=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
register(
|
register(
|
||||||
@@ -778,6 +783,7 @@ register(
|
|||||||
help_text=_('The OAuth2 secret (Client Secret) from your GitHub organization application.'),
|
help_text=_('The OAuth2 secret (Client Secret) from your GitHub organization application.'),
|
||||||
category=_('GitHub Team OAuth2'),
|
category=_('GitHub Team OAuth2'),
|
||||||
category_slug='github-team',
|
category_slug='github-team',
|
||||||
|
encrypted=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
register(
|
register(
|
||||||
@@ -852,6 +858,7 @@ register(
|
|||||||
help_text=_('The OAuth2 secret (Client Secret) from your Azure AD application.'),
|
help_text=_('The OAuth2 secret (Client Secret) from your Azure AD application.'),
|
||||||
category=_('Azure AD OAuth2'),
|
category=_('Azure AD OAuth2'),
|
||||||
category_slug='azuread-oauth2',
|
category_slug='azuread-oauth2',
|
||||||
|
encrypted=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
register(
|
register(
|
||||||
@@ -955,6 +962,7 @@ register(
|
|||||||
category=_('SAML'),
|
category=_('SAML'),
|
||||||
category_slug='saml',
|
category_slug='saml',
|
||||||
feature_required='enterprise_auth',
|
feature_required='enterprise_auth',
|
||||||
|
encrypted=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
register(
|
register(
|
||||||
|
|||||||
Reference in New Issue
Block a user