Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Caching of decrypted data #46

Merged
merged 14 commits into from
Apr 26, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config/schema/field_encrypt.schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ field.storage.*.*.third_party.field_encrypt:
encryption_profile:
type: string
label: 'Encryption profile'
uncacheable:
type: boolean
label: 'Exclude from cache'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit ambiguous. We're only focusing on render cache, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's broader than just render cache. When caching the entity object itself, unencrypted field data also shouldn't be stored in the (database) cache...

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think uncacheable would be a better name. It'd also fit in better with the terminology in Drupal core.


field_encrypt.settings:
type: config_object
Expand Down
46 changes: 45 additions & 1 deletion field_encrypt.module
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
* Contains module hooks for field_encrypt.
*/

use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\DynamicallyFieldableEntityStorageInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\field\Entity\FieldStorageConfig;

/**
* Implements hook_form_alter().
Expand Down Expand Up @@ -84,6 +88,19 @@ function field_encrypt_form_alter(&$form, FormStateInterface $form_state, $form_
],
);

// Add setting to decide if field should be excluded from cache.
$form['field_encrypt']['field_encrypt']['uncacheable'] = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome!

'#type' => 'checkbox',
'#title' => t('Uncacheable'),
'#description' => t('Mark this field as uncacheable. This will make sure your unencrypted data will not be exposed in the cache, but will have a negative impact on your performance.'),
'#default_value' => $field->getThirdPartySetting('field_encrypt', 'uncacheable', TRUE),
'#states' => [
'visible' => [
':input[name="field_encrypt[encrypt]"]' => array('checked' => TRUE),
],
],
];

// We add functions to process the form when it is saved.
$form['#entity_builders'][] = 'field_encrypt_form_field_add_form_builder';
}
Expand All @@ -105,7 +122,6 @@ function field_encrypt_form_alter(&$form, FormStateInterface $form_state, $form_
function field_encrypt_form_field_add_form_builder($entity_type, \Drupal\field\Entity\FieldStorageConfig $field_storage_config, &$form, \Drupal\Core\Form\FormStateInterface $form_state) {
$field_encryption_settings = $form_state->getValue('field_encrypt');
$field_encryption_settings['encrypt'] = (bool) $field_encryption_settings['encrypt'];
$original_encryption = $field_storage_config->getThirdPartySettings('field_encrypt');

// If the form has the value, we set it.
if ($field_encryption_settings['encrypt']) {
Expand All @@ -118,6 +134,16 @@ function field_encrypt_form_field_add_form_builder($entity_type, \Drupal\field\E
$field_storage_config->unsetThirdPartySetting('field_encrypt', 'encrypt');
$field_storage_config->unsetThirdPartySetting('field_encrypt', 'properties');
$field_storage_config->unsetThirdPartySetting('field_encrypt', 'encryption_profile');
$field_storage_config->unsetThirdPartySetting('field_encrypt', 'uncacheable');
}
}

/**
* Implements hook_entity_view().
*/
function field_encrypt_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
if (field_encrypt_allow_encryption($entity)) {
\Drupal::service('field_encrypt.process_entities')->entitySetCacheTags($entity, $build);
}
}

Expand Down Expand Up @@ -207,3 +233,21 @@ function field_encrypt_allow_encryption(EntityInterface $entity) {
}
return $allowed;
}

/**
* Implements hook_entity_type_alter().
*/
function field_encrypt_entity_type_alter(array &$entity_types) {
$uncacheable_types = \Drupal::state()->get('uncacheable_entity_types');
if ($uncacheable_types) {
// Mark entity types uncacheable if they contain an encrypted field
// that has been marked uncacheable.
// See Drupal\field_encrypt\EventSubscriber\ConfigSubscriber
// setUncacheableEntityTypes().
foreach ($uncacheable_types as $uncacheable_type) {
$entity_types[$uncacheable_type]->set('static_cache', FALSE);
$entity_types[$uncacheable_type]->set('render_cache', FALSE);
$entity_types[$uncacheable_type]->set('persistent_cache', FALSE);
}
}
}
2 changes: 1 addition & 1 deletion field_encrypt.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ services:

field_encrypt.config_subscriber:
class: Drupal\field_encrypt\EventSubscriber\ConfigSubscriber
arguments: ['@entity_type.manager', '@entity.query', '@queue', '@string_translation', '@field_encrypt.encrypted_field_value_manager']
arguments: ['@entity_type.manager', '@entity.query', '@queue', '@string_translation', '@field_encrypt.encrypted_field_value_manager', '@entity.manager', '@state']
tags:
- { name: event_subscriber }
174 changes: 129 additions & 45 deletions src/EventSubscriber/ConfigSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@

namespace Drupal\field_encrypt\EventSubscriber;

use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\DynamicallyFieldableEntityStorageInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Query\QueryFactory;
use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\State\StateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Url;
Expand All @@ -30,7 +35,7 @@ class ConfigSubscriber implements EventSubscriberInterface {
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityManager;
protected $entityTypeManager;

/**
* The entity query service.
Expand All @@ -53,6 +58,20 @@ class ConfigSubscriber implements EventSubscriberInterface {
*/
protected $encryptedFieldValueManager;

/**
* Entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;

/**
* The state key value store.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;

/**
* Constructs a new ConfigSubscriber object.
*
Expand All @@ -66,13 +85,19 @@ class ConfigSubscriber implements EventSubscriberInterface {
* The string translation service.
* @param \Drupal\field_encrypt\EncryptedFieldValueManagerInterface $encrypted_field_value_manager
* The EncryptedFieldValue entity manager.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\State\StateInterface $state
* The state key value store.
*/
public function __construct(EntityTypeManagerInterface $entity_manager, QueryFactory $entity_query, QueueFactory $queue_factory, TranslationInterface $translation, EncryptedFieldValueManagerInterface $encrypted_field_value_manager) {
$this->entityManager = $entity_manager;
public function __construct(EntityTypeManagerInterface $entity_manager, QueryFactory $entity_query, QueueFactory $queue_factory, TranslationInterface $translation, EncryptedFieldValueManagerInterface $encrypted_field_value_manager, EntityManagerInterface $entity_manager, StateInterface $state) {
$this->entityTypeManager = $entity_manager;
$this->entityQuery = $entity_query;
$this->queueFactory = $queue_factory;
$this->stringTranslation = $translation;
$this->encryptedFieldValueManager = $encrypted_field_value_manager;
$this->entityManager = $entity_manager;
$this->state = $state;
}

/**
Expand All @@ -92,52 +117,57 @@ public static function getSubscribedEvents() {
*/
public function onConfigSave(ConfigCrudEvent $event) {
$config = $event->getConfig();
if (substr($config->getName(), 0, 14) == 'field.storage.' && $event->isChanged('third_party_settings.field_encrypt')) {
// Get both the newly saved and original field_encrypt configuration.
$new_config = $config->get('third_party_settings.field_encrypt');
if (substr($config->getName(), 0, 14) == 'field.storage.') {
// Get the original field_encrypt configuration.
$original_config = $config->getOriginal('third_party_settings.field_encrypt');

// Get the entity type and field from the changed config key.
$storage_name = substr($config->getName(), 14);
list($entity_type, $field_name) = explode('.', $storage_name, 2);

// Load the FieldStorageConfig entity that was updated.
$field_storage_config = FieldStorageConfig::loadByName($entity_type, $field_name);
if ($field_storage_config) {
if ($field_storage_config->hasData()) {
// Get entities that need updating, because they contain the field
// that has its field encryption settings updated.
$query = $this->entityQuery->get($entity_type);
// Check if the field is present.
$query->exists($field_name);
// Make sure to get all revisions for revisionable entities.
if ($this->entityManager->getDefinition($entity_type)
->hasKey('revision')
) {
$query->allRevisions();
}
$entity_ids = $query->execute();

if (!empty($entity_ids)) {
// Call the Queue API and add items for processing.
/** @var QueueInterface $queue */
$queue = $this->queueFactory->get('cron_encrypted_field_update');

foreach (array_keys($entity_ids) as $entity_id) {
$data = [
"entity_id" => $entity_id,
"field_name" => $field_name,
"entity_type" => $entity_type,
"original_config" => $original_config,
];
$queue->createItem($data);
// Update the uncacheable entity types list.
$this->setUncacheableEntityTypes();

// Update existing entities, if data encryption settings changed.
if ($this->encryptionConfigChanged($config)) {
// Get the entity type and field from the changed config key.
$storage_name = substr($config->getName(), 14);
list($entity_type, $field_name) = explode('.', $storage_name, 2);

// Load the FieldStorageConfig entity that was updated.
$field_storage_config = FieldStorageConfig::loadByName($entity_type, $field_name);
if ($field_storage_config) {
if ($field_storage_config->hasData()) {
// Get entities that need updating, because they contain the field
// that has its field encryption settings updated.
$query = $this->entityQuery->get($entity_type);
// Check if the field is present.
$query->exists($field_name);
// Make sure to get all revisions for revisionable entities.
if ($this->entityTypeManager->getDefinition($entity_type)
->hasKey('revision')
) {
$query->allRevisions();
}
$entity_ids = $query->execute();

if (!empty($entity_ids)) {
// Call the Queue API and add items for processing.
/** @var QueueInterface $queue */
$queue = $this->queueFactory->get('cron_encrypted_field_update');

foreach (array_keys($entity_ids) as $entity_id) {
$data = [
"entity_id" => $entity_id,
"field_name" => $field_name,
"entity_type" => $entity_type,
"original_config" => $original_config,
];
$queue->createItem($data);
}
}
}

drupal_set_message($this->t('Updates to entities with existing data for this field have been queued to be processed. You should immediately <a href=":url">run this process manually</a>. Alternatively, the updates will be performed automatically by cron.', array(
':url' => Url::fromRoute('field_encrypt.field_update')
->toString()
)));
drupal_set_message($this->t('Updates to entities with existing data for this field have been queued to be processed. You should immediately <a href=":url">run this process manually</a>. Alternatively, the updates will be performed automatically by cron.', array(
':url' => Url::fromRoute('field_encrypt.field_update')
->toString()
)));
}
}
}
}
Expand All @@ -159,4 +189,58 @@ public function onConfigDelete(ConfigCrudEvent $event) {
}
}

/**
* Check whether the field encryption config has changed.
*
* @param \Drupal\Core\Config\Config $config
* The config to check.
*
* @return bool
* Whether the config has changed.
*/
protected function encryptionConfigChanged(Config $config) {
// Get both the newly saved and original field_encrypt configuration.
$new_config = $config->get('third_party_settings.field_encrypt');
$original_config = $config->getOriginal('third_party_settings.field_encrypt');

// Don't compare 'uncacheable' setting.
unset($new_config['uncacheable']);
unset($original_config['uncacheable']);
return $new_config !== $original_config;
}

/**
* Figure out which entity types are uncacheable due to encrypted fields.
*/
protected function setUncacheableEntityTypes() {
$types = [];
$entity_types = $this->entityTypeManager->getDefinitions();
foreach ($entity_types as $entity_type) {
if ($entity_type instanceof ContentEntityTypeInterface) {
$storage_class = $this->entityManager->createHandlerInstance($entity_type->getStorageClass(), $entity_type);
if ($storage_class instanceof DynamicallyFieldableEntityStorageInterface) {
// Query by filtering on the ID as this is more efficient than filtering
// on the entity_type property directly.
$ids = $this->entityQuery->get('field_storage_config')
->condition('id', $entity_type->id() . '.', 'STARTS_WITH')
->execute();
// Fetch all fields on entity type.
$field_storages = FieldStorageConfig::loadMultiple($ids);
if ($field_storages) {
foreach ($field_storages as $storage) {
// Check if field is encrypted.
if ($storage->getThirdPartySetting('field_encrypt', 'uncacheable', FALSE) == TRUE) {
// If there is an encrypted field, mark this entity type as
// uncacheable.
$type = $storage->getTargetEntityTypeId();
$types[$type] = $type;
}
}
}
}
}
}
$this->state->set('uncacheable_entity_types', $types);
}

}
37 changes: 37 additions & 0 deletions src/FieldEncryptProcessEntities.php
Original file line number Diff line number Diff line change
Expand Up @@ -338,4 +338,41 @@ public function updateStoredField($field_name, $field_entity_type, $original_enc
}
}

/**
* {@inheritdoc}
*/
public function entitySetCacheTags(ContentEntityInterface $entity, &$build) {
$uncacheable_fields = $this->getUncacheableFields($entity);
foreach ($uncacheable_fields as $field_name) {
$build[$field_name]['#cache']['max-age'] = 0;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only helps for render arrays, not for any caches where entities are being used.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IOW: this should not even be necessary, given changes elsewhere.

Furthermore, the function name doesn't match what it does.

}
}

/**
* Get field names for an entity that are set to be excluded from cache.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity to check.
*
* @return array
* List of field names that are excluded from cache.
*/
protected function getUncacheableFields(ContentEntityInterface $entity) {
$uncacheable_fields = [];
foreach ($entity->getFields() as $field) {
if ($this->checkField($field)) {
/* @var $definition \Drupal\Core\Field\BaseFieldDefinition */
$definition = $field->getFieldDefinition();
/* @var $storage \Drupal\Core\Field\FieldConfigStorageBase */
$storage = $definition->get('fieldStorage');

// If uncacheable is set, set caching max-age to 0.
if ($storage->getThirdPartySetting('field_encrypt', 'uncacheable', FALSE) == TRUE) {
$uncacheable_fields[] = $field->getName();
}
}
}
return $uncacheable_fields;
}

}
10 changes: 10 additions & 0 deletions src/FieldEncryptProcessEntitiesInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,14 @@ public function decryptEntity(ContentEntityInterface $entity);
*/
public function updateStoredField($field_name, $field_entity_type, $original_encryption_settings, $entity_id);

/**
* Set the cache tags correctly for each encrypted field on an entity.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity whose fields to set cache tags on.
* @param $build
* The entity render array.
*/
public function entitySetCacheTags(ContentEntityInterface $entity, &$build);

}
Loading