From d006ced6293c700c0bc4db5b4824fd2b446a0964 Mon Sep 17 00:00:00 2001 From: Doug Green Date: Thu, 8 Feb 2018 11:17:09 -0500 Subject: [PATCH] #2850985: Add credential provider configuration for storing credentials elsewhere, such as key module. --- cloudflare.install | 17 ++ config/install/cloudflare.settings.yml | 12 +- config/schema/cloudflare.settings.schema.yml | 38 ++++- .../Plugin/Purge/Purger/CloudFlarePurger.php | 6 +- src/CloudFlareCredentials.php | 105 ++++++++++++ src/Form/SettingsForm.php | 151 ++++++++++++++---- src/Tests/CloudFlareAdminSettingsFormTest.php | 36 ++--- ...CloudFlareAdminSettingsInvalidFormTest.php | 36 ++--- src/Zone.php | 5 +- 9 files changed, 328 insertions(+), 78 deletions(-) create mode 100644 src/CloudFlareCredentials.php diff --git a/cloudflare.install b/cloudflare.install index 8a5fa68..968476b 100644 --- a/cloudflare.install +++ b/cloudflare.install @@ -48,3 +48,20 @@ function cloudflare_requirements($phase) { function cloudflare_update_8001(&$sandbox) { \Drupal::service('module_installer')->install(['ctools']); } + +/** + * Convert user and pass config to credential provider config. + */ +function cloudflare_update_8002() { + $config = \Drupal::configFactory()->getEditable('cloudflare.settings'); + $email = $config->get('email'); + if ($email) { + $config + ->set('credential_provider', 'config') + ->set('credentials.cloudflare.email', $email) + ->set('credentials.cloudflare.apikey', $config->get('apikey')) + ->clear('email') + ->clear('apikey') + ->save(TRUE); + } +} diff --git a/config/install/cloudflare.settings.yml b/config/install/cloudflare.settings.yml index ab22eeb..039f7d3 100644 --- a/config/install/cloudflare.settings.yml +++ b/config/install/cloudflare.settings.yml @@ -2,5 +2,13 @@ client_ip_restore_enabled: false bypass_host: '' valid_credentials: false zone_id: '' -apikey: '' -email: '' +credential_provider: 'cloudflare' +credentials: + cloudflare: + email: '' + apikey: '' + key: + email: '' + apikey_key: '' + multikey: + email_apikey_key: '' diff --git a/config/schema/cloudflare.settings.schema.yml b/config/schema/cloudflare.settings.schema.yml index ad5cff8..36522a8 100644 --- a/config/schema/cloudflare.settings.schema.yml +++ b/config/schema/cloudflare.settings.schema.yml @@ -19,11 +19,41 @@ cloudflare.settings: type: string label: 'CloudFlare ZoneId corresponding to the site domain.' translatable: false + credential_provider: + type: 'string' + label: 'Credential provider' + credentials: + type: sequence + label: 'Credentials' + sequence: + type: cloudflare.credentials.[%key] + +cloudflare.credentials.cloudflare: + type: mapping + label: 'Cloudflare credentials' + mapping: + email: + type: string + label: 'Email' apikey: type: string - label: 'ApiKey used to authenticate against CloudFlare' - translatable: false + label: 'API Key' + +cloudflare.credentials.key: + type: mapping + label: 'Cloudflare credentials with Key Module' + mapping: email: type: string - label: 'Email used to authenticate against CloudFlare.' - translatable: false + label: 'Email' + apikey_key: + type: string + label: 'API key' + +cloudflare.credentials.multikey: + type: mapping + label: 'Cloudflare credentials with Key Module (user/password keys)' + mapping: + email_apikey_key: + type: string + label: 'Email/API key (User/Password)' diff --git a/modules/cloudflarepurger/src/Plugin/Purge/Purger/CloudFlarePurger.php b/modules/cloudflarepurger/src/Plugin/Purge/Purger/CloudFlarePurger.php index 4fed2ef..bde2f11 100644 --- a/modules/cloudflarepurger/src/Plugin/Purge/Purger/CloudFlarePurger.php +++ b/modules/cloudflarepurger/src/Plugin/Purge/Purger/CloudFlarePurger.php @@ -3,6 +3,7 @@ namespace Drupal\cloudflarepurger\Plugin\Purge\Purger; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\cloudflare\CloudFlareCredentials; use Drupal\cloudflare\CloudFlareStateInterface; use Drupal\cloudflare\CloudFlareComposerDependenciesCheckInterface; use Drupal\cloudflarepurger\EventSubscriber\CloudFlareCacheTagHeaderGenerator; @@ -166,8 +167,9 @@ private function purgeChunk(array &$invalidations) { // This is a unique case where the ApiSdk is being accessed directly and not // via a service. Purging should only ever happen through the purge module // which is why this is NOT in a service. - $api_key = $this->config->get('apikey'); - $email = $this->config->get('email'); + $credentials = new CloudFlareCredentials($this->config); + $api_key = $credentials->getApikey(); + $email = $credentials->getEmail(); $this->zone = $this->config->get('zone_id'); $this->zoneApi = new ZoneApi($api_key, $email); diff --git a/src/CloudFlareCredentials.php b/src/CloudFlareCredentials.php new file mode 100644 index 0000000..dd7a736 --- /dev/null +++ b/src/CloudFlareCredentials.php @@ -0,0 +1,105 @@ +get('credential_provider'); + $credentials = $config->get('credentials'); + if ($credentials) { + $this->setCredentials($credential_provider, $credentials); + } + } + } + + /** + * Set the credentials from configuration array. + * + * @param string $credential_provider + * The credential provider. + * @param array $providers + * Nested array of all the credential providers. + */ + public function setCredentials($credential_provider, array $providers) { + switch ($credential_provider) { + case 'cloudflare': + $this->email = $providers['cloudflare']['email']; + $this->apikey = $providers['cloudflare']['apikey']; + break; + + case 'key': + $this->email = $providers['key']['email']; + + /** @var \Drupal\Core\Entity\EntityStorageInterface $storage */ + $storage = \Drupal::entityTypeManager()->getStorage('key'); + /** @var \Drupal\key\KeyInterface $apikey_key */ + $apikey_key = $storage->load($providers['key']['apikey_key']); + if ($apikey_key) { + $this->apikey = $apikey_key->getKeyValue(); + } + break; + + case 'multikey': + /** @var \Drupal\Core\Entity\EntityStorageInterface $storage */ + $storage = \Drupal::entityTypeManager()->getStorage('key'); + /** @var \Drupal\key\KeyInterface $key */ + $key = $storage->load($providers['multikey']['email_apikey_key']); + if ($key) { + $values = $key->getKeyValues(); + $this->email = $values['username']; + $this->apikey = $values['password']; + } + break; + } + } + + /** + * Return the email address. + * + * @return string + * The email. + */ + public function getEmail() { + return $this->email; + } + + /** + * Return the API Key. + * + * @return string + * The API key. + */ + public function getApikey() { + return $this->apikey; + } + +} diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php index 48f6427..c368684 100644 --- a/src/Form/SettingsForm.php +++ b/src/Form/SettingsForm.php @@ -8,6 +8,7 @@ use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; +use Drupal\cloudflare\CloudFlareCredentials; use Drupal\cloudflare\CloudFlareStateInterface; use Drupal\cloudflare\CloudFlareZoneInterface; use Drupal\cloudflare\CloudFlareComposerDependenciesCheckInterface; @@ -140,8 +141,11 @@ public function getFormId() { * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { + // Nest submitted form values. + $form['#tree'] = TRUE; + $config = $this->configFactory->get('cloudflare.settings'); - $form = array_merge($form, $this->buildApiCredentialsSection($config)); + $form = array_merge($form, $this->buildApiCredentialsSection($config, $form_state)); $form = array_merge($form, $this->buildZoneSelectSection($config)); $form = array_merge($form, $this->buildGeneralConfig($config)); @@ -151,8 +155,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { if (!$this->cloudFlareComposerDependenciesMet) { drupal_set_message((CloudFlareComposerDependenciesCheckInterface::ERROR_MESSAGE), 'error'); - $form['api_credentials_fieldset']['apikey']['#disabled'] = TRUE; - $form['api_credentials_fieldset']['email']['#disabled'] = TRUE; + $form['credentials']['credential_provider']['#disabled'] = TRUE; $form['cloudflare_config']['client_ip_restore_enabled']['#disabled'] = TRUE; $form['cloudflare_config']['bypass_host']['#disabled'] = TRUE; $form['actions']['submit']['#disabled'] = TRUE; @@ -166,31 +169,103 @@ public function buildForm(array $form, FormStateInterface $form_state) { * * @param \Drupal\Core\Config\Config $config * The readonly configuration. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. * * @return array * Form Api render array with credentials section. */ - protected function buildApiCredentialsSection(Config $config) { + protected function buildApiCredentialsSection(Config $config, FormStateInterface $form_state) { $section = []; - $section['api_credentials_fieldset'] = [ - '#type' => 'fieldset', - '#title' => $this->t('API Credentials'), + $section['credentials'] = [ + '#id' => 'credentials', + '#type' => 'details', + '#title' => $this->t('Credentials'), + '#open' => TRUE, ]; - $section['api_credentials_fieldset']['apikey'] = [ - '#type' => 'textfield', - '#title' => $this->t('CloudFlare API Key'), - '#description' => $this->t('Your API key. Get it at cloudflare.com/a/account/my-account.'), - '#default_value' => $config->get('apikey'), - '#required' => TRUE, + + $credential_provider = $config->get('credential_provider'); + $credential_provider = ($form_state->hasValue(['credentials', 'credential_provider'])) ? $form_state->getValue(['credentials', 'credential_provider']) : $credential_provider; + + $section['credentials']['credential_provider'] = [ + '#type' => 'select', + '#title' => $this->t('Credential provider'), + '#options' => [ + 'cloudflare' => $this->t('Local configuration'), + ], + '#default_value' => $credential_provider, + '#ajax' => [ + 'callback' => [$this, 'ajaxCallback'], + 'wrapper' => 'credentials_configuration', + 'method' => 'replace', + 'effect' => 'fade', + ], ]; - $section['api_credentials_fieldset']['email'] = [ - '#type' => 'textfield', - '#title' => $this->t('Account e-mail address'), - '#default_value' => $config->get('email'), - '#required' => TRUE, + + $section['credentials']['providers'] = [ + '#type' => 'item', + '#id' => 'credentials_configuration', ]; + if (\Drupal::moduleHandler()->moduleExists('key')) { + $section['credentials']['credential_provider']['#options']['key'] = $this->t('Key Module'); + + /** @var \Drupal\key\Plugin\KeyPluginManager $key_type */ + $key_type = \Drupal::service('plugin.manager.key.key_type'); + if ($key_type->hasDefinition('user_password')) { + $section['credentials']['credential_provider']['#options']['multikey'] = $this->t('Key Module (user/password)'); + } + } + + if ($credential_provider == 'cloudflare') { + $section['credentials']['providers']['cloudflare']['email'] = [ + '#type' => 'textfield', + '#title' => $this->t('Account e-mail address'), + '#default_value' => $config->get('credentials.cloudflare.email'), + '#required' => TRUE, + ]; + $section['credentials']['providers']['cloudflare']['apikey'] = [ + '#type' => 'textfield', + '#title' => $this->t('CloudFlare API Key'), + '#default_value' => $config->get('credentials.cloudflare.apikey'), + '#description' => $this->t('Your API key. Get it at cloudflare.com/a/account/my-account.'), + '#required' => TRUE, + ]; + } + elseif ($credential_provider == 'key') { + $email = $config->get('credentials.key.email'); + if (empty($email)) { + $email = $config->get('credentials.cloudflare.email'); + } + $section['credentials']['providers']['key']['email'] = [ + '#type' => 'textfield', + '#title' => $this->t('Account e-mail address'), + '#default_value' => $email, + '#required' => TRUE, + ]; + $section['credentials']['providers']['key']['apikey_key'] = [ + '#type' => 'key_select', + '#title' => $this->t('API Key'), + '#default_value' => $config->get('credentials.key.apikey_key'), + '#empty_option' => $this->t('- Please select -'), + '#key_filters' => ['type' => 'authentication'], + '#description' => $this->t('Your API key stored as a secure key. Get it at cloudflare.com/a/account/my-account.'), + '#required' => TRUE, + ]; + } + elseif ($credential_provider == 'multikey') { + $section['credentials']['providers']['multikey']['email_apikey_key'] = [ + '#type' => 'key_select', + '#title' => $this->t('Email/API key (User/Password)'), + '#default_value' => $config->get('credentials.multikey.email_apikey_key'), + '#empty_option' => $this->t('- Please select -'), + '#key_filters' => ['type' => 'user_password'], + '#description' => $this->t('Your account e-mail address and API key stored as a secure key. Get it at cloudflare.com/a/account/my-account.'), + '#required' => TRUE, + ]; + } + return $section; } @@ -290,8 +365,12 @@ protected function buildGeneralConfig(Config $config) { */ public function validateForm(array &$form, FormStateInterface $form_state) { // Get the email address and apikey. - $email = trim($form_state->getValue('email')); - $apikey = trim($form_state->getValue('apikey')); + $credentials = new CloudFlareCredentials(); + $credential_provider = $form_state->getValue(['credentials', 'credential_provider']); + $credentials_values = $form_state->getValue(['credentials', 'providers']); + $credentials->setCredentials($credential_provider, $credentials_values); + $email = $credentials->getEmail(); + $apikey = $credentials->getApikey(); // Validate the email address. if (!$this->emailValidator->isValid($email)) { @@ -305,16 +384,16 @@ public function validateForm(array &$form, FormStateInterface $form_state) { } catch (CloudFlareTimeoutException $e) { $message = $this->t('Unable to connect to CloudFlare in order to validate credentials. Connection timed out. Please try again later.'); - $form_state->setErrorByName('apikey', $message); + $form_state->setErrorByName('providers', $message); $this->logger->error($message); return; } catch (CloudFlareInvalidCredentialException $e) { - $form_state->setErrorByName('apiKey', $e->getMessage()); + $form_state->setErrorByName('providers', $e->getMessage()); return; } catch (CloudFlareException $e) { - $form_state->setErrorByName('apikey', $this->t("An unknown error has occurred when attempting to connect to CloudFlare's API") . $e->getMessage()); + $form_state->setErrorByName('providers', $this->t("An unknown error has occurred when attempting to connect to CloudFlare's API") . $e->getMessage()); return; } @@ -342,21 +421,35 @@ public function validateForm(array &$form, FormStateInterface $form_state) { * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { - $api_key = trim($form_state->getValue('apikey')); - $email = trim($form_state->getValue('email')); - // Deslash the host URL. $bypass_host = trim(rtrim($form_state->getValue('bypass_host'), "/")); $client_ip_restore_enabled = $form_state->getValue('client_ip_restore_enabled'); + // Save the configuration. + $credential_provider = $form_state->getValue(['credentials', 'credential_provider']); $config = $this->configFactory->getEditable('cloudflare.settings'); + $credentials = $form_state->getValue([ + 'credentials', + 'providers', + $credential_provider, + ]); $config - ->set('apikey', $api_key) - ->set('email', $email) ->set('valid_credentials', TRUE) ->set('bypass_host', $bypass_host) - ->set('client_ip_restore_enabled', $client_ip_restore_enabled); + ->set('client_ip_restore_enabled', $client_ip_restore_enabled) + ->set('credential_provider', $credential_provider) + ->set("credentials.$credential_provider", $credentials); $config->save(); } + /** + * Ajax callback for the credential dependent configuration options. + * + * @return array + * The form element containing the configuration options. + */ + public static function ajaxCallback($form, FormStateInterface $form_state) { + return $form['credentials']['providers']; + } + } diff --git a/src/Tests/CloudFlareAdminSettingsFormTest.php b/src/Tests/CloudFlareAdminSettingsFormTest.php index 6bd759a..bc26cf9 100644 --- a/src/Tests/CloudFlareAdminSettingsFormTest.php +++ b/src/Tests/CloudFlareAdminSettingsFormTest.php @@ -29,6 +29,13 @@ class CloudFlareAdminSettingsFormTest extends WebTestBase { */ protected $route = 'cloudflare.admin_settings_form'; + /** + * Edit form of credentials, used in all tests. + * + * @var array + */ + protected $edit; + /** * Setup the test. */ @@ -37,6 +44,11 @@ public function setUp() { $this->adminUser = $this->drupalCreateUser(['access administration pages']); $this->route = Url::fromRoute('cloudflare.admin_settings_form'); + $this->edit = [ + 'credentials.credential_provider' => 'cloudflare', + 'credentials.providers.apikey' => '68ow48650j63zfzx1w9jd29cr367u0ezb6a4g', + 'credentials.providers.email' => 'test@test.com', + ]; $this->drupalLogin($this->adminUser); ZoneMock::mockAssertValidCredentials(TRUE); ComposerDependenciesCheckMock::mockComposerDependenciesMet(TRUE); @@ -46,12 +58,8 @@ public function setUp() { * Test posting an invalid host to the form. */ public function testValidCredentials() { - $edit = [ - 'apikey' => '68ow48650j63zfzx1w9jd29cr367u0ezb6a4g', - 'email' => 'test@test.com', - ]; ComposerDependenciesCheckMock::mockComposerDependenciesMet(TRUE); - $this->drupalPostForm($this->route, $edit, t('Next')); + $this->drupalPostForm($this->route, $this->edit, t('Next')); $this->assertUrl('/admin/config/services/cloudflare/two?js=nojs'); $this->drupalPostForm(NULL, [], t('Finish')); $this->assertRaw('68ow48650j63zfzx1w9jd29cr367u0ezb6a4g'); @@ -64,13 +72,9 @@ public function testValidCredentials() { */ public function testMultiZoneSelection() { ZoneMock::mockAssertValidCredentials(TRUE); - $edit = [ - 'apikey' => '68ow48650j63zfzx1w9jd29cr367u0ezb6a4g', - 'email' => 'test@test.com', - ]; ComposerDependenciesCheckMock::mockComposerDependenciesMet(TRUE); ZoneMock::mockMultiZoneAccount(TRUE); - $this->drupalPostForm($this->route, $edit, t('Next')); + $this->drupalPostForm($this->route, $this->edit, t('Next')); $this->assertUrl('/admin/config/services/cloudflare/two?js=nojs'); $this->drupalPostForm(NULL, ['zone_selection' => "123456789999"], t('Finish')); $this->assertRaw('68ow48650j63zfzx1w9jd29cr367u0ezb6a4g'); @@ -81,9 +85,7 @@ public function testMultiZoneSelection() { * Test posting an invalid host with https protocol to the form. */ public function testInvalidBypassHostWithHttps() { - $edit = [ - 'apikey' => '68ow48650j63zfzx1w9jd29cr367u0ezb6a4g', - 'email' => 'test@test.com', + $edit = $this->edit + [ 'client_ip_restore_enabled' => TRUE, 'bypass_host' => 'https://blah.com', ]; @@ -105,9 +107,7 @@ public function testInvalidBypassHostWithHttps() { * Test posting an invalid host with http protocol to the form. */ public function testInvalidBypassHostWithHttp() { - $edit = [ - 'apikey' => '68ow48650j63zfzx1w9jd29cr367u0ezb6a4g', - 'email' => 'test@test.com', + $edit = $this->edit + [ 'client_ip_restore_enabled' => TRUE, 'bypass_host' => 'http://blah.com', ]; @@ -120,9 +120,7 @@ public function testInvalidBypassHostWithHttp() { * Test posting an invalid host to the form. */ public function testInvalidBypassHost() { - $edit = [ - 'apikey' => '68ow48650j63zfzx1w9jd29cr367u0ezb6a4g', - 'email' => 'test@test.com', + $edit = $this->edit + [ 'client_ip_restore_enabled' => TRUE, 'bypass_host' => 'blah!@#!@', ]; diff --git a/src/Tests/CloudFlareAdminSettingsInvalidFormTest.php b/src/Tests/CloudFlareAdminSettingsInvalidFormTest.php index 79bd75f..d7420bd 100644 --- a/src/Tests/CloudFlareAdminSettingsInvalidFormTest.php +++ b/src/Tests/CloudFlareAdminSettingsInvalidFormTest.php @@ -31,6 +31,13 @@ class CloudFlareAdminSettingsInvalidFormTest extends WebTestBase { */ protected $route = 'cloudflare.admin_settings_form'; + /** + * Edit form of credentials, used in all tests. + * + * @var array + */ + protected $edit; + /** * Setup the test. */ @@ -39,6 +46,11 @@ public function setUp() { $this->adminUser = $this->drupalCreateUser(['access administration pages']); $this->route = Url::fromRoute('cloudflare.admin_settings_form'); + $this->edit = [ + 'credentials.credential_provider' => 'cloudflare', + 'credentials.providers.apikey' => '68ow48650j63zfzx1w9jd29cr367u0ezb6a4g', + 'credentials.providers.email' => 'test@test.com', + ]; ComposerDependenciesCheckMock::mockComposerDependenciesMet(TRUE); } @@ -85,11 +97,7 @@ public function testInvalidCredentials() { $container->set('cloudflare.zone', $zone_mock); $this->drupalLogin($this->adminUser); - $edit = [ - 'apikey' => '68ow48650j63zfzx1w9jd29cr367u0ezb6a4g', - 'email' => 'test@test.com', - ]; - $this->drupalPostForm($this->route, $edit, t('Next')); + $this->drupalPostForm($this->route, $this->edit, t('Next')); $this->assertUrl('/admin/config/services/cloudflare'); } @@ -99,12 +107,8 @@ public function testInvalidCredentials() { public function testUpperCaseInvalidCredentials() { ZoneMock::mockAssertValidCredentials(TRUE); ComposerDependenciesCheckMock::mockComposerDependenciesMet(TRUE); - $edit = [ - 'apikey' => 'fDK5M9sf51x6CEAspHSUYM4vt40m5XC2T6i1K', - 'email' => 'test@test.com', - ]; $this->drupalLogin($this->adminUser); - $this->drupalPostForm($this->route, $edit, t('Next')); + $this->drupalPostForm($this->route, $this->edit, t('Next')); $this->assertText('Invalid Api Key: Key can only contain lowercase or numerical characters.'); } @@ -114,12 +118,8 @@ public function testUpperCaseInvalidCredentials() { public function testInvalidKeyLength() { ZoneMock::mockAssertValidCredentials(TRUE); ComposerDependenciesCheckMock::mockComposerDependenciesMet(TRUE); - $edit = [ - 'apikey' => '68ow48650j63zfzx1w9jd29cr367u0ezb6a4g0', - 'email' => 'test@test.com', - ]; $this->drupalLogin($this->adminUser); - $this->drupalPostForm($this->route, $edit, t('Next')); + $this->drupalPostForm($this->route, $this->edit, t('Next')); $this->assertText('Invalid Api Key: Key should be 37 chars long.'); } @@ -129,12 +129,8 @@ public function testInvalidKeyLength() { public function testInvalidKeySpecialChars() { ZoneMock::mockAssertValidCredentials(TRUE); ComposerDependenciesCheckMock::mockComposerDependenciesMet(FALSE); - $edit = [ - 'apikey' => '!8ow48650j63zfzx1w9jd29cr367u0ezb6a4g', - 'email' => 'test@test.com', - ]; $this->drupalLogin($this->adminUser); - $this->drupalPostForm($this->route, $edit, t('Next')); + $this->drupalPostForm($this->route, $this->edit, t('Next')); $this->assertText('Invalid Api Key: Key can only contain alphanumeric characters.'); } diff --git a/src/Zone.php b/src/Zone.php index 74e7768..79c1c2b 100644 --- a/src/Zone.php +++ b/src/Zone.php @@ -79,8 +79,9 @@ class Zone implements CloudFlareZoneInterface { */ public static function create(ConfigFactoryInterface $config_factory, LoggerInterface $logger, CacheBackendInterface $cache, CloudFlareStateInterface $state, CloudFlareComposerDependenciesCheckInterface $check_interface) { $config = $config_factory->get('cloudflare.settings'); - $api_key = $config->get('apikey'); - $email = $config->get('email'); + $credentials = new CloudFlareCredentials($config); + $api_key = $credentials->getApikey(); + $email = $credentials->getEmail(); // If someone has not correctly installed composer here is where we need to // handle it to prevent PHP error.