diff --git a/composer.json b/composer.json index 57d2f1f..9b785c5 100644 --- a/composer.json +++ b/composer.json @@ -1,34 +1,36 @@ { - "name": "drupal/salesforce", - "description": "Provides Drupal modules to integrate with Salesforce.", - "type": "drupal-module", - "homepage": "https://drupal.org/project/salesforce", - "authors": [ - { - "name": "Aaron Bauman (aaronbauman)", - "homepage": "https://www.drupal.org/u/aaronbauman", - "role": "Maintainer" - }, - { - "name": "Alexander Rhodes (ironsizide)", - "homepage": "https://www.drupal.org/u/ironsizide", - "role": "Maintainer" - } - ], - "support": { - "issues": "https://drupal.org/project/issues/salesforce", - "source": "http://cgit.drupalcode.org/salesforce" + "name": "drupal/salesforce", + "description": "Provides Drupal modules to integrate with Salesforce.", + "type": "drupal-module", + "homepage": "https://drupal.org/project/salesforce", + "authors": [ + { + "name": "Aaron Bauman (aaronbauman)", + "homepage": "https://www.drupal.org/u/aaronbauman", + "role": "Maintainer" }, - "extra": { - "drush": { - "services": { - "drush.services.yml": "^9" - } - } - }, - "require": { - "consolidation/output-formatters": "^3.2.0", - "drupal/dynamic_entity_reference": "^2.0-alpha8", - "drupal/encrypt": "^3.0-rc1" + { + "name": "Alexander Rhodes (ironsizide)", + "homepage": "https://www.drupal.org/u/ironsizide", + "role": "Maintainer" + } + ], + "support": { + "issues": "https://drupal.org/project/issues/salesforce", + "source": "http://cgit.drupalcode.org/salesforce" + }, + "extra": { + "drush": { + "services": { + "drush.services.yml": "^9" + } } + }, + "require": { + "consolidation/output-formatters": "^3.2.0", + "drupal/dynamic_entity_reference": "^2.0-alpha8", + "drupal/encrypt": "^3.0-rc1", + "drupal/key": "^1.7", + "lusitanian/oauth": "^0.8.11" + } } diff --git a/config/install/salesforce.settings.yml b/config/install/salesforce.settings.yml index 8732401..d9cddb2 100644 --- a/config/install/salesforce.settings.yml +++ b/config/install/salesforce.settings.yml @@ -7,4 +7,7 @@ global_push_limit: 100000 pull_max_queue_size: 100000 show_all_objects: false standalone: false -limit_mapped_object_revisions: 10 \ No newline at end of file +limit_mapped_object_revisions: 10 +salesforce_auth: + provider: '' + config_id: '' \ No newline at end of file diff --git a/config/schema/salesforce.schema.yml b/config/schema/salesforce.schema.yml index e00a026..b986c28 100644 --- a/config/schema/salesforce.schema.yml +++ b/config/schema/salesforce.schema.yml @@ -52,3 +52,35 @@ salesforce.settings: version: type: string label: 'Version' + +salesforce.salesforce_auth.*: + type: config_entity + label: 'Salesforce Auth Provider' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + translatable: true + provider: + type: string + label: 'Provider Plugin' + provider_settings: + type: salesforce.auth_provider_settings.[%parent.provider] + label: 'Provider Plugin Settings' + +salesforce.auth_provider_settings.oauth: + type: mapping + label: 'Salesforce JWT Provider Settings' + mapping: + consumer_key: + type: string + label: 'Consumer Key' + consumer_secret: + type: string + label: 'Consumer Secret' + login_url: + type: uri + label: 'Login URL' diff --git a/modules/salesforce_encrypt/config/schema/salesforce_encrypt.schema.yml b/modules/salesforce_encrypt/config/schema/salesforce_encrypt.schema.yml new file mode 100644 index 0000000..6e530bd --- /dev/null +++ b/modules/salesforce_encrypt/config/schema/salesforce_encrypt.schema.yml @@ -0,0 +1,16 @@ +salesforce.auth_provider_settings.oauth_encrypt: + type: mapping + label: 'Salesforce JWT Provider Settings' + mapping: + consumer_key: + type: string + label: 'Consumer Key' + consumer_secret: + type: string + label: 'Consumer Secret' + login_url: + type: uri + label: 'Login URL' + encryption_profile: + type: encrypt.profile.[%key] + label: 'Encryption Profile ID' diff --git a/modules/salesforce_encrypt/salesforce_encrypt.info.yml b/modules/salesforce_encrypt/salesforce_encrypt.info.yml index 74f83be..8129abd 100644 --- a/modules/salesforce_encrypt/salesforce_encrypt.info.yml +++ b/modules/salesforce_encrypt/salesforce_encrypt.info.yml @@ -1,6 +1,6 @@ name: Salesforce Encrypted Keys type: module -description: Supplants Salesforce RestClient service to provide encrypted stateful data. +description: Encryption provider for Salesforce credentials. package: Salesforce core: 8.x dependencies: diff --git a/modules/salesforce_encrypt/salesforce_encrypt.install b/modules/salesforce_encrypt/salesforce_encrypt.install deleted file mode 100644 index 74ece30..0000000 --- a/modules/salesforce_encrypt/salesforce_encrypt.install +++ /dev/null @@ -1,50 +0,0 @@ -getEncryptionProfile(); - } - catch (EntityNotFoundException $e) { - // Noop. - } - $requirements['salesforce_encrypt'] = [ - 'title' => t('Salesforce Encrypt'), - 'value' => t('Encryption Profile'), - ]; - if (empty($profile)) { - $requirements['salesforce_encrypt'] += [ - 'severity' => REQUIREMENT_ERROR, - 'description' => t('You need to select an encryption profile in order to fully enable Salesforce Encrypt and protect sensitive information.', ['@url' => Url::fromRoute('salesforce_encrypt.settings')->toString()]), - ]; - } - else { - $requirements['salesforce_encrypt'] += [ - 'severity' => REQUIREMENT_OK, - 'description' => t('Profile id: %profile', ['%profile' => $profile->id(), ':url' => $profile->url()]), - ]; - } - } - return $requirements; -} - -/** - * Implements hook_uninstall to decrypt and purge our data. - */ -function salesforce_encrypt_uninstall() { - \Drupal::service('salesforce.client')->disableEncryption(); - \Drupal::state()->delete('salesforce_encrypt.profile'); -} diff --git a/modules/salesforce_encrypt/salesforce_encrypt.links.menu.yml b/modules/salesforce_encrypt/salesforce_encrypt.links.menu.yml deleted file mode 100644 index 7825f42..0000000 --- a/modules/salesforce_encrypt/salesforce_encrypt.links.menu.yml +++ /dev/null @@ -1,6 +0,0 @@ -salesforce_encrypt.settings: - route_name: salesforce_encrypt.settings - parent: salesforce.admin_config_salesforce - title: Salesforce Encrypt - description: 'Encrypt sensitive Salesforce OAuth credentials and identity.' - weight: 10 diff --git a/modules/salesforce_encrypt/salesforce_encrypt.module b/modules/salesforce_encrypt/salesforce_encrypt.module index ec0966d..a63e406 100644 --- a/modules/salesforce_encrypt/salesforce_encrypt.module +++ b/modules/salesforce_encrypt/salesforce_encrypt.module @@ -5,6 +5,9 @@ */ use Drupal\encrypt\EncryptionProfileInterface; +use Drupal\salesforce_encrypt\Plugin\SalesforceAuthProvider\SalesforceEncryptedOAuthPlugin; +use Drupal\Core\Entity\EntityInterface; +use Drupal\salesforce\Entity\SalesforceAuthConfig; /** * Implements hook_encryption_profile_predelete() @@ -13,3 +16,20 @@ use Drupal\encrypt\EncryptionProfileInterface; function salesforce_encryption_profile_predelete(EncryptionProfileInterface $entity) { \Drupal::service('salesforce.client')->hookEncryptionProfileDelete($entity); } + +/** + * Implements hook_entity_presave(). + */ +function salesforce_encrypt_entity_presave(EntityInterface $entity) { + if (!$entity instanceof SalesforceAuthConfig) { + return; + } + // Encrypt credentials prior to saving the config entity. + if ($entity->getPlugin() instanceof SalesforceEncryptedOAuthPlugin) { + $plugin = $entity->getPlugin(); + $settings = $entity->get('provider_settings'); + $settings['consumer_key'] = $plugin->encrypt($settings['consumer_key']); + $settings['consumer_secret'] = $plugin->encrypt($settings['consumer_secret']); + $entity->set('provider_settings', $settings); + } +} diff --git a/modules/salesforce_encrypt/salesforce_encrypt.routing.yml b/modules/salesforce_encrypt/salesforce_encrypt.routing.yml deleted file mode 100644 index ad30e49..0000000 --- a/modules/salesforce_encrypt/salesforce_encrypt.routing.yml +++ /dev/null @@ -1,8 +0,0 @@ -salesforce_encrypt.settings: - path: '/admin/config/salesforce/encrypt' - defaults: - _form: '\Drupal\salesforce_encrypt\Form\SettingsForm' - _title: 'Salesforce Encryption' - _description: 'Encrypt sensitive Salesforce OAuth credentials and identity.' - requirements: - _permission: 'administer salesforce encryption' diff --git a/modules/salesforce_encrypt/src/Consumer/OAuthEncryptedCredentials.php b/modules/salesforce_encrypt/src/Consumer/OAuthEncryptedCredentials.php new file mode 100644 index 0000000..5bb50d2 --- /dev/null +++ b/modules/salesforce_encrypt/src/Consumer/OAuthEncryptedCredentials.php @@ -0,0 +1,34 @@ +consumerSecret = $consumerSecret; + $this->encryptionProfileId = $encryptionProfileId; + } + + /** + * @return string + */ + public function getEncryptionProfileId() { + return $this->encryptionProfileId; + } + + /** + * {@inheritdoc} + */ + public function getCallbackUrl() { + return SalesforceEncryptedOAuthPlugin::getAuthCallbackUrl(); + } + +} \ No newline at end of file diff --git a/modules/salesforce_encrypt/src/Form/SettingsForm.php b/modules/salesforce_encrypt/src/Form/SettingsForm.php index b9fbd4c..34d5e9c 100644 --- a/modules/salesforce_encrypt/src/Form/SettingsForm.php +++ b/modules/salesforce_encrypt/src/Form/SettingsForm.php @@ -7,7 +7,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\State\StateInterface; use Drupal\encrypt\EncryptionProfileManagerInterface; use Drupal\salesforce\EntityNotFoundException; -use Drupal\salesforce_encrypt\Rest\EncryptedRestClientInterface; +use Drupal\salesforce_encrypt\SalesforceEncryptedAuthTokenStorageInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Url; @@ -19,15 +19,20 @@ class SettingsForm extends FormBase { protected $encryptionProfileManager; /** + * @var \Drupal\salesforce_encrypt\SalesforceEncryptedAuthTokenStorageInterface + */ + protected $storage; + + /** * Constructs a new key form base. * * @param \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $storage * The key storage. */ - public function __construct(StateInterface $state, EncryptionProfileManagerInterface $encryptionProfileManager, EncryptedRestClientInterface $client) { + public function __construct(StateInterface $state, EncryptionProfileManagerInterface $encryptionProfileManager, SalesforceEncryptedAuthTokenStorageInterface $storage) { $this->encryptionProfileManager = $encryptionProfileManager; $this->state = $state; - $this->client = $client; + $this->storage = $storage; } /** @@ -37,7 +42,7 @@ class SettingsForm extends FormBase { return new static( $container->get('state'), $container->get('encrypt.encryption_profile.manager'), - $container->get('salesforce.client') + $container->get('salesforce.auth_token_storage') ); } @@ -57,14 +62,14 @@ class SettingsForm extends FormBase { ->getEncryptionProfileNamesAsOptions(); $default = NULL; try { - $profile = $this->client->getEncryptionProfile(); + $profile = $this->storage->getEncryptionProfile(); if (!empty($profile)) { $default = $profile->id(); } } catch (EntityNotFoundException $e) { - drupal_set_message($e->getFormattableMessage(), 'error'); - drupal_set_message($this->t('Error while loading encryption profile. You will need to assign a new encryption profile, then re-authenticate to Salesforce.', [':encrypt' => Url::fromRoute('salesforce_encrypt.settings')->toString(), ':oauth' => Url::fromRoute('salesforce.authorize')->toString()]), 'error'); + $this->messenger()->addError($e->getFormattableMessage()); + $this->messenger()->addError($this->t('Error while loading encryption profile. You will need to assign a new encryption profile, then re-authenticate to Salesforce.', [':encrypt' => Url::fromRoute('salesforce_encrypt.settings')->toString(), ':oauth' => Url::fromRoute('salesforce.admin_config_salesforce')->toString()])); } $form['profile'] = [ @@ -73,7 +78,7 @@ class SettingsForm extends FormBase { '#description' => $this->t('Choose an encryption profile with which to encrypt Salesforce information.'), '#options' => $options, '#default_value' => $default, - '#empty_option' => $this->t('Do not use encryption'), + '#empty_option' => TRUE, ]; $form['actions']['#type'] = 'actions'; @@ -113,19 +118,19 @@ class SettingsForm extends FormBase { ->getEncryptionProfile($profile_id); if (empty($profile_id)) { // New profile id empty: disable encryption. - $this->client->disableEncryption(); + $this->storage->disableEncryption(); } elseif (empty($old_profile_id)) { // Old profile id empty: enable encryption anew. - $this->client->enableEncryption($profile); + $this->storage->enableEncryption($profile); } else { // Changing encryption profiles: disable, then re-enable. - $this->client->disableEncryption(); - $this->client->enableEncryption($profile); + $this->storage->disableEncryption(); + $this->storage->enableEncryption($profile); } $this->state->resetCache(); - drupal_set_message($this->t('The configuration options have been saved.')); + $this->messenger()->addStatus($this->t('The configuration options have been saved.')); } } diff --git a/modules/salesforce_encrypt/src/Plugin/SalesforceAuthProvider/SalesforceEncryptedOAuthPlugin.php b/modules/salesforce_encrypt/src/Plugin/SalesforceAuthProvider/SalesforceEncryptedOAuthPlugin.php new file mode 100644 index 0000000..f81eeed --- /dev/null +++ b/modules/salesforce_encrypt/src/Plugin/SalesforceAuthProvider/SalesforceEncryptedOAuthPlugin.php @@ -0,0 +1,281 @@ +getLoginUrl())); + $this->id = $id; + $this->encryptionProfileManager = $encryptionProfileManager; + $this->encryption = $encrypt; + $this->encryptionProfileId = $credentials->getEncryptionProfileId(); + } + + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + $configuration = array_merge(self::defaultConfiguration(), $configuration); + $storage = $container->get('salesforce.auth_token_storage'); + /** @var EncryptServiceInterface $encrypt */ + $encrypt = $container->get('encryption'); + $encryptProfileMan = $container->get('encrypt.encryption_profile.manager'); + if ($configuration['encryption_profile']) { + try { + $profile = $encryptProfileMan->getEncryptionProfile($configuration['encryption_profile']); + $configuration['consumer_key'] = $encrypt->decrypt($configuration['consumer_key'], $profile); + $configuration['consumer_secret'] = $encrypt->decrypt($configuration['consumer_secret'], $profile); + } + catch (\Exception $e) { + // Any exception here may cause WSOD, don't allow that to happen. + watchdog_exception('SFOAuthEncrypted', $e); + } + } + $cred = new OAuthEncryptedCredentials($configuration['consumer_key'], $configuration['login_url'], $configuration['consumer_secret'], $configuration['encryption_profile']); + return new static($configuration['id'], $cred, $container->get('salesforce.http_client_wrapper'), $storage, $encryptProfileMan, $encrypt); + } + + public static function defaultConfiguration() { + $defaults = parent::defaultConfiguration(); + return array_merge($defaults, [ + 'encryption_profile' => NULL, + ]); + } + + /** + * {@inheritdoc} + */ + public function hookEncryptionProfileDelete(EncryptionProfileInterface $profile) { + if ($this->encryptionProfile()->id() == $profile->id()) { + // @todo decrypt identity, access token, refresh token, consumer secret, consumer key and re-save + } + } + + public function encryptionProfile() { + if ($this->encryptionProfile) { + return $this->encryptionProfile; + } + elseif (empty($this->encryptionProfileId)) { + return NULL; + } + else { + $this->encryptionProfile = $this->encryptionProfileManager + ->getEncryptionProfile($this->encryptionProfileId); + if (empty($this->encryptionProfile)) { + throw new EntityNotFoundException(['id' => $this->encryptionProfileId], 'encryption_profile'); + } + return $this->encryptionProfile; + } + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $options = $this + ->encryptionProfileManager + ->getEncryptionProfileNamesAsOptions(); + $default = NULL; + try { + $profile = $this->encryptionProfile(); + if (!empty($profile)) { + $default = $profile->id(); + } + } + catch (EntityNotFoundException $e) { + $this->messenger()->addError($e->getFormattableMessage()); + $this->messenger()->addError($this->t('Error while loading encryption profile. You will need to assign a new encryption profile and re-authenticate to Salesforce.')); + } + + if (empty($options)) { + $this->messenger()->addError($this->t('Please create an encryption profile before adding an OAuth Encrypted provider.', ['@href' => Url::fromRoute('entity.encryption_profile.add_form')->toString()])); + } + + $form['consumer_key'] = [ + '#title' => t('Salesforce consumer key'), + '#type' => 'textfield', + '#description' => t('Consumer key of the Salesforce remote application you want to grant access to. VALUE WILL BE ENCRYPTED ON FORM SUBMISSION.'), + '#required' => TRUE, + '#default_value' => $this->credentials->getConsumerKey() + ]; + + $form['consumer_secret'] = [ + '#title' => $this->t('Salesforce consumer secret'), + '#type' => 'textfield', + '#description' => $this->t('Consumer secret of the Salesforce remote application. VALUE WILL BE ENCRYPTED ON FORM SUBMISSION.'), + '#required' => TRUE, + '#default_value' => $this->credentials->getConsumerSecret() + ]; + + $form['login_url'] = [ + '#title' => t('Login URL'), + '#type' => 'textfield', + '#default_value' => $this->credentials->getLoginUrl(), + '#description' => t('Enter a login URL, either https://login.salesforce.com or https://test.salesforce.com.'), + '#required' => TRUE, + ]; + $form['encryption_profile'] = [ + '#type' => 'select', + '#title' => $this->t('Encryption Profile'), + '#description' => $this->t('Choose an encryption profile with which to encrypt Salesforce information.'), + '#options' => $options, + '#default_value' => $default, + '#required' => TRUE, + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->setConfiguration($form_state->getValues()); + $settings = $form_state->getValue('provider_settings'); + $this->encryptionProfileId = $settings['encryption_profile']; + $consumer_key = $settings['consumer_key']; + $settings['consumer_key'] = $this->encrypt($settings['consumer_key']); + $settings['consumer_secret'] = $this->encrypt($settings['consumer_secret']); + $form_state->setValue('provider_settings', $settings); + parent::submitConfigurationForm($form, $form_state); + + // Write the config id to private temp store, so that we can use the same + // callback URL for all OAuth applications in Salesforce. + /** @var \Drupal\Core\TempStore\PrivateTempStore $tempstore */ + $tempstore = \Drupal::service('user.private_tempstore')->get('salesforce_oauth'); + $tempstore->set('config_id', $form_state->getValue('id')); + + try { + $path = $this->getAuthorizationEndpoint(); + $query = [ + 'redirect_uri' => self::getAuthCallbackUrl(), + 'response_type' => 'code', + 'client_id' => $consumer_key, + ]; + + // Send the user along to the Salesforce OAuth login form. If successful, + // the user will be redirected to {redirect_uri} to complete the OAuth + // handshake, and thence to the entity listing. Upon failure, the user + // redirect URI will send the user back to the edit form. + $response = new TrustedRedirectResponse($path . '?' . http_build_query($query), 302); + $response->send(); + return; + } + catch (\Exception $e) { + $this->messenger()->addError(t("Error during authorization: %message", ['%message' => $e->getMessage()])); + // $this->eventDispatcher->dispatch(SalesforceEvents::ERROR, new SalesforceErrorEvent($e)); + } + } + + /** + * @param string $value + * + * @return string + * @throws \Drupal\encrypt\Exception\EncryptException + */ + public function decrypt($value) { + return $this->encryption->decrypt($value, $this->encryptionProfile()); + } + + /** + * @param string $value + * + * @return string + * @throws \Drupal\encrypt\Exception\EncryptException + */ + public function encrypt($value) { + return $this->encryption->encrypt($value, $this->encryptionProfile()); + } + + public static function getAuthCallbackUrl() { + return Url::fromRoute('salesforce.oauth_callback', [], [ + 'absolute' => TRUE, + 'https' => TRUE, + ])->toString(); + } + + public function getConsumerSecret() { + return $this->credentials->getConsumerSecret(); + } + + /** + * @return bool + * @throws \OAuth\Common\Http\Exception\TokenResponseException + * @see \Drupal\salesforce\Controller\SalesforceOAuthController + */ + public function finalizeOauth() { + $this->requestAccessToken(\Drupal::request()->get('code')); + $token = $this->getAccessToken(); + + // Initialize identity. + $headers = [ + 'Authorization' => 'OAuth ' . $token->getAccessToken(), + 'Content-type' => 'application/json', + ]; + dpm($headers); + $data = $token->getExtraParams(); + $response = $this->httpClient->retrieveResponse(new Uri($data['id']), [], $headers); + $identity = $this->parseIdentityResponse($response); + $this->storage->storeIdentity($this->service(), $identity); + return TRUE; + } + +} \ No newline at end of file diff --git a/modules/salesforce_encrypt/src/Rest/EncryptedRestClientInterface.php b/modules/salesforce_encrypt/src/Rest/EncryptedRestClientInterface.php index 58f771f..b5e96ea 100644 --- a/modules/salesforce_encrypt/src/Rest/EncryptedRestClientInterface.php +++ b/modules/salesforce_encrypt/src/Rest/EncryptedRestClientInterface.php @@ -2,80 +2,13 @@ namespace Drupal\salesforce_encrypt\Rest; -use Drupal\encrypt\EncryptionProfileInterface; use Drupal\salesforce\Rest\RestClientInterface; /** * Objects, properties, and methods to communicate with the Salesforce REST API. + * + * @deprecated use SalesforceEncryptedAuthTokenStorage */ interface EncryptedRestClientInterface extends RestClientInterface { - /** - * Encrypts all sensitive salesforce config values. - * - * @param string $profile_id - * Id of the Encrypt Profile to use for encryption. - * - * @return bool - * TRUE if encryption was enabled or FALSE if it is already enabled - * - * @throws RuntimeException - * if Salesforce encryption profile hasn't been selected - */ - public function enableEncryption(EncryptionProfileInterface $profile); - - /** - * Inverse of ::enableEncryption. Decrypts all sensitive salesforce config - * values. - * - * @return bool - * TRUE if encryption was disabled or FALSE if it is already disabled - * - * @throws RuntimeException - * if Salesforce encryption profile hasn't been selected - */ - public function disableEncryption(); - - /** - * Returns the EncryptionProfileInterface assigned to Salesforce Encrypt, or - * NULL if no profile is assigned. - * - * @throws \Drupal\salesforce\EntityNotFoundException - * if a profile is assigned, but cannot be loaded. - * - * @return \Drupal\encrypt\EncryptionProfileInterface | NULL - */ - public function getEncryptionProfile(); - - /** - * Since we rely on a specific encryption profile, we need to respond in case - * it gets deleted. Check to see if the profile being deleted is the one - * assigned for encryption; if so, decrypt our config and disable encryption. - * - * @param \Drupal\encrypt\EncryptionProfileInterface $profile - */ - public function hookEncryptionProfileDelete(EncryptionProfileInterface $profile); - - /** - * Encrypts a value using the encryption profile given by salesforce_encrypt.profile. - * - * @param string $value - * The value to encrypt. - * - * @return string - * The encrypted value. - */ - public function encrypt($value); - - /** - * Decrypts a value using the encryption profile given by salesforce_encrypt.profile. - * - * @param string $value - * The value to decrypt. - * - * @return string - * The decrypted value. - */ - public function decrypt($value); - } diff --git a/modules/salesforce_encrypt/src/Rest/RestClient.php b/modules/salesforce_encrypt/src/Rest/RestClient.php index a2be2b6..e74c53e 100644 --- a/modules/salesforce_encrypt/src/Rest/RestClient.php +++ b/modules/salesforce_encrypt/src/Rest/RestClient.php @@ -15,258 +15,15 @@ use Drupal\encrypt\EncryptionProfileInterface; use Drupal\encrypt\EncryptionProfileManagerInterface; use Drupal\salesforce\EntityNotFoundException; use Drupal\salesforce\Rest\RestClient as SalesforceRestClient; +use Drupal\salesforce\SalesforceAuthProviderPluginManager; use GuzzleHttp\ClientInterface; use Drupal\Component\Datetime\TimeInterface; /** * Objects, properties, and methods to communicate with the Salesforce REST API. + * + * @deprecated use \Drupal\salesforce\SalesforceAuthProviderPluginManager::getConfig() to access the current active auth configuration. */ class RestClient extends SalesforceRestClient implements EncryptedRestClientInterface { - use StringTranslationTrait; - - protected $encryption; - protected $encryptionProfileManager; - protected $encryptionProfileId; - - /** - * The encryption profile to use when encrypting and decrypting data. - * - * @var \Drupal\encrypt\EncryptionProfileInterface - */ - protected $encryptionProfile; - - /** - * Construct a new Encrypted Rest Client. - * - * @param \GuzzleHttp\ClientInterface $http_client - * The GuzzleHttp Client. - * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory - * The config factory service. - * @param \Drupal\Core\State\StateInterface $state - * The state service. - * @param \Drupal\Core\Cache\CacheBackendInterface $cache - * The cache service. - * @param \Drupal\Component\Serialization\Json $json - * The JSON serializer service. - * @param \Drupal\encrypt\EncryptServiceInterface $encryption - * The encryption service. - * @param \Drupal\encrypt\EncryptionProfileManagerInterface $encryptionProfileManager - * The Encryption profile manager service. - * @param \Drupal\Core\Lock\LockBackendInterface $lock - * The lock backend service. - */ - public function __construct(ClientInterface $http_client, ConfigFactoryInterface $config_factory, StateInterface $state, CacheBackendInterface $cache, Json $json, TimeInterface $time, EncryptServiceInterface $encryption, EncryptionProfileManagerInterface $encryptionProfileManager, LockBackendInterface $lock) { - parent::__construct($http_client, $config_factory, $state, $cache, $json, $time); - $this->encryption = $encryption; - $this->encryptionProfileId = $this->state->get('salesforce_encrypt.profile'); - $this->encryptionProfileManager = $encryptionProfileManager; - $this->encryptionProfile = NULL; - $this->lock = $lock; - } - - /** - * Encrypts all sensitive salesforce config values. - * - * @throws RuntimeException if Salesforce if encryption was not enabled. - */ - public function enableEncryption(EncryptionProfileInterface $profile) { - if ($ret = $this->setEncryption($profile)) { - $this->state->resetCache(); - } - return $ret; - } - - /** - * Inverse of ::enableEncryption. Decrypts all sensitive salesforce config - * values. - * - * @throws RuntimeException if Salesforce encryption can't be disabled - */ - public function disableEncryption() { - if ($ret = $this->setEncryption()) { - $this->state->resetCache(); - } - return $ret; - } - - /** - * - */ - public function hookEncryptionProfileDelete(EncryptionProfileInterface $profile) { - if ($this->encryptionProfileId == $profile->id()) { - $this->disableEncryption(); - } - } - - /** - * - */ - protected function setEncryption(EncryptionProfileInterface $profile = NULL) { - if (!$this->lock->acquire('salesforce_encrypt')) { - throw new \RuntimeException('Unable to acquire lock.'); - } - - $access_token = $this->getAccessToken(); - $refresh_token = $this->getRefreshToken(); - $identity = $this->getIdentity(); - $consumerKey = $this->getConsumerKey(); - $consumerSecret = $this->getConsumerSecret(); - - $this->encryptionProfileId = $profile == NULL ? NULL : $profile->id(); - $this->encryptionProfile = $profile; - $this->state->set('salesforce_encrypt.profile', $this->encryptionProfileId); - - $this->setAccessToken($access_token); - $this->setRefreshToken($refresh_token); - $this->setIdentity($identity); - $this->setConsumerKey($consumerKey); - $this->setConsumerSecret($consumerSecret); - - $this->lock->release('salesforce_encrypt'); - } - - /** - * {@inheritdoc} - */ - public function getEncryptionProfile() { - if ($this->encryptionProfile) { - return $this->encryptionProfile; - } - elseif (empty($this->encryptionProfileId)) { - return NULL; - } - else { - $this->encryptionProfile = $this->encryptionProfileManager - ->getEncryptionProfile($this->encryptionProfileId); - if (empty($this->encryptionProfile)) { - throw new EntityNotFoundException(['id' => $this->encryptionProfileId], 'encryption_profile'); - } - return $this->encryptionProfile; - } - } - - /** - * Exception-handling wrapper around getEncryptionProfile(). - * - * getEncryptionProfile() will throw an EntityNotFoundException exception - * if it has an encryption profile ID but cannot load it. In this wrapper - * we handle that exception by setting a helpful error message and allow - * execution to proceed. - * - * @return \Drupal\encrypt\EncryptionProfileInterface | NULL - * The encryption profile if it can be loaded, otherwise NULL. - */ - protected function _getEncryptionProfile() { - try { - $profile = $this->getEncryptionProfile(); - } - catch (EntityNotFoundException $e) { - drupal_set_message($this->t('Error while loading encryption profile. You will need to assign a new encryption profile, then re-authenticate to Salesforce.', [':encrypt' => Url::fromRoute('salesforce_encrypt.settings')->toString(), ':oauth' => Url::fromRoute('salesforce.authorize')->toString()]), 'error'); - } - - return $profile; - } - - /** - * {@inheritdoc} - */ - public function encrypt($value) { - if (empty($this->_getEncryptionProfile())) { - return $value; - } - else { - return $this->encryption->encrypt($value, $this->_getEncryptionProfile()); - } - } - - /** - * {@inheritdoc} - */ - public function decrypt($value) { - if (empty($this->_getEncryptionProfile()) || empty($value) || mb_strlen($value) === 0) { - return $value; - } - else { - return $this->encryption->decrypt($value, $this->_getEncryptionProfile()); - } - } - - /** - * {@inheritdoc} - */ - public function getAccessToken() { - return $this->decrypt(parent::getAccessToken()); - } - - /** - * {@inheritdoc} - */ - public function setAccessToken($token) { - return parent::setAccessToken($this->encrypt($token)); - } - - /** - * {@inheritdoc} - */ - public function getRefreshToken() { - return $this->decrypt(parent::getRefreshToken()); - } - - /** - * {@inheritdoc} - */ - public function setRefreshToken($token) { - return parent::setRefreshToken($this->encrypt($token)); - } - - /** - * {@inheritdoc} - */ - public function setIdentity($data) { - if (is_array($data)) { - $data = serialize($data); - } - return parent::setIdentity($this->encrypt($data)); - } - - /** - * {@inheritdoc} - */ - public function getIdentity() { - $data = $this->decrypt(parent::getIdentity()); - if (!empty($data) && !is_array($data)) { - $data = unserialize($data); - } - return $data; - } - - /** - * {@inheritdoc} - */ - public function getConsumerKey() { - return $this->decrypt(parent::getConsumerKey()); - } - - /** - * {@inheritdoc} - */ - public function setConsumerKey($value) { - return parent::setConsumerKey($this->encrypt($value)); - } - - /** - * {@inheritdoc} - */ - public function getConsumerSecret() { - return $this->decrypt(parent::getConsumerSecret()); - } - - /** - * {@inheritdoc} - */ - public function setConsumerSecret($value) { - return parent::setConsumerSecret($this->encrypt($value)); - } - } diff --git a/modules/salesforce_encrypt/src/SalesforceEncryptServiceProvider.php b/modules/salesforce_encrypt/src/SalesforceEncryptServiceProvider.php index 6146bfb..a602528 100644 --- a/modules/salesforce_encrypt/src/SalesforceEncryptServiceProvider.php +++ b/modules/salesforce_encrypt/src/SalesforceEncryptServiceProvider.php @@ -16,12 +16,8 @@ class SalesforceEncryptServiceProvider extends ServiceProviderBase { * {@inheritdoc} */ public function alter(ContainerBuilder $container) { - // Overrides salesforce.client class with our EncryptedRestClientInterface. - $container->getDefinition('salesforce.client') - ->setClass(RestClient::class) - ->addArgument(new Reference('encryption')) - ->addArgument(new Reference('encrypt.encryption_profile.manager')) - ->addArgument(new Reference('lock')); + $container->getDefinition('salesforce.auth_token_storage') + ->setClass(SalesforceEncryptedAuthTokenStorage::CLASS); } } diff --git a/modules/salesforce_encrypt/src/SalesforceEncryptedAuthTokenStorage.php b/modules/salesforce_encrypt/src/SalesforceEncryptedAuthTokenStorage.php new file mode 100644 index 0000000..59fd935 --- /dev/null +++ b/modules/salesforce_encrypt/src/SalesforceEncryptedAuthTokenStorage.php @@ -0,0 +1,91 @@ +authPluginManager) { + $this->authPluginManager = \Drupal::service('plugin.manager.salesforce.auth_providers'); + } + $auth = SalesforceAuthConfig::load($service_id); + return $auth->getPlugin(); + } + + /** + * {@inheritdoc} + */ + public function retrieveAccessToken($service_id) { + $token = parent::retrieveAccessToken($service_id); + if ($token instanceof TokenInterface || !$this->service($service_id) instanceof SalesforceEncryptedOAuthPlugin) { + return $token; + } + $token = unserialize($this->service($service_id)->decrypt($token)); + return $token; + } + + /** + * {@inheritdoc} + */ + public function storeAccessToken($service_id, TokenInterface $token) { + if ($this->service($service_id) instanceof SalesforceEncryptedOAuthPlugin) { + $token = $this->service($service_id)->encrypt(serialize($token)); + } + $this->state->set(self::getTokenStorageId($service_id), $token); + return $this; + } + + /** + * {@inheritdoc} + */ + public function storeIdentity($service_id, $identity) { + if ($this->service($service_id) instanceof SalesforceEncryptedOAuthPlugin) { + if (is_array($identity)) { + $identity = serialize($identity); + } + $identity = $this->service($service_id)->encrypt($identity); + } + $this->state->set(self::getIdentityStorageId($service_id), $identity); + return $this; + } + + /** + * {@inheritdoc} + */ + public function retrieveIdentity($service_id) { + $identity = parent::retrieveIdentity($service_id); + if (!$this->service($service_id) instanceof SalesforceEncryptedOAuthPlugin) { + return $identity; + } + $identity = $this->service($service_id)->decrypt($identity); + if (!empty($identity) && !is_array($identity)) { + $identity = unserialize($identity); + } + return $identity; + } + + +} \ No newline at end of file diff --git a/modules/salesforce_encrypt/src/SalesforceEncryptedAuthTokenStorageInterface.php b/modules/salesforce_encrypt/src/SalesforceEncryptedAuthTokenStorageInterface.php new file mode 100644 index 0000000..6321e93 --- /dev/null +++ b/modules/salesforce_encrypt/src/SalesforceEncryptedAuthTokenStorageInterface.php @@ -0,0 +1,10 @@ +loginUser = $loginUser; + $this->encryptKeyId = $encryptKeyId; + } + public function getLoginUser() { + return $this->loginUser; + } + public function getEncryptKeyId() { + return $this->encryptKeyId; + } +} \ No newline at end of file diff --git a/modules/salesforce_jwt/src/Plugin/SalesforceAuthProvider/SalesforceJWTPlugin.php b/modules/salesforce_jwt/src/Plugin/SalesforceAuthProvider/SalesforceJWTPlugin.php new file mode 100644 index 0000000..0f7b094 --- /dev/null +++ b/modules/salesforce_jwt/src/Plugin/SalesforceAuthProvider/SalesforceJWTPlugin.php @@ -0,0 +1,256 @@ +getLoginUrl())); + $this->id = $id; + } + + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + $configuration = array_merge(self::defaultConfiguration(), $configuration); + $cred = new JWTCredentials($configuration['consumer_key'], $configuration['login_url'], $configuration['login_user'], $configuration['encrypt_key']); + return new static($configuration['id'], $cred, $container->get('salesforce.http_client_wrapper'), $container->get('salesforce.auth_token_storage')); + } + + public static function defaultConfiguration() { + $defaults = parent::defaultConfiguration(); + return array_merge($defaults, [ + 'login_user' => '', + 'encrypt_key' => '', + ]); + } + + public function getLoginUrl() { + return $this->credentials->getLoginUrl(); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + if (!$this->keyRepository()) { + $this->messenger()->addError($this->t('JWT Auth requires Key module. Please install before adding a JWT Auth config.')); + return $form; + } + if (!$this->keyRepository()->getKeyNamesAsOptions(['type' => 'authentication'])) { + $this->messenger()->addError($this->t('Please add an authentication key before creating a JWT Auth provider.', ['@href' => Url::fromRoute('entity.key.add_form')->toString()])); + return $form; + } + $form['consumer_key'] = [ + '#title' => t('Salesforce consumer key'), + '#type' => 'textfield', + '#description' => t('Consumer key of the Salesforce remote application you want to grant access to'), + '#required' => TRUE, + '#default_value' => $this->credentials->getConsumerKey(), + ]; + + $form['login_user'] = [ + '#title' => $this->t('Salesforce login user'), + '#type' => 'textfield', + '#description' => $this->t('User account to issue token to'), + '#required' => TRUE, + '#default_value' => $this->credentials->getLoginUser(), + ]; + + $form['login_url'] = [ + '#title' => t('Login URL'), + '#type' => 'textfield', + '#default_value' => $this->credentials->getLoginUrl(), + '#description' => t('Enter a login URL, either https://login.salesforce.com or https://test.salesforce.com.'), + '#required' => TRUE, + ]; + + // Can't use key-select here because its #process method is not firing on ajax, and the list is empty. DERP. + $form['encrypt_key'] = [ + '#title' => 'Private Key', + '#type' => 'select', + '#options' => $this->keyRepository()->getKeyNamesAsOptions(['type' => 'authentication']), + '#required' => TRUE, + '#default_value' => $this->credentials->getEncryptKeyId(), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + if (!$this->keyRepository()) { + $form_state->setError($form, $this->t('JWT Auth requires Key module. Please install before adding a JWT Auth config.')); + return; + } + + parent::validateConfigurationForm($form, $form_state); + try { + $settings = $form_state->getValue('provider_settings'); + $this->getToken($settings['login_url']); + \Drupal::messenger()->addStatus(t('Successfully connected to Salesforce as user %name.', ['%name' => $this->getIdentity()['display_name']])); + } + catch (\Exception $e) { + $form_state->setError($form, $this->t('Failed to connect to Salesforce: %message', ['%message' => $e->getMessage()])); + } + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + $this->setConfiguration($form_state->getValues()); + } + + /** + * Gets a token from the given JWT OAuth endpoint. + */ + protected function getToken($login_url) { + // Initialize access token. + $assertion = $this->generateAssertion(); + $data = [ + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'assertion' => $assertion, + ]; + $response = $this->httpClient->retrieveResponse(new Uri($login_url . static::AUTH_TOKEN_PATH), $data, ['Content-Type' => 'application/x-www-form-urlencoded']); + $token = $this->parseAccessTokenResponse($response); + $this->storage->storeAccessToken($this->service(), $token); + + // Initialize identity. + $headers = [ + 'Authorization' => 'OAuth ' . $token->getAccessToken(), + 'Content-type' => 'application/json', + ]; + $data = $token->getExtraParams(); + $response = $this->httpClient->retrieveResponse(new Uri($data['id']), [], $headers); + $identity = $this->parseIdentityResponse($response); + $this->storage->storeIdentity($this->service(), $identity); + + return $token; + } + + /** + * Refreshes an OAuth2 access token. + * + * @param TokenInterface $token + * + * @return TokenInterface $token + * + * @throws \OAuth\OAuth2\Service\Exception\MissingRefreshTokenException + * @throws \OAuth\Common\Http\Exception\TokenResponseException + */ + public function refreshAccessToken(TokenInterface $token) { + $token = $this->getToken($this->getLoginUrl()); + return $token; + } + + /** + * Key repository wrapper. + * + * @return \Drupal\key\KeyRepository|FALSE + * The key repo. + */ + protected function keyRepository() { + if (!\Drupal::hasService('key.repository')) { + return FALSE; + } + return \Drupal::service('key.repository'); + } + + /** + * Returns a JWT Assertion to authenticate. + * + * @return string + * JWT Assertion. + */ + private function generateAssertion() { + $header = $this->generateAssertionHeader(); + $claim = $this->generateAssertionClaim(); + $header_encoded = $this->b64UrlEncode($header); + $claim_encoded = $this->b64UrlEncode($claim); + $encoded_string = $header_encoded . '.' . $claim_encoded; + $key = $this->keyRepository()->getKey($this->credentials->getEncryptKeyId())->getKeyValue(); + openssl_sign($encoded_string, $signed, $key, 'sha256WithRSAEncryption'); + $signed_encoded = $this->b64UrlEncode($signed); + $assertion = $encoded_string . '.' . $signed_encoded; + return $assertion; + } + + /** + * Returns a JSON encoded JWT Header. + * + * @return string + * The encoded header. + */ + private function generateAssertionHeader() { + $header = new \stdClass(); + $header->alg = 'RS256'; + return json_encode($header); + } + + /** + * Returns a JSON encoded JWT Claim. + * + * @return string + * The encoded claim. + */ + private function generateAssertionClaim() { + $claim = new \stdClass(); + $claim->iss = $this->credentials->getConsumerKey(); + $claim->sub = $this->credentials->getLoginUser(); + $claim->aud = $this->credentials->getLoginUrl(); + $claim->exp = \Drupal::time()->getCurrentTime() + 60; + return json_encode($claim); + } + + /** + * Base 64 URL Safe Encoding. + * + * @param string $data + * String to encode. + * + * @return string + * Encoded string. + */ + private function b64UrlEncode($data) { + return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); + } + +} \ No newline at end of file diff --git a/modules/salesforce_mapping/src/Form/SalesforceMappingDeleteForm.php b/modules/salesforce_mapping/src/Form/SalesforceMappingDeleteForm.php index 066fc61..ec011e9 100644 --- a/modules/salesforce_mapping/src/Form/SalesforceMappingDeleteForm.php +++ b/modules/salesforce_mapping/src/Form/SalesforceMappingDeleteForm.php @@ -7,7 +7,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; /** - * Salesforce Mapping Delete Form . + * Salesforce Mapping Delete Form. */ class SalesforceMappingDeleteForm extends EntityConfirmFormBase { diff --git a/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php b/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php index 51bca52..f2a218c 100644 --- a/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php +++ b/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php @@ -35,7 +35,7 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase { $object_type_options = $this->get_salesforce_object_type_options(); } catch (\Exception $e) { - $href = new Url('salesforce.authorize'); + $href = new Url('salesforce.admin_config_salesforce'); drupal_set_message($this->t('Error when connecting to Salesforce. Please check your credentials and try again: %message', ['@href' => $href->toString(), '%message' => $e->getMessage()]), 'error'); return $form; } diff --git a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/RelatedIDs.php b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/RelatedIDs.php index 55cd016..4e3afcb 100644 --- a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/RelatedIDs.php +++ b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/RelatedIDs.php @@ -64,8 +64,8 @@ class RelatedIDs extends SalesforceMappingFieldPluginBase { } $field = $entity->get($field_name); - if (empty($field->getValue()) || is_null($field->entity)) { - // This reference field is blank or the referenced entity no longer exists. + if (empty($field->getValue())) { + // This reference field is blank. return; } diff --git a/salesforce.info.yml b/salesforce.info.yml index 0e3ba3a..c6a3d3f 100644 --- a/salesforce.info.yml +++ b/salesforce.info.yml @@ -3,4 +3,4 @@ type: module description: Modules to integrate Drupal and Salesforce package: Salesforce core: 8.x -configure: salesforce.config_index +configure: salesforce.admin_config_salesforce diff --git a/salesforce.install b/salesforce.install index 804acf8..03dc192 100644 --- a/salesforce.install +++ b/salesforce.install @@ -5,6 +5,9 @@ */ use Drupal\Component\Serialization\Json; +use Drupal\salesforce\Entity\SalesforceAuthConfig; +use Drupal\salesforce\Token\SalesforceToken; +use Drupal\Core\Utility\UpdateException; /** * Purge Salesforce module state variables. @@ -23,7 +26,7 @@ function salesforce_uninstall() { } /** - * + * Implements hook_requirements(). */ function salesforce_requirements($phase) { if ($phase != 'runtime') { @@ -33,7 +36,12 @@ function salesforce_requirements($phase) { // Check requirements once per 24 hours. $last = \Drupal::state()->get('salesforce.last_requirements_check', 0); - $requirements['salesforce_usage'] = salesforce_get_usage_requirements(); + $requirements['salesforce_auth_provider'] = salesforce_get_auth_provider_requirements(); + + // Don't bother checking usage if we aren't connected to Salesforce. + if ($requirements['salesforce_auth_provider']['severity'] == REQUIREMENT_OK) { + $requirements['salesforce_usage'] = salesforce_get_usage_requirements(); + } $requirements['salesforce_tls'] = salesforce_get_tls_requirements(); if ($last < REQUEST_TIME - (60 * 60 * 24) || empty($requirements['salesforce_tls'])) { @@ -45,6 +53,53 @@ function salesforce_requirements($phase) { } /** + * Check to see if an auth provider has been set. + */ +function salesforce_get_auth_provider_requirements() { + $requirements = [ + 'title' => t('Salesforce Authentication Status'), + 'value' => t('Provider Status'), + ]; + /** @var \Drupal\salesforce\SalesforceAuthProviderPluginManager $authMan */ + $authMan = \Drupal::service('plugin.manager.salesforce.auth_providers'); + if (!$authMan->hasProviders()) { + $requirements += [ + 'description' => t('No auth providers have been created. Please create an auth provider to connect to Salesforce.', ['@href' => \Drupal\Core\Url::fromRoute('entity.salesforce_auth.add_form')]), + 'severity' => REQUIREMENT_WARNING + ]; + } + elseif (!$authMan->getConfig()) { + $requirements += [ + 'description' => t('Default auth provider has not been set. Please choose an auth provider to connect to Salesforce.', ['@href' => \Drupal\Core\Url::fromRoute('salesforce.auth_config')->toString()]), + 'severity' => REQUIREMENT_WARNING + ]; + } + else { + $failMessage = t('Salesforce authentication failed. Please check your auth provider settings to connect to Salesforce.', ['@href' => \Drupal\Core\Url::fromRoute('entity.salesforce_auth.edit_form', ['salesforce_auth' => $authMan->getConfig()])]); + try { + if (!$authMan->getToken()) { + $requirements += [ + 'description' => $failMessage, + 'severity' => REQUIREMENT_WARNING + ]; + } + } + catch (Exception $e) { + $requirements += [ + 'description' => $failMessage, + 'severity' => REQUIREMENT_WARNING + ]; + } + } + if (empty($requirements['severity'])) { + $requirements += [ + 'severity' => REQUIREMENT_OK + ]; + } + return $requirements; +} + +/** * */ function salesforce_fetch_new_tls() { @@ -127,6 +182,7 @@ function salesforce_get_usage_requirements() { } if (empty($usage)) { + // Missing usage information is not necessarily a problem. $requirements += [ 'severity' => REQUIREMENT_OK, 'description' => t('Usage information unavailable'), @@ -147,7 +203,7 @@ function salesforce_get_usage_requirements() { ]; $requirements += [ 'description' => t('Usage: %usage requests of %limit limit (%pct) in the past 24 hours.', $args), - 'severity' => $pct >= 100 ? REQUIREMENT_WARNING : REQUIREMENT_OK, + 'severity' => $pct >= 100 ? REQUIREMENT_ERROR : ($pct >= 80 ? REQUIREMENT_WARNING : REQUIREMENT_OK), ]; } @@ -204,3 +260,93 @@ function salesforce_update_8003() { function salesforce_update_8004() { \Drupal::cache()->delete('salesforce:objects'); } + +/** + * Convert legacy oauth credentials to new auth plugin config. + */ +function salesforce_update_8005() { + $change_list = \Drupal::entityDefinitionUpdateManager()->getChangeSummary(); + if (!empty($change_list['salesforce_auth'])) { + throw new UpdateException("** PENDING SCHEMA UPDATES ** \n** Please install entity updates (entup) to install Salesforce Auth Config before proceeding with database update."); + } + + $config = \Drupal::configFactory()->getEditable('salesforce.settings'); + $state = \Drupal::state(); + $message = ''; + // If auth plugin providers have not been created already, convert existing + if (SalesforceAuthConfig::load('oauth_default')) { + // If an auth config with our name already exists, we are done here. + $message = 'Existing "oauth_default" provider config detected. Refused to set legacy credentials.'; + } + else { + $encryption_enabled = FALSE; + $settings = [ + // Get fresh values here from config, in case they were corrupted by failed decryption. + 'consumer_key' => $config->get('consumer_key'), + 'consumer_secret' => $config->get('consumer_secret'), + 'login_url' => $config->get('login_url'), + ]; + // config to new plugin config system. + $values = [ + 'id' => 'oauth_default', + 'label' => 'OAuth Default', + 'provider' => 'oauth', + 'provider_settings' => $settings, + ]; + + // Convert existing token to new token storage. + $access_token = $state->get('salesforce.access_token'); + $refresh_token = $state->get('salesforce.refresh_token'); + + if (\Drupal::moduleHandler()->moduleExists('salesforce_encrypt')) { + // Not sure how to recover from exceptions here. + // Allow them to bubble up and prevent update. + $encryption_profile = $state->get('salesforce_encrypt.profile'); + $profile = \Drupal::service('encrypt.encryption_profile.manager') + ->getEncryptionProfile($encryption_profile); + /** @var \Drupal\encrypt\EncryptServiceInterface $encryption */ + $encryption = \Drupal::service('encryption'); + $values['provider_settings']['consumer_key'] = + $encryption->decrypt($config->get('consumer_key'), $profile); + $values['provider_settings']['consumer_secret'] = + $encryption->decrypt($config->get('consumer_secret'), $profile); + $refresh_token = $encryption->decrypt($refresh_token, $profile); + $access_token = $encryption->decrypt($access_token, $profile); + if ((empty($values['provider_settings']['consumer_key']) && !empty($config->get('consumer_key'))) + || (empty($values['provider_settings']['consumer_secret']) && !empty($config->get('consumer_secret'))) + || (empty($access_token) && !empty($state->get('salesforce.access_token'))) + || empty($refresh_token) && !empty($state->get('salesforce.refresh_token'))) { + throw new \Exception('Decryption of legacy Salesforce credentials failed. Automated update to Auth Providers not possible. Please disable Salesforce Encrypt module and re-attempt db update.'); + } + $values['provider_settings']['encryption_profile'] = $encryption_profile; + $values['provider'] = 'oauth_encrypted'; + $values['label'] = 'OAuth Default, Encrypted'; + } + + $oauth = SalesforceAuthConfig::create($values); + $oauth->save(); + + $token = new SalesforceToken($access_token, $refresh_token); + \Drupal::service('salesforce.auth_token_storage') + ->storeAccessToken('oauth_default', $token); + + $config->set('salesforce_auth.provider', 'oauth_default'); + $message = 'Default OAuth provider created from legacy credentials.'; + } + + $config->clear('consumer_key') + ->clear('consumer_secret') + ->clear('login_url') + ->save(); + + // Regardless of plugin conversion status, clean up the defunct values. + + $state->deleteMultiple([ + 'salesforce.access_token', + 'salesforce.refresh_token', + 'salesforce.identity', + 'salesforce.instance_url', + 'salesforce_encrypt.profile', + ]); + return $message; +} \ No newline at end of file diff --git a/salesforce.links.action.yml b/salesforce.links.action.yml new file mode 100644 index 0000000..f9d6829 --- /dev/null +++ b/salesforce.links.action.yml @@ -0,0 +1,14 @@ +salesforce_auth.add_action: + route_name: entity.salesforce_auth.add_form + title: 'Add Salesforce Auth Provider' + appears_on: + - entity.salesforce_auth.collection + - entity.salesforce_auth.edit_form + - salesforce.auth_config + +salesforce_auth.list_action: + route_name: entity.salesforce_auth.collection + title: 'Salesforce Auth Provider List' + appears_on: + - entity.salesforce_auth.add_form + - entity.salesforce_auth.edit_form diff --git a/salesforce.links.menu.yml b/salesforce.links.menu.yml index 2d7e8f1..92bae5c 100644 --- a/salesforce.links.menu.yml +++ b/salesforce.links.menu.yml @@ -17,16 +17,14 @@ salesforce.global_settings: description: 'Manage global settings for Salesforce Suite.' weight: -100 -salesforce.authorize: - route_name: salesforce.authorize +salesforce.auth_config: + route_name: salesforce.auth_config parent: salesforce.admin_config_salesforce title: Salesforce Authorization - description: 'Manage OAuth consumer key and secret and authorize. View existing authorization details.' - weight: 99 + description: 'Salesforce Authorization.' -salesforce.revoke: - route_name: salesforce.revoke - parent: salesforce.admin_config_salesforce - title: Revoke Salesforce Authorization - description: 'Revoke OAuth tokens.' - weight: 100 +entity.salesforce_auth.collection: + route_name: entity.salesforce_auth.collection + parent: salesforce.auth_config + title: Salesforce Authorization Providers + description: 'Salesforce Authorization Providers.' diff --git a/salesforce.links.task.yml b/salesforce.links.task.yml new file mode 100644 index 0000000..f2715a9 --- /dev/null +++ b/salesforce.links.task.yml @@ -0,0 +1,9 @@ +salesforce.auth_config: + route_name: salesforce.auth_config + base_route: salesforce.auth_config + title: 'Authorization' + +entity.salesforce_auth.collection: + route_name: entity.salesforce_auth.collection + base_route: salesforce.auth_config + title: 'Providers' diff --git a/salesforce.module b/salesforce.module index d734e7e..cf7b92f 100644 --- a/salesforce.module +++ b/salesforce.module @@ -20,7 +20,7 @@ function salesforce_help($route_name, RouteMatchInterface $route_match) { } $client = \Drupal::service('salesforce.client'); if (!$client->isAuthorized()) { - $output .= '

' . t('You must authorize your account with Salesforce in order to configure Salesforce Mappings.', [':authorize' => (new Url('salesforce.authorize'))->toString()]) . '

'; + $output .= '

' . t('You must authorize your account with Salesforce in order to configure Salesforce Mappings.', [':authorize' => (new Url('salesforce.admin_config_salesforce'))->toString()]) . '

'; } return $output; @@ -45,7 +45,7 @@ function salesforce_help($route_name, RouteMatchInterface $route_match) { $output .= '