Add support for encrypting settings that are passwords.

This commit is contained in:
Chris Church
2016-11-30 11:22:39 -05:00
parent 2efe178fd4
commit 6a02ca1de0
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: