View source
<?php
namespace Drupal\apigee_edge;
use Apigee\Edge\Exception\ApiRequestException;
use Apigee\Edge\Exception\ApigeeOnGcpOauth2AuthenticationException;
use Apigee\Edge\Exception\OauthAuthenticationException;
use Apigee\Edge\HttpClient\Plugin\Authentication\Oauth;
use Drupal\apigee_edge\Exception\AuthenticationKeyException;
use Drupal\apigee_edge\Exception\InvalidArgumentException;
use Drupal\apigee_edge\Exception\KeyProviderRequirementsException;
use Drupal\apigee_edge\Plugin\EdgeKeyTypeInterface;
use Drupal\apigee_edge\Plugin\KeyProviderRequirementsInterface;
use Drupal\apigee_edge\Plugin\KeyType\ApigeeAuthKeyType;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Component\Utility\EmailValidatorInterface;
use Drupal\Component\Utility\Random;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\Render\Element\StatusMessages;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\key\Form\KeyFormBase;
use Drupal\key\KeyInterface;
use Drupal\key\Plugin\KeyProviderSettableValueInterface;
use GuzzleHttp\Exception\ConnectException;
use Http\Client\Exception\NetworkException;
final class KeyEntityFormEnhancer {
use MessengerTrait;
use StringTranslationTrait;
use DependencySerializationTrait;
private $connector;
private $entityTypeManager;
private $oauthTokenStorage;
private $configFactory;
private $emailValidator;
public function __construct(SDKConnectorInterface $connector, OauthTokenStorageInterface $oauth_token_storage, EntityTypeManagerInterface $entity_type_manager, ConfigFactoryInterface $config_factory, EmailValidatorInterface $email_validator) {
$this->connector = $connector;
$this->entityTypeManager = $entity_type_manager;
$this->oauthTokenStorage = $oauth_token_storage;
$this->configFactory = $config_factory;
$this->emailValidator = $email_validator;
}
public function alterForm(array &$form, FormStateInterface $form_state) : void {
if (!$form_state
->getFormObject() instanceof KeyFormBase) {
return;
}
$key = $form_state
->getFormObject()
->getEntity();
if (!$key
->isNew() && isset($form['confirm_edit'])) {
return;
}
$form['#prefix'] = '<div id="apigee-edge-key-form-enhancer">';
$form['#suffix'] = '</div>';
$form['#validate'][] = [
$this,
'validateForm',
];
if ($this
->isApigeeKeyTypeAuthForm($form_state)) {
$key_provider = $key
->getKeyProvider();
if ($key_provider instanceof KeyProviderRequirementsInterface) {
try {
$key_provider
->checkRequirements($key);
} catch (KeyProviderRequirementsException $exception) {
$form['settings']['provider_section']['key_provider_error'] = [
'#theme' => 'status_messages',
'#message_list' => [
'error' => [
$this
->t('The requirements of the selected %key_provider key provider are not fulfilled. Fix errors described below or change the key provider.', [
'%key_provider' => $key_provider
->getPluginDefinition()['label'],
]),
$exception
->getTranslatableMarkupMessage(),
],
],
'#weight' => -100,
];
}
}
$form['settings']['messages'] = [
'#theme' => 'status_messages',
'#message_list' => [],
'#weight' => -100,
];
$form['settings']['debug_placeholder'] = [
'#type' => 'html_tag',
'#tag' => 'div',
'#attributes' => [
'id' => 'apigee-edge-auth-form-debug-info',
],
];
$form['settings']['debug'] = [
'#type' => 'details',
'#title' => $this
->t('Debug information'),
'#access' => FALSE,
'#open' => FALSE,
'#theme_wrappers' => [
'details' => [],
'container' => [
'#attributes' => [
'id' => 'apigee-edge-auth-form-debug-info',
],
],
],
];
$form['settings']['debug']['debug_text'] = [
'#type' => 'textarea',
'#disabled' => TRUE,
'#rows' => 20,
];
$form['settings']['test_connection'] = [
'#type' => 'details',
'#title' => $this
->t('Test connection'),
'#description' => $this
->t('Send request using the given API credentials.'),
'#open' => TRUE,
'#theme_wrappers' => [
'details' => [],
'container' => [
'#attributes' => [
'id' => 'apigee-edge-connection-info',
],
],
],
];
if (!$this
->keyIsWritable($key)) {
if ($key
->isNew()) {
$form['settings']['test_connection']['#description'] = $this
->t('Send request using the stored credentials in the key provider');
}
else {
$form['settings']['test_connection']['#description'] = $this
->t("Send request using the <a href=':key_config_uri' target='_blank'>active authentication key</a>.", [
':key_config_uri' => $key
->toUrl()
->toString(),
]);
}
}
$form['settings']['test_connection']['test_connection_submit'] = [
'#type' => 'submit',
'#executes_submit_callback' => FALSE,
'#value' => $this
->t('Send request'),
'#name' => 'test_connection',
'#ajax' => [
'callback' => [
$this,
'testConnectionAjaxCallback',
],
'wrapper' => 'apigee-edge-key-form-enhancer',
'progress' => [
'type' => 'throbber',
'message' => $this
->t('Waiting for response...'),
],
],
'#states' => [
'enabled' => [
[
':input[name="key_input_settings[organization]"]' => [
'empty' => FALSE,
],
':input[name="key_input_settings[password]"]' => [
'empty' => FALSE,
],
':input[name="key_input_settings[username]"]' => [
'empty' => FALSE,
],
],
[
':input[name="key_input_settings[instance_type]"]' => [
'value' => EdgeKeyTypeInterface::INSTANCE_TYPE_HYBRID,
],
':input[name="key_input_settings[organization]"]' => [
'empty' => FALSE,
],
':input[name="key_input_settings[account_json_key]"]' => [
'empty' => FALSE,
],
],
],
],
];
}
}
public function validateForm(array &$form, FormStateInterface $form_state) : void {
if (!$this
->isApigeeKeyTypeAuthForm($form_state)) {
return;
}
if (!in_array($form_state
->getTriggeringElement()['#name'] ?? [], [
'test_connection',
'op',
])) {
return;
}
if (!empty($form_state
->getErrors())) {
return;
}
$key = $form_state
->getFormObject()
->getEntity();
if ($this
->keyIsWritable($key)) {
if (isset($form_state
->getStorage()['key_value']['processed_submitted'])) {
$key_value = $form_state
->getStorage()['key_value']['processed_submitted'];
}
else {
$key_input_plugin_form_state = clone $form_state;
$key_input_plugin_form_state
->setValues($form_state
->getValue('key_input_settings', []));
$key_input_processed_values = $key
->getKeyInput()
->processSubmittedKeyValue($key_input_plugin_form_state);
$key_value = $key_input_processed_values['processed_submitted'];
$key_value_from_storage =& $form_state
->get('key_value');
unset($key_value_from_storage['original'], $key_value_from_storage['processed_original']);
}
$random = new Random();
$test_key = $this->entityTypeManager
->getStorage('key')
->create([
'id' => strtolower($random
->name(16)),
'key_type' => $key
->getKeyType()
->getPluginID(),
'key_input' => $key
->getKeyInput()
->getPluginID(),
'key_provider' => 'config',
'key_provider_settings' => [
'key_value' => $key_value,
],
]);
}
else {
$test_key = clone $key;
}
$test_key_type = $test_key
->getKeyType();
$test_auth_type = $test_key_type
->getAuthenticationType($test_key);
try {
if (in_array($test_auth_type, [
EdgeKeyTypeInterface::EDGE_AUTH_TYPE_OAUTH,
EdgeKeyTypeInterface::EDGE_AUTH_TYPE_JWT,
])) {
$this->oauthTokenStorage
->checkRequirements();
$this
->cleanUpOauthTokenData();
}
$this->connector
->testConnection($test_key);
$this
->messenger()
->addStatus($this
->t('Connection successful.'));
drupal_flush_all_caches();
} catch (\Exception $exception) {
watchdog_exception('apigee_edge', $exception);
$form_state
->setError($form, $this
->t('@suggestion Error message: %response', [
'@suggestion' => $this
->createSuggestion($exception, $test_key),
'%response' => $exception
->getMessage(),
]));
$form['settings']['debug']['#access'] = $form['settings']['debug']['debug_text']['#access'] = TRUE;
$form['settings']['debug']['debug_text']['#value'] = $this
->createDebugText($exception, $test_key);
$is_provider_env = $test_key && $test_key
->getKeyProvider()
->getPluginId() === 'env';
$is_provider_file = $test_key && $test_key
->getKeyProvider()
->getPluginId() === 'file';
if ($test_key_type
->getInstanceType($test_key) != EdgeKeyTypeInterface::INSTANCE_TYPE_HYBRID && !$is_provider_env && !$is_provider_file) {
$form['settings']['input_section']['key_input_settings']['password']['#attributes']['value'] = $test_key_type
->getPassword($test_key);
}
} finally {
$this
->cleanUpOauthTokenData();
}
}
private function cleanUpOauthTokenData() : void {
if ($this->oauthTokenStorage instanceof OauthTokenFileStorage) {
$this->oauthTokenStorage
->removeTokenFile();
}
else {
$this->oauthTokenStorage
->removeToken();
}
}
private function isApigeeKeyTypeAuthForm(FormStateInterface $form_state) : bool {
if (!$form_state
->getFormObject() instanceof KeyFormBase) {
FALSE;
}
$key = $form_state
->getFormObject()
->getEntity();
$key_type_from_user_input = $form_state
->getUserInput()['key_type'] ?? '';
return $key
->getKeyType() instanceof ApigeeAuthKeyType || $key_type_from_user_input === 'apigee_auth';
}
public static function testConnectionAjaxCallback(array $form, FormStateInterface $form_state) : array {
$original_weight = $form['settings']['messages']['#weight'] ?? 0;
$form['settings']['messages'] = StatusMessages::renderMessages();
$form['settings']['messages']['#weight'] = $original_weight;
return $form;
}
private function keyIsWritable(KeyInterface $key) : bool {
return $key
->getKeyProvider() instanceof KeyProviderSettableValueInterface;
}
private function createSuggestion(\Exception $exception, KeyInterface $key) : MarkupInterface {
$fail_text = $this
->t('Failed to connect to Apigee Edge.');
$suggestion = $this
->t('@fail_text', [
'@fail_text' => $fail_text,
]);
$key_type = $key
->getKeyType();
if ($exception instanceof AuthenticationKeyException) {
$suggestion = $this
->t('@fail_text Verify the Apigee Edge connection settings.', [
'@fail_text' => $fail_text,
]);
}
elseif ($exception instanceof ApigeeOnGcpOauth2AuthenticationException) {
$fail_text = $this
->t('Failed to connect to the authorization server.');
$suggestion = $this
->t('@fail_text Check the debug information below for more details.', [
'@fail_text' => $fail_text,
]);
if ($exception
->getPrevious() && $exception
->getPrevious() instanceof \DomainException) {
$suggestion = $this
->t('@fail_text The private key in the GCP service account key JSON is invalid.', [
'@fail_text' => $fail_text,
]);
}
}
elseif ($exception instanceof OauthAuthenticationException) {
$fail_text = $this
->t('Failed to connect to the OAuth authorization server.');
$suggestion = $this
->t('@fail_text Check the debug information below for more details.', [
'@fail_text' => $fail_text,
]);
if ($exception
->getCode() === 401) {
if ($key_type
->getClientId($key) !== Oauth::DEFAULT_CLIENT_ID || $key_type
->getClientSecret($key) !== Oauth::DEFAULT_CLIENT_SECRET) {
$suggestion = $this
->t('@fail_text The given username (%username) or password or client ID (%client_id) or client secret is incorrect.', [
'@fail_text' => $fail_text,
'%client_id' => $key_type
->getClientId($key),
'%username' => $key_type
->getUsername($key),
]);
}
else {
$suggestion = $this
->t('@fail_text The given username (%username) or password is incorrect.', [
'@fail_text' => $fail_text,
'%username' => $key_type
->getUsername($key),
]);
}
}
elseif ($exception
->getCode() === 0) {
if ($exception
->getPrevious() instanceof ApiRequestException && $exception
->getPrevious()
->getPrevious() instanceof NetworkException && $exception
->getPrevious()
->getPrevious()
->getPrevious() instanceof ConnectException) {
$curl_exception = $exception
->getPrevious()
->getPrevious()
->getPrevious();
if ($curl_exception
->getHandlerContext()['errno'] === CURLE_OPERATION_TIMEDOUT) {
$suggestion = $this
->t('@fail_text The connection timeout threshold (%connect_timeout) or the request timeout (%timeout) is too low or something is wrong with the connection.', [
'@fail_text' => $fail_text,
'%connect_timeout' => $this
->config('apigee_edge.client')
->get('http_client_connect_timeout'),
'%timeout' => $this
->config('apigee_edge.client')
->get('http_client_timeout'),
]);
}
if ($curl_exception
->getHandlerContext()['errno'] === CURLE_COULDNT_RESOLVE_HOST) {
$suggestion = $this
->t('@fail_text The given authorization server (%authorization_server) is incorrect or something is wrong with the connection.', [
'@fail_text' => $fail_text,
'%authorization_server' => $key_type
->getAuthorizationServer($key),
]);
}
}
}
}
else {
if ($exception
->getCode() === 401 || $exception
->getCode() === 500 && $exception
->getEdgeErrorCode() === 'usersandroles.SsoInternalServerError') {
if ($key_type
->getInstanceType($key) === EdgeKeyTypeInterface::INSTANCE_TYPE_PUBLIC && !$this->emailValidator
->isValid($key_type
->getUsername($key))) {
$suggestion = $this
->t('@fail_text The organization username should be a valid email.', [
'@fail_text' => $fail_text,
]);
}
else {
$suggestion = $this
->t('@fail_text The given username (%username) or password is incorrect.', [
'@fail_text' => $fail_text,
'%username' => $key_type
->getUsername($key),
]);
}
}
elseif ($exception
->getCode() === 404) {
$suggestion = $this
->t('@fail_text The given organization name (%organization) is incorrect.', [
'@fail_text' => $fail_text,
'%organization' => $key_type
->getOrganization($key),
]);
}
elseif ($exception
->getCode() === 0) {
if ($exception
->getPrevious() instanceof NetworkException && $exception
->getPrevious()
->getPrevious() instanceof ConnectException) {
$curl_exception = $exception
->getPrevious()
->getPrevious();
if ($curl_exception
->getHandlerContext()['errno'] === CURLE_OPERATION_TIMEDOUT) {
$suggestion = $this
->t('@fail_text The connection timeout threshold (%connect_timeout) or the request timeout (%timeout) is too low or something is wrong with the connection.', [
'@fail_text' => $fail_text,
'%connect_timeout' => $this
->config('apigee_edge.client')
->get('http_client_connect_timeout'),
'%timeout' => $this
->config('apigee_edge.client')
->get('http_client_timeout'),
]);
}
elseif ($curl_exception
->getHandlerContext()['errno'] === CURLE_COULDNT_RESOLVE_HOST) {
$suggestion = $this
->t('@fail_text The given endpoint (%endpoint) is incorrect or something is wrong with the connection.', [
'@fail_text' => $fail_text,
'%endpoint' => $key_type
->getEndpoint($key),
]);
}
}
elseif ($exception instanceof InvalidArgumentException) {
$suggestion = $this
->t('@fail_text The given endpoint (%endpoint) is incorrect or something is wrong with the connection.', [
'@fail_text' => $fail_text,
'%endpoint' => $key_type
->getEndpoint($key),
]);
}
}
}
return $suggestion;
}
private function createDebugText(\Exception $exception, KeyInterface $key) : string {
$key_type = $key
->getKeyType();
$credentials = [];
$keys = [
'auth_type' => $key_type instanceof EdgeKeyTypeInterface ? $key_type
->getAuthenticationType($key) : 'invalid credentials',
'key_provider' => get_class($key
->getKeyProvider()),
];
if ($key_type instanceof EdgeKeyTypeInterface) {
$credentials = [
'endpoint' => $key_type
->getEndpoint($key),
'organization' => $key_type
->getOrganization($key),
];
if ($key_type
->getInstanceType($key) != EdgeKeyTypeInterface::INSTANCE_TYPE_HYBRID) {
$credentials['username'] = $key_type
->getUsername($key);
}
if ($key_type
->getAuthenticationType($key) === EdgeKeyTypeInterface::EDGE_AUTH_TYPE_OAUTH) {
$credentials['authorization_server'] = $key_type
->getAuthorizationServer($key);
$credentials['client_id'] = $key_type
->getClientId($key);
$credentials['client_secret'] = $key_type
->getClientSecret($key) === Oauth::DEFAULT_CLIENT_SECRET ? Oauth::DEFAULT_CLIENT_SECRET : '***client-secret***';
}
}
$exception_text = preg_replace([
'/(.*refresh_token=)([^\\&\\r\\n]+)(.*)/',
'/(.*mfa_token=)([^\\&\\r\\n]+)(.*)/',
'/(.*password=)([^\\&\\r\\n]+)(.*)/',
'/(Authorization: (Basic|Bearer) ).*/',
], [
'$1***refresh-token***$3',
'$1***mfa-token***$3',
'$1***password***$3',
'$1***credentials***',
], (string) $exception);
$client_config = array_filter($this->configFactory
->get('apigee_edge.client')
->get(), static function ($key) {
return !is_string($key) || $key[0] !== '_';
}, ARRAY_FILTER_USE_KEY);
return json_encode($credentials, JSON_PRETTY_PRINT) . PHP_EOL . json_encode($keys, JSON_PRETTY_PRINT) . PHP_EOL . json_encode($client_config, JSON_PRETTY_PRINT) . PHP_EOL . $exception_text;
}
}