Commit af765ffa authored by azteroid's avatar azteroid
Browse files

chore: update default module config

parent f6c7fc0a
Loading
Loading
Loading
Loading
+3 −0
Changes for CHANGELOG.md: 3 added lines, 0 removed lines.
Original line number Diff line number Diff line
# Changelog

## 3.2.2
- fix: Generate a site-specific encryption key on install and update, and check it in the status report.

## 3.2.1
- fix: library sometimes resolved model paths incorrectly.

+1 −0
Changes for cms_content_sync.info.yml: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@ dependencies:
  - drupal:rest
  - drupal:serialization
  - encrypt:encrypt
  - key:key
  - real_aes:real_aes
  - drupal:menu_link_content

+191 −1
Changes for cms_content_sync.install: 191 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -9,12 +9,14 @@ use Drupal\cms_content_sync\Controller\AuthenticationByUser;
use Drupal\cms_content_sync\Controller\ContentSyncSettings;
use Drupal\cms_content_sync\Entity\Flow;
use Drupal\cms_content_sync\Entity\Pool;
use Drupal\cms_content_sync\Plugin\KeyProvider\StateKeyProvider;
use Drupal\cms_content_sync\SyncCoreInterface\SyncCoreFactory;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Link;
use Drupal\Core\Site\Settings;
use Drupal\encrypt\Entity\EncryptionProfile;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
use EdgeBox\SyncCore\Interfaces\IApplicationInterface;
@@ -53,6 +55,74 @@ define('CMS_CONTENT_SYNC_REQUIRED_CONFIG', [
  'user.role.cms_content_sync',
]);

/**
 * The Base64-encoded default encryption key value of earlier releases.
 *
 * Used to detect sites that still use it; see cms_content_sync_update_8037()
 * and cms_content_sync_requirements().
 */
define('CMS_CONTENT_SYNC_LEGACY_KEY_VALUE', 'Ly8gQHRvZG86IE5lZWQgdG8gYmUgY2hhbmdlZCoqKio=');

/**
 * Generate a fresh, site-specific encryption key value.
 *
 * The value is 256 bit of random data, stored Base64-encoded in the site
 * state by the cms_content_sync_state key provider. Cached copies of the key
 * are discarded so that the next encryption uses the new value.
 */
function _cms_content_sync_generate_encryption_key() {
  \Drupal::state()->set(StateKeyProvider::STATE_KEY, base64_encode(random_bytes(32)));

  $entity_type_manager = \Drupal::entityTypeManager();
  $entity_type_manager->getStorage('key')->resetCache(['cms_content_sync']);
  $entity_type_manager->getStorage('encryption_profile')->resetCache([CMS_CONTENT_SYNC_ENCRYPTION_PROFILE_NAME]);

  $key = \Drupal::service('key.repository')->getKey('cms_content_sync');
  if ($key) {
    $key->getKeyValue(TRUE);
  }
}

/**
 * Get the value of the encryption key that is active right now.
 *
 * @return string|null
 *   The raw key value, an empty string if the key has no value, or NULL if
 *   the key entity doesn't exist.
 */
function _cms_content_sync_encryption_key_value() {
  $key = \Drupal::service('key.repository')->getKey('cms_content_sync');

  return $key ? (string) $key->getKeyValue(TRUE) : NULL;
}

/**
 * Decrypt the given values with an explicit key value.
 *
 * Used by cms_content_sync_update_8037() to read credentials that were
 * encrypted with the default key value of earlier releases.
 *
 * @param array $values
 *   The encrypted values.
 * @param string $key_value
 *   The raw key value to decrypt them with.
 *
 * @return array
 *   The decrypted values.
 *
 * @throws \Drupal\encrypt\Exception\EncryptException
 *   If a value can't be decrypted with the given key.
 */
function _cms_content_sync_decrypt_values_with_key(array $values, $key_value) {
  $method = EncryptionProfile::load(CMS_CONTENT_SYNC_ENCRYPTION_PROFILE_NAME)->getEncryptionMethod();

  foreach ($values as $key => $value) {
    $values[$key] = $method->decrypt($value, $key_value);
  }

  return $values;
}

/**
 * Update the weight of the Content Sync module. As Content Sync depends on
 * other modules being run before it, we need to make sure our hooks like
@@ -102,6 +172,11 @@ function cms_content_sync_install() {
    $config_storage->write($name, $source->read($name));
  }

  // Every site gets its own encryption key. The key entity above only points
  // at the site state; the value must be generated before anything is
  // encrypted with it.
  _cms_content_sync_generate_encryption_key();

  $username = 'CMS Content Sync';
  Drupal::moduleHandler()->alter('cms_content_sync_username', $username);
  $data = [
@@ -193,6 +268,7 @@ function cms_content_sync_uninstall() {
  $state->delete('cms_content_sync.base_url');
  $state->delete('cms_content_sync.site_id');
  $state->delete('cms_content_sync.site_uuid');
  $state->delete(StateKeyProvider::STATE_KEY);
}

/**
@@ -219,6 +295,43 @@ function cms_content_sync_requirements($phase) {
      ];
    }

    // The encryption key must be unique to this site.
    $key_value = _cms_content_sync_encryption_key_value();
    if (NULL !== $key_value) {
      if (base64_encode($key_value) === CMS_CONTENT_SYNC_LEGACY_KEY_VALUE) {
        $requirements['cms_content_sync_encryption_key'] = [
          'title' => t('Content Sync: Encryption key'),
          'value' => t('The encryption key is still the default value of earlier releases of Content Sync. Run the database updates (drush updb). If the key was re-imported from your configuration sync directory afterwards, export the configuration again (drush cex) so that the directory no longer contains a key value.'),
          'severity' => REQUIREMENT_ERROR,
        ];
      }
      elseif ('' === $key_value) {
        $requirements['cms_content_sync_encryption_key'] = [
          'title' => t('Content Sync: Encryption key'),
          'value' => t('No encryption key value is stored for this site. Run the database updates (drush updb) or re-install the module.'),
          'severity' => REQUIREMENT_ERROR,
        ];
      }
    }

    // Warn before a configuration import replaces the site-specific key with
    // a value stored in configuration.
    try {
      $sync_storage = \Drupal::service('config.storage.sync');
      $exported_key = $sync_storage->exists('key.key.cms_content_sync') ? $sync_storage->read('key.key.cms_content_sync') : NULL;
    }
    catch (\Exception $e) {
      // No configuration sync directory is defined.
      $exported_key = NULL;
    }
    if (!empty($exported_key['key_provider_settings']['key_value'])) {
      $requirements['cms_content_sync_encryption_key_export'] = [
        'title' => t('Content Sync: Exported encryption key'),
        'value' => t('The configuration sync directory contains a key value for key.key.cms_content_sync. Importing it would replace the site-specific encryption key with a value that is stored in configuration. Export the configuration again (drush cex).'),
        'severity' => REQUIREMENT_WARNING,
      ];
    }

    // Show error when the Content Sync user doesn't have the Content Sync role.
    $users = Drupal::entityTypeManager()
      ->getStorage('user')
@@ -261,11 +374,24 @@ function cms_content_sync_requirements($phase) {
      }

      $auth = AuthenticationByUser::getInstance();
      try {
        $expected_password = $auth->getPassword();
      }
      catch (\Exception $e) {
        $expected_password = NULL;
        $requirements['cms_content_sync_user_password'] = [
          'title' => t('Content Sync: User password'),
          'value' => t('The stored credentials of the service user "@username" cannot be read: @message Set a new password for the service user, then re-register the site.', [
            '@username' => $user->getAccountName(),
            '@message' => $e->getMessage(),
          ]),
          'severity' => REQUIREMENT_ERROR,
        ];
      }
      $hashed_password = $user->getPassword();

      $password_hasher = \Drupal::service('password');
      if (!$password_hasher->check($expected_password, $hashed_password)) {
      if (NULL !== $expected_password && !$password_hasher->check($expected_password, $hashed_password)) {
        $requirements['cms_content_sync_user_password'] = [
          'title' => t('Content Sync: User password'),
          'value' => t('The service user "@username" password changed unexpectedly.', [
@@ -404,6 +530,70 @@ function cms_content_sync_update_8035() {
  return t("The $entity_id.$field_name field length has been increased from 255 to $new_length characters.");
}

/**
 * Implements hook_update_N().
 *
 * Move the encryption key to a site-specific value and re-encrypt the service
 * user credentials with it.
 */
function cms_content_sync_update_8037() {
  $key_config = \Drupal::configFactory()->getEditable('key.key.cms_content_sync');
  if ($key_config->isNew()) {
    return t('The Content Sync encryption key does not exist, so there is nothing to replace. Re-install the module to create it.');
  }

  $provider = $key_config->get('key_provider');
  if (!in_array($provider, ['config', StateKeyProvider::PLUGIN_ID], TRUE)) {
    return t('The Content Sync encryption key uses the "@provider" key provider and was left unchanged.', ['@provider' => $provider]);
  }

  // Read the stored credentials with the key that is active right now. If that
  // fails, e.g. because the new key configuration was imported before this
  // update ran, fall back to the default key value of earlier releases.
  $uid = _cms_content_sync_user_id();
  $user_data = \Drupal::service('user.data');
  $encrypted = $uid ? $user_data->get('cms_content_sync', $uid, 'sync_data') : NULL;
  $decrypted = NULL;
  $error = NULL;
  if (!empty($encrypted)) {
    try {
      $decrypted = cms_content_sync_decrypt_values($encrypted);
    }
    catch (\Throwable $e) {
      $error = $e->getMessage();
      try {
        $decrypted = _cms_content_sync_decrypt_values_with_key($encrypted, base64_decode(CMS_CONTENT_SYNC_LEGACY_KEY_VALUE));
      }
      catch (\Throwable $legacy_error) {
        $decrypted = NULL;
      }
    }
  }

  // Point the key at the site state and generate a fresh value there.
  $key_config
    ->set('key_provider', StateKeyProvider::PLUGIN_ID)
    ->set('key_provider_settings', [])
    ->set('key_input', 'none')
    ->set('key_input_settings', [])
    ->save();
  _cms_content_sync_generate_encryption_key();

  if (NULL !== $decrypted) {
    $user_data->set('cms_content_sync', $uid, 'sync_data', cms_content_sync_encrypt_values($decrypted));

    return t('Generated a site-specific encryption key and re-encrypted the credentials of the Content Sync service user with it.');
  }

  if (!empty($encrypted)) {
    \Drupal::logger('cms_content_sync')->error('The stored credentials of the Content Sync service user could not be decrypted with the previous encryption key: @error', ['@error' => $error]);

    return t('Generated a site-specific encryption key, but the stored credentials of the Content Sync service user could not be decrypted with the previous key: @error Set a new password for the service user, then re-register the site.', ['@error' => $error]);
  }

  return t('Generated a site-specific encryption key. No service user credentials were stored yet.');
}

/**
 * Implements hook_update_N();.
 *
+25 −0
Changes for cms_content_sync.module: 25 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -1923,6 +1923,31 @@ function cms_content_sync_encrypt_values(array $values) {
  return $values;
}

/**
 * Decrypt the provided values.
 *
 * Counterpart of cms_content_sync_encrypt_values().
 *
 * @param array $values
 *   The values to decrypt.
 *
 * @return array
 *   The input array, but with decrypted values
 *
 * @throws \Drupal\encrypt\Exception\EncryptException
 *   If a value can't be decrypted with the current key.
 */
function cms_content_sync_decrypt_values(array $values) {
  $encryption_profile = EncryptionProfile::load(CMS_CONTENT_SYNC_ENCRYPTION_PROFILE_NAME);

  foreach ($values as $key => $value) {
    $values[$key] = Drupal::service('encryption')
      ->decrypt($value, $encryption_profile);
  }

  return $values;
}

/**
 * Check whether any special behavior for pulled entities should be applied.
 * This is true if the entity was pulled AND all parent entities have been
+5 −8
Changes for config/install/key.key.cms_content_sync.yml: 5 added lines, 8 removed lines.
Original line number Diff line number Diff line
@@ -8,14 +8,11 @@ dependencies:
      - cms_content_sync
id: cms_content_sync
label: 'Content Sync'
description: 'Encryption key for synchronization'
description: 'Encryption key for synchronization. The value is generated per site and stored in the site state, never in configuration.'
key_type: encryption
key_type_settings:
  key_size: 256
key_provider: config
key_provider_settings:
  base64_encoded: true
  key_value: Ly8gQHRvZG86IE5lZWQgdG8gYmUgY2hhbmdlZCoqKio=
key_input: text_field
key_input_settings:
  base64_encoded: true
key_provider: cms_content_sync_state
key_provider_settings: {  }
key_input: none
key_input_settings: {  }
Loading