diff --git a/README.txt b/README.txt
index f689305..7a02f5a 100644
--- a/README.txt
+++ b/README.txt
@@ -203,6 +203,17 @@ On the client website:
(admin/content/entity_share/pull), and select your remote website, the
available channels will be listed and when selecting a channel, the entities
exposed on this channel will be available to synchronize.
+ * Optional Key Module Integration
+ - https://www.drupal.org/project/key
+ - https://www.drupal.org/docs/8/modules/key/concepts-and-terminology
+ Credentials used to authorize pulling from remotes may be more securely stored
+ using the Key module. Additional optional modules allow the storage in an
+ external key/value storage service. With only the Key module credentials may
+ be stored in JSON format in files outside the web root.
+ 1. Configure Keys: Key types for Entity Share are listed in Key config form
+ (/admin/config/system/keys). Instructions for each type are shown in the form.
+ 2. Create a remote and select Key module as the credential provider, then
+ select the appropriate key.
TROUBLESHOOTING
diff --git a/modules/entity_share_client/config/schema/remote.schema.yml b/modules/entity_share_client/config/schema/remote.schema.yml
index cd30b16..bd4bdb1 100644
--- a/modules/entity_share_client/config/schema/remote.schema.yml
+++ b/modules/entity_share_client/config/schema/remote.schema.yml
@@ -11,9 +11,28 @@ entity_share_client.remote.*:
url:
type: string
label: 'URL'
- basic_auth_username:
- type: string
- label: 'Basic auth username'
- basic_auth_password:
- type: string
- label: 'Basic auth password'
+ auth:
+ type: mapping
+ mapping:
+ pid:
+ label: 'Plugin ID'
+ type: string
+ uuid:
+ label: 'UUID'
+ type: string
+ verified:
+ label: 'Verified'
+ type: boolean
+ data:
+ type: mapping
+ label: 'Credential data'
+ mapping:
+ credential_provider:
+ type: string
+ label: 'Credential provider'
+ storage_key:
+ type: string
+ label: 'Storage key'
+
+key.type.entity_share_basic_auth:
+ type: sequence
diff --git a/modules/entity_share_client/entity_share_client.install b/modules/entity_share_client/entity_share_client.install
new file mode 100644
index 0000000..5ffe354
--- /dev/null
+++ b/modules/entity_share_client/entity_share_client.install
@@ -0,0 +1,39 @@
+createInstance('basic_auth');
+ $configuration = $plugin->getConfiguration();
+ $credentials = [];
+ $credentials['username'] = $remote->get('basic_auth_username');
+ $credentials['password'] = $remote->get('basic_auth_password');
+ unset($remote->basic_auth_username);
+ unset($remote->basic_auth_password);
+ $state->set($configuration['uuid'], $credentials);
+ $key = $configuration['uuid'];
+ $configuration['data'] = [
+ 'credential_provider' => 'entity_share',
+ 'storage_key' => $key,
+ ];
+ $plugin->setConfiguration($configuration);
+ $remote->mergePluginConfig($plugin);
+ $remote->save();
+ }
+}
diff --git a/modules/entity_share_client/entity_share_client.services.yml b/modules/entity_share_client/entity_share_client.services.yml
index 45bc927..222038a 100644
--- a/modules/entity_share_client/entity_share_client.services.yml
+++ b/modules/entity_share_client/entity_share_client.services.yml
@@ -9,7 +9,6 @@ services:
entity_share_client.remote_manager:
class: Drupal\entity_share_client\Service\RemoteManager
arguments:
- - '@http_client_factory'
- '@entity_share_client.request'
entity_share_client.jsonapi_helper:
@@ -61,3 +60,13 @@ services:
- '@entity_share_client.jsonapi_helper'
- '@entity_type.manager'
- '@entity_share_client.request'
+
+ entity_share_client.key_provider:
+ class: Drupal\entity_share_client\Service\KeyProvider
+ arguments: ['@state']
+ calls:
+ - [setKeyRepository, ['@?key.repository']]
+
+ plugin.manager.entity_share_auth:
+ class: Drupal\entity_share_client\Plugin\ClientAuthorizationManager
+ parent: default_plugin_manager
diff --git a/modules/entity_share_client/src/Annotation/ClientAuthorization.php b/modules/entity_share_client/src/Annotation/ClientAuthorization.php
new file mode 100644
index 0000000..8a8a5b2
--- /dev/null
+++ b/modules/entity_share_client/src/Annotation/ClientAuthorization.php
@@ -0,0 +1,41 @@
+auth;
+ if (!empty($pluginData['pid'])) {
+ // DI not available in entities:
+ // https://www.drupal.org/project/drupal/issues/2142515.
+ /** @var \Drupal\entity_share_client\Plugin\ClientAuthorizationManager $manager */
+ $manager = \Drupal::service('plugin.manager.entity_share_auth');
+ $pluginId = $pluginData['pid'];
+ unset($pluginData['pid']);
+ return $manager->createInstance($pluginId, $pluginData);
+ }
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function mergePluginConfig(ClientAuthorizationInterface $plugin) {
+ $auth = ['pid' => $plugin->getPluginId()] +
+ $plugin->getConfiguration();
+ $this->auth = $auth;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getHttpClient(bool $json) {
+ $plugin = $this->getAuthPlugin();
+ if ($json) {
+ return $plugin->getJsonApiClient($this->url);
+ }
+ return $plugin->getClient($this->url);
+ }
+
}
diff --git a/modules/entity_share_client/src/Entity/RemoteInterface.php b/modules/entity_share_client/src/Entity/RemoteInterface.php
index 77d1147..e667f5f 100644
--- a/modules/entity_share_client/src/Entity/RemoteInterface.php
+++ b/modules/entity_share_client/src/Entity/RemoteInterface.php
@@ -5,11 +5,40 @@ declare(strict_types = 1);
namespace Drupal\entity_share_client\Entity;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\entity_share_client\Plugin\ClientAuthorizationInterface;
/**
* Provides an interface for defining Remote entities.
*/
interface RemoteInterface extends ConfigEntityInterface {
- // Add get/set methods for your configuration properties here.
+ /**
+ * Copies plugin specific data into the Remote.
+ *
+ * @param \Drupal\entity_share_client\Plugin\ClientAuthorizationInterface $plugin
+ * The authorization plugin to merge.
+ */
+ public function mergePluginConfig(ClientAuthorizationInterface $plugin);
+
+ /**
+ * Helper method to instantiate auth plugin from this entity.
+ *
+ * @return \Drupal\entity_share_client\Plugin\ClientAuthorizationInterface|null
+ * The plugin if it is defined.
+ *
+ * @throws \Drupal\Component\Plugin\Exception\PluginException
+ */
+ public function getAuthPlugin();
+
+ /**
+ * Prepares a client object with options pulled from the auth plugin.
+ *
+ * @param bool $json
+ * Is this client for JSON operations?
+ *
+ * @return \GuzzleHttp\Client
+ * The configured client.
+ */
+ public function getHttpClient(bool $json);
+
}
diff --git a/modules/entity_share_client/src/Form/RemoteForm.php b/modules/entity_share_client/src/Form/RemoteForm.php
index 397077b..9660750 100644
--- a/modules/entity_share_client/src/Form/RemoteForm.php
+++ b/modules/entity_share_client/src/Form/RemoteForm.php
@@ -7,6 +7,10 @@ namespace Drupal\entity_share_client\Form;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Form\SubformState;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\entity_share_client\Plugin\ClientAuthorizationInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class RemoteForm.
@@ -15,6 +19,29 @@ use Drupal\Core\Form\FormStateInterface;
*/
class RemoteForm extends EntityForm {
+ /**
+ * Injected plugin service.
+ *
+ * @var \Drupal\entity_share_client\Plugin\ClientAuthorizationManager
+ */
+ protected $authPluginManager;
+
+ /**
+ * The currently configured auth plugin.
+ *
+ * @var \Drupal\entity_share_client\Plugin\ClientAuthorizationInterface
+ */
+ protected $authPlugin;
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ $instance = parent::create($container);
+ $instance->authPluginManager = $container->get('plugin.manager.entity_share_auth');
+ return $instance;
+ }
+
/**
* {@inheritdoc}
*/
@@ -36,6 +63,7 @@ class RemoteForm extends EntityForm {
'#type' => 'machine_name',
'#default_value' => $remote->id(),
'#machine_name' => [
+ 'source' => ['label'],
'exists' => '\Drupal\entity_share_client\Entity\Remote::load',
],
'#disabled' => !$remote->isNew(),
@@ -50,23 +78,7 @@ class RemoteForm extends EntityForm {
'#required' => TRUE,
];
- $form['basic_auth'] = [
- '#type' => 'fieldset',
- '#title' => $this->t('Basic Auth'),
- ];
-
- $form['basic_auth']['basic_auth_username'] = [
- '#type' => 'textfield',
- '#title' => $this->t('Username'),
- '#default_value' => $remote->get('basic_auth_username'),
- '#required' => TRUE,
- ];
-
- $form['basic_auth']['basic_auth_password'] = [
- '#type' => 'password',
- '#title' => $this->t('Password'),
- '#required' => TRUE,
- ];
+ $this->addAuthOptions($form, $form_state);
return $form;
}
@@ -79,6 +91,26 @@ class RemoteForm extends EntityForm {
if (!UrlHelper::isValid($form_state->getValue('url'), TRUE)) {
$form_state->setError($form['url'], $this->t('Invalid URL.'));
}
+ $selectedPlugin = $this->getSelectedPlugin($form, $form_state);
+ if ($selectedPlugin instanceof PluginFormInterface) {
+ $subformState = SubformState::createForSubform($form['auth']['data'], $form, $form_state);
+ $selectedPlugin->validateConfigurationForm($form['auth']['data'], $subformState);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ parent::submitForm(
+ $form,
+ $form_state
+ );
+ $selectedPlugin = $this->getSelectedPlugin($form, $form_state);
+ $subformState = SubformState::createForSubform($form['auth']['data'], $form, $form_state);
+ // Store the remote entity in case the plugin submission needs its data.
+ $subformState->set('remote', $this->entity);
+ $selectedPlugin->submitConfigurationForm($form['auth']['data'], $subformState);
}
/**
@@ -87,6 +119,12 @@ class RemoteForm extends EntityForm {
public function save(array $form, FormStateInterface $form_state) {
/** @var \Drupal\entity_share_client\Entity\RemoteInterface $remote */
$remote = $this->entity;
+
+ if (!empty($form['auth']['#plugins'])) {
+ $selectedPlugin = $this->getSelectedPlugin($form, $form_state);
+ $remote->mergePluginConfig($selectedPlugin);
+ }
+
$status = $remote->save();
switch ($status) {
@@ -104,4 +142,120 @@ class RemoteForm extends EntityForm {
$form_state->setRedirectUrl($remote->toUrl('collection'));
}
+ /**
+ * Helper function to build the authorization options in the form.
+ *
+ * @param array $form
+ * The form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current form state.
+ *
+ * @throws \Drupal\Component\Plugin\Exception\PluginException
+ */
+ protected function addAuthOptions(array &$form, FormStateInterface $form_state) {
+ $options = [];
+ $plugins = [];
+ if ($this->getAuthPlugin()) {
+ $options[$this->authPlugin->getPluginId()] = $this->authPlugin->getLabel();
+ $plugins[$this->authPlugin->getPluginId()] = $this->authPlugin;
+ }
+ $availablePlugins = $this->authPluginManager->getAvailablePlugins();
+ foreach ($availablePlugins as $id => $plugin) {
+ if (empty($options[$id])) {
+ // This plugin type was not previously set as an option.
+ /** @var \Drupal\entity_share_client\Plugin\ClientAuthorizationInterface $plugin */
+ $options[$id] = $plugin->getLabel();
+ $plugins[$id] = $plugin;
+ }
+ }
+ // Do we have a value?
+ $selected = $form_state->getValue('pid');
+ if (!empty($selected)) {
+ $selectedPlugin = $plugins[$selected];
+ }
+ elseif (!empty($this->authPlugin)) {
+ // Is a plugin previously stored?
+ $selectedPlugin = $this->authPlugin;
+ }
+ else {
+ // Fallback: take the first option.
+ $selectedPlugin = reset($plugins);
+ }
+ $form['auth'] = [
+ '#type' => 'container',
+ '#plugins' => $plugins,
+ 'pid' => [
+ '#type' => 'radios',
+ '#title' => $this->t('Authorization methods'),
+ '#options' => $options,
+ '#default_value' => $selectedPlugin->getPluginId(),
+ '#ajax' => [
+ 'wrapper' => 'plugin-form-ajax-container',
+ 'callback' => [get_class($this), 'ajaxPluginForm'],
+ ],
+ ],
+ 'data' => [],
+ ];
+ $subformState = SubformState::createForSubform($form['auth']['data'], $form, $form_state);
+ $form['auth']['data'] = $selectedPlugin->buildConfigurationForm($form['auth']['data'], $subformState);
+ $form['auth']['data']['#tree'] = TRUE;
+ $form['auth']['data']['#prefix'] = '
';
+ $form['auth']['data']['#suffix'] = '
';
+ }
+
+ /**
+ * Callback function to return the credentials portion of the form.
+ *
+ * @param array $form
+ * The rebuilt form.
+ * @param \Drupal\Core\Form\FormStateInterface $formState
+ * The current form state.
+ *
+ * @return array
+ * A portion of the render array.
+ */
+ public static function ajaxPluginForm(array $form, FormStateInterface $formState) {
+ return $form['auth']['data'];
+ }
+
+ /**
+ * Helper method to instantiate plugin from this entity.
+ *
+ * @return bool
+ * The Remote entity has a plugin.
+ *
+ * @throws \Drupal\Component\Plugin\Exception\PluginException
+ */
+ protected function getAuthPlugin() {
+ /** @var \Drupal\entity_share_client\Entity\RemoteInterface $remote */
+ $remote = $this->entity;
+ $plugin = $remote->getAuthPlugin();
+ if ($plugin instanceof ClientAuthorizationInterface) {
+ $this->authPlugin = $plugin;
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Helper method to get selected plugin from the form.
+ *
+ * @param array $form
+ * The form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current form state.
+ *
+ * @return \Drupal\entity_share_client\Plugin\ClientAuthorizationInterface
+ * The selected plugin.
+ */
+ protected function getSelectedPlugin(
+ array &$form,
+ FormStateInterface $form_state) {
+ $authPluginId = $form_state->getValue('pid');
+ $plugins = $form['auth']['#plugins'];
+ /** @var \Drupal\entity_share_client\Plugin\ClientAuthorizationInterface $selectedPlugin */
+ $selectedPlugin = $plugins[$authPluginId];
+ return $selectedPlugin;
+ }
+
}
diff --git a/modules/entity_share_client/src/Plugin/ClientAuthorization/BasicAuth.php b/modules/entity_share_client/src/Plugin/ClientAuthorization/BasicAuth.php
new file mode 100644
index 0000000..f8bc191
--- /dev/null
+++ b/modules/entity_share_client/src/Plugin/ClientAuthorization/BasicAuth.php
@@ -0,0 +1,95 @@
+keyService->getCredentials($this);
+ $http_client = $this->httpClientFactory->fromOptions([
+ 'base_uri' => $url . '/',
+ 'cookies' => TRUE,
+ 'allow_redirects' => TRUE,
+ ]);
+
+ $http_client->post('/user/login', [
+ 'form_params' => [
+ 'name' => $credentials['username'],
+ 'pass' => $credentials['password'],
+ 'form_id' => 'user_login_form',
+ ],
+ ]);
+
+ return $http_client;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getJsonApiClient($url) {
+ $credentials = $this->keyService->getCredentials($this);
+ return $this->httpClientFactory->fromOptions([
+ 'base_uri' => $url . '/',
+ 'auth' => [
+ $credentials['username'],
+ $credentials['password'],
+ ],
+ 'headers' => [
+ 'Content-type' => 'application/vnd.api+json',
+ ],
+ ]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(
+ array $form,
+ FormStateInterface $form_state
+ ) {
+ $form = parent::buildConfigurationForm($form, $form_state);
+
+ $credentials = $this->keyService->getCredentials($this);
+ $form['entity_share']['username'] = [
+ '#type' => 'textfield',
+ '#required' => FALSE,
+ '#title' => $this->t('Username'),
+ '#default_value' => $credentials['username'] ?? '',
+ ];
+
+ $form['entity_share']['password'] = [
+ '#type' => 'textfield',
+ '#required' => FALSE,
+ '#title' => $this->t('Password'),
+ '#default_value' => $credentials['password'] ?? '',
+ ];
+ if ($this->keyService->additionalProviders()) {
+ $this->expandedProviderOptions($form);
+ $form['key']['id']['#key_filters'] = ['type' => 'entity_share_basic_auth'];
+ $form['key']['id']['#description'] = $this->t('Select the key you have configured to hold the Basic Auth credentials.');
+ }
+ return $form;
+ }
+
+}
diff --git a/modules/entity_share_client/src/Plugin/ClientAuthorization/Oauth.php b/modules/entity_share_client/src/Plugin/ClientAuthorization/Oauth.php
new file mode 100644
index 0000000..bb20472
--- /dev/null
+++ b/modules/entity_share_client/src/Plugin/ClientAuthorization/Oauth.php
@@ -0,0 +1,328 @@
+moduleHandler = $container->get('module_handler');
+ $instance->messenger = $container->get('messenger');
+ $instance->logger = $container->get('logger.channel.entity_share_client');
+ return $instance;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function checkIfAvailable() {
+ /*
+ * The oauth2_client client module currently places client_secret in
+ * code annotations, which doesn't seem secure. But it also has a composer
+ * dependency on League\OAuth2\Client so for the moment we use that package
+ * directly.
+ */
+ return $this->moduleHandler->moduleExists('oauth2_client');
+ }
+
+ /**
+ * Obtains the stored or renewed access token based on expiration state.
+ *
+ * @param string $url
+ * The url to the remote.
+ *
+ * @return string
+ * The token value.
+ *
+ * @throws \Exception
+ */
+ protected function getAccessToken($url) {
+ $configuration = $this->getConfiguration();
+ $accessToken = $this->state->get($configuration['uuid'] . '-oauth');
+ $credentials = $this->keyService->getCredentials($this);
+ if ($accessToken instanceof AccessTokenInterface) {
+ if ($accessToken->hasExpired()) {
+ // Get the oauth client:
+ $oauthClient = new GenericProvider(
+ [
+ 'clientId' => $credentials['client_id'],
+ 'clientSecret' => $credentials['client_secret'],
+ 'urlAuthorize' => $url . $credentials['authorization_path'],
+ 'urlAccessToken' => $url . $credentials['token_path'],
+ 'urlResourceOwnerDetails' => '',
+ ]
+ );
+ // Try to get an access token using the authorization code grant.
+ try {
+ $newAccessToken = $oauthClient->getAccessToken(
+ 'refresh_token',
+ [
+ 'refresh_token' => $accessToken->getRefreshToken(),
+ ]
+ );
+ $this->state->set($configuration['uuid'] . '-oauth', $newAccessToken);
+ }
+ catch (\Exception $e) {
+ $this->logger->critical(
+ 'Entity Share new oauth token request failed with Exception: %exception_type and error: %error.',
+ [
+ '%exception_type' => get_class($e),
+ '%error' => $e->getMessage()
+ ]
+ );
+ throw $e;
+ }
+ return $newAccessToken->getToken();
+ }
+ return $accessToken->getToken();
+ }
+ throw new \UnexpectedValueException('Access Token object not found in storage');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getClient($url) {
+ $token = $this->getAccessToken($url);
+ return $this->httpClientFactory->fromOptions(
+ [
+ 'base_uri' => $url . '/',
+ 'allow_redirects' => TRUE,
+ 'headers' => [
+ 'Authorization' => 'Bearer ' . $token,
+ ],
+ ]
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getJsonApiClient($url) {
+ $token = $this->getAccessToken($url);
+ return $this->httpClientFactory->fromOptions(
+ [
+ 'base_uri' => $url . '/',
+ 'headers' => [
+ 'Content-type' => 'application/vnd.api+json',
+ 'Authorization' => 'Bearer ' . $token,
+ ],
+ ]
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(
+ array $form,
+ FormStateInterface $form_state
+ ) {
+ $form = parent::buildConfigurationForm($form, $form_state);
+ $form['entity_share']['#title'] = $this->t(
+ 'Oauth using Resource owner password credentials grant'
+ );
+ $form['entity_share']['#description'] = $this->t(
+ 'A token will be requested and saved in State storage when this form is submitted. The username and password entered here are not saved, but are only used to request the token.'
+ );
+ $form['entity_share']['username'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Username'),
+ ];
+
+ $form['entity_share']['password'] = [
+ '#type' => 'password',
+ '#title' => $this->t('Password'),
+ ];
+
+ $credentials = $this->keyService->getCredentials($this);
+ $form['entity_share']['client_id'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Client ID'),
+ '#default_value' => $credentials['client_id'] ?? '',
+ ];
+ $form['entity_share']['client_secret'] = [
+ '#type' => 'password',
+ '#title' => $this->t('Client secret'),
+ ];
+ $form['entity_share']['authorization_path'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Authorization path on the remote'),
+ '#default_value' => $credentials['authorization_path'] ?? '/oauth/token',
+ ];
+ $form['entity_share']['token_path'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Token path on the remote'),
+ '#default_value' => $credentials['token_path'] ?? '/oauth/token',
+ ];
+ if ($this->keyService->additionalProviders()) {
+ $this->expandedProviderOptions($form);
+ // Username and password also needed if Key module is involved.
+ $form['key']['#description'] = $this->t(
+ 'A token will be requested and saved in State storage when this form is submitted. The username and password entered here are not saved, but are only used to request the token.'
+ );
+ $form['key']['username'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Username'),
+ ];
+
+ $form['key']['password'] = [
+ '#type' => 'password',
+ '#title' => $this->t('Password'),
+ ];
+ $form['key']['id']['#key_filters'] = ['type' => 'entity_share_oauth'];
+ $form['key']['id']['#description'] = $this->t('Select the key you have configured to hold the Oauth credentials.');
+ }
+ return $form;
+ }
+
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitConfigurationForm(
+ array &$form,
+ FormStateInterface $form_state
+ ) {
+ $values = $form_state->getValues();
+ $configuration = $this->getConfiguration();
+ /** @var \Drupal\entity_share_client\Entity\Remote $remote */
+ $remote = $form_state->get('remote');
+ $resetConfiguration = $configuration;
+ $provider = $values['credential_provider'];
+ $credentials = $values[$provider];
+ $key = $configuration['uuid'];
+ if ($provider == 'key') {
+ $key = $credentials['id'];
+ }
+ $configuration['data'] = [
+ 'credential_provider' => $provider,
+ 'storage_key' => $key,
+ ];
+ $this->setConfiguration($configuration);
+ try {
+ // Try to obtain a token.
+ switch ($provider) {
+ case 'key':
+ $requestCredentials = $this->keyService->getCredentials($this);
+ $requestCredentials['username'] = $credentials['username'];
+ $requestCredentials['password'] = $credentials['password'];
+ $accessToken = $this->initalizeToken($remote, $requestCredentials);
+ $this->state->delete($configuration['uuid']);
+
+ break;
+
+ default:
+ $accessToken = $this->initalizeToken($remote, $credentials);
+ // Remove the username and password.
+ unset($credentials['username']);
+ unset($credentials['password']);
+ $this->state->set($configuration['uuid'], $credentials);
+ }
+
+ // Save the token.
+ $this->state->set($configuration['uuid'] . '-oauth', $accessToken);
+
+ $this->messenger->addStatus(
+ $this->t('OAuth token obtained from remote and stored.')
+ );
+ }
+ catch (IdentityProviderException $e) {
+ // Failed to get the access token.
+ // Reset original configuration.
+ $this->setConfiguration($resetConfiguration);
+ $this->messenger->addError(
+ $this->t(
+ 'Unable to obtain an OAuth token. Error message is: @message',
+ ['@message' => $e->getMessage()]
+ )
+ );
+ }
+ }
+
+ /**
+ * @param \Drupal\entity_share_client\Entity\Remote $remote
+ * The remote for which authorization is needed.
+ * @param array $credentials
+ * Trial credentials.
+ *
+ * @return AccessTokenInterface
+ * A valid access token.
+ *
+ * @throws \League\OAuth2\Client\Provider\Exception\IdentityProviderException
+ */
+ protected function initalizeToken(Remote $remote, array $credentials) {
+ $oauthClient = new GenericProvider(
+ [
+ 'clientId' => $credentials['client_id'],
+ 'clientSecret' => $credentials['client_secret'],
+ 'urlAuthorize' => $remote->get('url') . $credentials['authorization_path'],
+ 'urlAccessToken' => $remote->get('url') . $credentials['token_path'],
+ 'urlResourceOwnerDetails' => '',
+ ]
+ );
+ // Try to get an access token using the
+ // resource owner password credentials grant.
+ return $oauthClient->getAccessToken(
+ 'password',
+ [
+ 'username' => $credentials['username'],
+ 'password' => $credentials['password'],
+ ]
+ );
+ }
+
+}
diff --git a/modules/entity_share_client/src/Plugin/ClientAuthorizationBase.php b/modules/entity_share_client/src/Plugin/ClientAuthorizationBase.php
new file mode 100644
index 0000000..3e51127
--- /dev/null
+++ b/modules/entity_share_client/src/Plugin/ClientAuthorizationBase.php
@@ -0,0 +1,275 @@
+keyService = $keyProvider;
+ $this->state = $state;
+ $this->uuid = $uuid;
+ $this->httpClientFactory = $clientFactory;
+ $this->setConfiguration($configuration);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(
+ ContainerInterface $container,
+ array $configuration,
+ $plugin_id,
+ $plugin_definition
+ ) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('entity_share_client.key_provider'),
+ $container->get('state'),
+ $container->get('uuid'),
+ $container->get('http_client_factory')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ 'uuid' => $this->uuid->generate(),
+ 'verified' => FALSE,
+ 'data' => [],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfiguration() {
+ return $this->configuration;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setConfiguration(array $configuration) {
+ $this->configuration = NestedArray::mergeDeep(
+ $this->defaultConfiguration(),
+ $configuration
+ );
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCredentialProvider() {
+ $configuration = $this->getConfiguration();
+ return $configuration['data']['credential_provider'] ?? NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStorageKey() {
+ $configuration = $this->getConfiguration();
+ return $configuration['data']['storage_key'] ?? NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(
+ array $form,
+ FormStateInterface $form_state
+ ) {
+ return $form + [
+ 'credential_provider' => [
+ '#type' => 'hidden',
+ '#value' => 'entity_share',
+ ],
+ 'entity_share' => [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Stored in local State'),
+ ],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLabel() {
+ return $this->pluginDefinition['label'];
+ }
+
+ /**
+ * Helper method to build the credential provider elements of the form.
+ *
+ * @param array $form
+ * The configuration form.
+ */
+ protected function expandedProviderOptions(array &$form) {
+ $provider = $this->getCredentialProvider();
+ // Provide selectors for the api key credential provider.
+ $form['credential_provider'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Credential provider'),
+ '#default_value' => empty($provider) ? 'entity_share' : $provider,
+ '#options' => [
+ 'entity_share' => $this->t('Local Storage'),
+ 'key' => $this->t('Key Module'),
+ ],
+ '#attributes' => [
+ 'data-states-selector' => 'provider',
+ ],
+ '#weight' => -99,
+ ];
+ $form['entity_share']['#states'] = [
+ 'required' => [
+ ':input[data-states-selector="provider"]' => ['value' => 'entity_share'],
+ ],
+ 'visible' => [
+ ':input[data-states-selector="provider"]' => ['value' => 'entity_share'],
+ ],
+ 'enabled' => [
+ ':input[data-states-selector="provider"]' => ['value' => 'entity_share'],
+ ],
+ ];
+ $key_id = $provider == 'key' ? $this->getStorageKey() : '';
+ $form['key'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Managed by Key module'),
+ '#states' => [
+ 'required' => [
+ ':input[data-states-selector="provider"]' => ['value' => 'key'],
+ ],
+ 'visible' => [
+ ':input[data-states-selector="provider"]' => ['value' => 'key'],
+ ],
+ 'enabled' => [
+ ':input[data-states-selector="provider"]' => ['value' => 'key'],
+ ],
+ ],
+ ];
+ $form['key']['id'] = [
+ '#type' => 'key_select',
+ '#title' => $this->t('Select a Stored Key'),
+ '#default_value' => $key_id,
+ '#empty_option' => $this->t('- Please select -'),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateConfigurationForm(
+ array &$form,
+ FormStateInterface $form_state
+ ) {
+ $values = $form_state->getValues();
+ if (empty($values['credential_provider'])) {
+ $form_state->setError(
+ $form['credential_provider'],
+ 'A credential provider is required.'
+ );
+ }
+ else {
+ $provider = $values['credential_provider'];
+ foreach ($values[$provider] as $key => $value) {
+ if (empty($value)) {
+ $form_state->setError(
+ $form[$provider][$key],
+ 'All credential values are required.'
+ );
+ }
+ }
+ }
+
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitConfigurationForm(
+ array &$form,
+ FormStateInterface $form_state
+ ) {
+ $key = NULL;
+ $values = $form_state->getValues();
+ $configuration = $this->getConfiguration();
+ $provider = $values['credential_provider'];
+ $credentials = $values[$provider];
+ switch ($provider) {
+ case 'entity_share':
+ $this->state->set($configuration['uuid'], $credentials);
+ $key = $configuration['uuid'];
+ break;
+
+ case 'key':
+ $this->state->delete($configuration['uuid']);
+ $key = $credentials['id'];
+ break;
+
+ }
+ $configuration['data'] = [
+ 'credential_provider' => $provider,
+ 'storage_key' => $key,
+ ];
+ $this->setConfiguration($configuration);
+ }
+
+}
diff --git a/modules/entity_share_client/src/Plugin/ClientAuthorizationInterface.php b/modules/entity_share_client/src/Plugin/ClientAuthorizationInterface.php
new file mode 100644
index 0000000..4c7ed5e
--- /dev/null
+++ b/modules/entity_share_client/src/Plugin/ClientAuthorizationInterface.php
@@ -0,0 +1,70 @@
+alterInfo('entity_share_client_entity_share_auth_info');
+ $this->setCacheBackend($cache_backend, 'entity_share_client_entity_share_auth_plugins');
+ }
+
+ /**
+ * Builds an array of currently available plugin instances.
+ *
+ * @return \Drupal\entity_share_client\Plugin\ClientAuthorizationInterface[]
+ * The array of plugins.
+ *
+ * @throws \Drupal\Component\Plugin\Exception\PluginException
+ */
+ public function getAvailablePlugins() {
+ $plugins = [];
+ $definitions = $this->getDefinitions();
+ foreach ($definitions as $definition) {
+ /** @var \Drupal\entity_share_client\Plugin\ClientAuthorizationInterface $plugin */
+ $plugin = $this->createInstance($definition['id']);
+ if ($plugin->checkIfAvailable()) {
+ $plugins[$plugin->getPluginId()] = $plugin;
+ }
+ }
+ return $plugins;
+ }
+
+}
diff --git a/modules/entity_share_client/src/Plugin/KeyType/EntityShareBasicAuth.php b/modules/entity_share_client/src/Plugin/KeyType/EntityShareBasicAuth.php
new file mode 100644
index 0000000..02017e4
--- /dev/null
+++ b/modules/entity_share_client/src/Plugin/KeyType/EntityShareBasicAuth.php
@@ -0,0 +1,29 @@
+{
"username": "username value",
"password": "password value"
}
"),
+ * group = "authentication",
+ * key_value = {
+ * "plugin" = "textarea_field"
+ * },
+ * multivalue = {
+ * "enabled" = true,
+ * "fields" = {
+ * "username" = @Translation("Username"),
+ * "password" = @Translation("Password")
+ * }
+ * }
+ * )
+ */
+class EntityShareBasicAuth extends AuthenticationMultivalueKeyType {
+
+}
diff --git a/modules/entity_share_client/src/Plugin/KeyType/EntityShareOauth.php b/modules/entity_share_client/src/Plugin/KeyType/EntityShareOauth.php
new file mode 100644
index 0000000..8b08670
--- /dev/null
+++ b/modules/entity_share_client/src/Plugin/KeyType/EntityShareOauth.php
@@ -0,0 +1,31 @@
+{
"client_id": "client_id value",
"client_secret": "client_secret value"
,
"authorization_path": "authorization_path value"
,
"token_path": "token_path value"
}
"),
+ * group = "authentication",
+ * key_value = {
+ * "plugin" = "textarea_field"
+ * },
+ * multivalue = {
+ * "enabled" = true,
+ * "fields" = {
+ * "client_id" = @Translation("Client ID"),
+ * "client_secret" = @Translation("Client Secret"),
+ * "authorization_path" = @Translation("Authorization Path"),
+ * "token_path" = @Translation("Token Path")
+ * }
+ * }
+ * )
+ */
+class EntityShareOauth extends AuthenticationMultivalueKeyType {
+
+}
diff --git a/modules/entity_share_client/src/Service/KeyProvider.php b/modules/entity_share_client/src/Service/KeyProvider.php
new file mode 100644
index 0000000..b0bf7c5
--- /dev/null
+++ b/modules/entity_share_client/src/Service/KeyProvider.php
@@ -0,0 +1,94 @@
+state = $state;
+ }
+
+ /**
+ * Provides a means to our services.yml file to conditionally inject service.
+ *
+ * @param \Drupal\key\KeyRepositoryInterface $repository
+ * The injected service, if it exists.
+ *
+ * @see maw_luminate.services.yml
+ */
+ public function setKeyRepository(KeyRepositoryInterface $repository) {
+ $this->keyRepository = $repository;
+ }
+
+ /**
+ * Detects if key module service was injected.
+ *
+ * @return bool
+ * True if the KeyRepository is present.
+ */
+ public function additionalProviders() {
+ return $this->keyRepository instanceof KeyRepositoryInterface;
+ }
+
+ /**
+ * Get the provided credentials.
+ *
+ * @param \Drupal\entity_share_client\Plugin\ClientAuthorizationInterface $plugin
+ * An authorization plugin.
+ *
+ * @return array|string
+ * The value of the configured key.
+ */
+ public function getCredentials(ClientAuthorizationInterface $plugin) {
+ $provider = $plugin->getCredentialProvider();
+ $credentials = [];
+ if (empty($provider)) {
+ return $credentials;
+ }
+ switch ($provider) {
+ case 'key':
+ $keyEntity = $this->keyRepository->getKey($plugin->getStorageKey());
+ if ($keyEntity instanceof Key) {
+ // A key was found in the repository.
+ $credentials = $keyEntity->getKeyValues();
+ }
+ break;
+
+ default:
+ $credentials = $this->state->get($plugin->getStorageKey());
+ }
+
+ return $credentials;
+ }
+
+}
diff --git a/modules/entity_share_client/src/Service/RemoteManager.php b/modules/entity_share_client/src/Service/RemoteManager.php
index 405b7aa..74a445e 100644
--- a/modules/entity_share_client/src/Service/RemoteManager.php
+++ b/modules/entity_share_client/src/Service/RemoteManager.php
@@ -5,7 +5,6 @@ declare(strict_types = 1);
namespace Drupal\entity_share_client\Service;
use Drupal\Component\Serialization\Json;
-use Drupal\Core\Http\ClientFactory;
use Drupal\entity_share_client\Entity\RemoteInterface;
/**
@@ -16,11 +15,18 @@ use Drupal\entity_share_client\Entity\RemoteInterface;
class RemoteManager implements RemoteManagerInterface {
/**
- * The HTTP client factory.
+ * A constant to document the call for a standard client.
*
- * @var \Drupal\Core\Http\ClientFactory
+ * @var bool
*/
- protected $httpClientFactory;
+ const STANDARD_CLIENT = FALSE;
+
+ /**
+ * A constant to document the call for a JSON:API client.
+ *
+ * @var bool
+ */
+ const JSON_API_CLIENT = TRUE;
/**
* The request service.
@@ -32,16 +38,12 @@ class RemoteManager implements RemoteManagerInterface {
/**
* RemoteManager constructor.
*
- * @param \Drupal\Core\Http\ClientFactory $http_client_factory
- * The HTTP client factory.
* @param \Drupal\entity_share_client\Service\RequestServiceInterface $request_service
* The request service.
*/
public function __construct(
- ClientFactory $http_client_factory,
RequestServiceInterface $request_service
) {
- $this->httpClientFactory = $http_client_factory;
$this->requestService = $request_service;
}
@@ -49,37 +51,14 @@ class RemoteManager implements RemoteManagerInterface {
* {@inheritdoc}
*/
public function prepareClient(RemoteInterface $remote) {
- $http_client = $this->httpClientFactory->fromOptions([
- 'base_uri' => $remote->get('url') . '/',
- 'cookies' => TRUE,
- 'allow_redirects' => TRUE,
- ]);
-
- $http_client->post('user/login', [
- 'form_params' => [
- 'name' => $remote->get('basic_auth_username'),
- 'pass' => $remote->get('basic_auth_password'),
- 'form_id' => 'user_login_form',
- ],
- ]);
-
- return $http_client;
+ return $remote->getHttpClient(self::STANDARD_CLIENT);
}
/**
* {@inheritdoc}
*/
public function prepareJsonApiClient(RemoteInterface $remote) {
- return $this->httpClientFactory->fromOptions([
- 'base_uri' => $remote->get('url') . '/',
- 'auth' => [
- $remote->get('basic_auth_username'),
- $remote->get('basic_auth_password'),
- ],
- 'headers' => [
- 'Content-type' => 'application/vnd.api+json',
- ],
- ]);
+ return $remote->getHttpClient(self::JSON_API_CLIENT);
}
/**
@@ -88,10 +67,12 @@ class RemoteManager implements RemoteManagerInterface {
public function getChannelsInfos(RemoteInterface $remote) {
$http_client = $this->prepareJsonApiClient($remote);
- $response = $this->requestService->request($http_client, 'GET', 'entity_share');
- $json = Json::decode((string) $response->getBody());
+ if ($response = $this->requestService->request($http_client, 'GET', 'entity_share')) {
+ $json = Json::decode((string) $response->getBody());
- return $json['data']['channels'];
+ return $json['data']['channels'];
+ }
+ return [];
}
}