diff --git a/awx/conf/models.py b/awx/conf/models.py index afdacb1757..7dae8bc77e 100644 --- a/awx/conf/models.py +++ b/awx/conf/models.py @@ -74,6 +74,10 @@ class Setting(CreatedModifiedModel): def get_cache_key(self, key): return key + @classmethod + def get_cache_id_key(self, key): + return '{}_ID'.format(key) + import awx.conf.signals # noqa diff --git a/awx/conf/settings.py b/awx/conf/settings.py index 89bfa0660a..d288ba3a16 100644 --- a/awx/conf/settings.py +++ b/awx/conf/settings.py @@ -124,9 +124,12 @@ class EncryptedCacheProxy(object): if value is not empty and self.registry.is_setting_encrypted(key): # If the setting exists in the database, we'll use its primary key # as part of the AES key when encrypting/decrypting + obj_id = self.cache.get(Setting.get_cache_id_key(key), default=empty) + if obj_id is empty: + obj_id = getattr(self._get_setting_from_db(key), 'pk', None) return method( TransientSetting( - pk=getattr(self._get_setting_from_db(key), 'pk', None), + pk=obj_id, value=value ), 'value' @@ -241,11 +244,13 @@ class SettingsWrapper(UserSettingsHolder): # to indicate from the cache that the setting is not configured without # a database lookup. settings_to_cache = get_settings_to_cache(self.registry) + setting_ids = {} # Load all settings defined in the database. 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 if self.registry.is_setting_encrypted(setting.key): + setting_ids[setting.key] = setting.id try: value = decrypt_field(setting, 'value') except ValueError, e: @@ -268,6 +273,9 @@ class SettingsWrapper(UserSettingsHolder): pass # Generate a cache key for each setting and store them all at once. settings_to_cache = dict([(Setting.get_cache_key(k), v) for k, v in settings_to_cache.items()]) + for k, id_val in setting_ids.items(): + logger.debug('Saving id in cache for encrypted setting %s', k) + self.cache.set(Setting.get_cache_id_key(k), id_val) settings_to_cache['_awx_conf_preload_expires'] = self._awx_conf_preload_expires logger.debug('cache set_many(%r, %r)', settings_to_cache, SETTING_CACHE_TIMEOUT) self.cache.set_many(settings_to_cache, timeout=SETTING_CACHE_TIMEOUT) @@ -293,6 +301,7 @@ class SettingsWrapper(UserSettingsHolder): field = self.registry.get_setting_field(name) if value is empty: setting = None + setting_id = None if not field.read_only or name in ( # these two values are read-only - however - we *do* want # to fetch their value from the database @@ -303,6 +312,7 @@ class SettingsWrapper(UserSettingsHolder): if setting: if getattr(field, 'encrypted', False): value = decrypt_field(setting, 'value') + setting_id = setting.id else: value = setting.value else: @@ -319,6 +329,9 @@ class SettingsWrapper(UserSettingsHolder): logger.debug('cache set(%r, %r, %r)', cache_key, get_cache_value(value), SETTING_CACHE_TIMEOUT) + if setting_id: + logger.debug('Saving id in cache for encrypted setting %s', cache_key) + self.cache.set(Setting.get_cache_id_key(cache_key), setting_id) self.cache.set(cache_key, get_cache_value(value), timeout=SETTING_CACHE_TIMEOUT) if value == SETTING_CACHE_NOTSET and not SETTING_CACHE_DEFAULTS: try: diff --git a/awx/conf/tests/unit/test_settings.py b/awx/conf/tests/unit/test_settings.py index f7f1540108..2f3c197478 100644 --- a/awx/conf/tests/unit/test_settings.py +++ b/awx/conf/tests/unit/test_settings.py @@ -391,7 +391,20 @@ def test_charfield_properly_sets_none(settings, mocker): ) -def test_settings_use_an_encrypted_cache(settings): +def test_settings_use_cache(settings, mocker): + settings.registry.register( + 'AWX_VAR', + field_class=fields.CharField, + category=_('System'), + category_slug='system' + ) + settings.cache.set('AWX_VAR', 'foobar') + settings.cache.set('_awx_conf_preload_expires', 100) + # Will fail test if database is used + getattr(settings, 'AWX_VAR') + + +def test_settings_use_an_encrypted_cache(settings, mocker): settings.registry.register( 'AWX_ENCRYPTED', field_class=fields.CharField, @@ -402,6 +415,11 @@ def test_settings_use_an_encrypted_cache(settings): assert isinstance(settings.cache, EncryptedCacheProxy) assert settings.cache.__dict__['encrypter'] == encrypt_field assert settings.cache.__dict__['decrypter'] == decrypt_field + settings.cache.set('AWX_ENCRYPTED_ID', 402) + settings.cache.set('AWX_ENCRYPTED', 'foobar') + settings.cache.set('_awx_conf_preload_expires', 100) + # Will fail test if database is used + getattr(settings, 'AWX_ENCRYPTED') def test_sensitive_cache_data_is_encrypted(settings, mocker):