From d80074865318bb0e70884b951b846480c93646a5 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 29 Sep 2021 16:46:28 -0300 Subject: [PATCH] Event-based STOMP header generation (#839) * More flexible headers for STOMP messages, theoretically. * Lower the priority of the base implementation. * More context. * Add in the "aud" claim. ... ensure we're dealing with our tokens. * Rectify the other base class. * Remove some chaff intro'd from merge conflicts. * Return after failing to find the "aud" claim. * Permissively allow without the "aud" claim... ... _could_ roll more conditionally, with some state set during an update hook; however, seems like unnecessary complexity. * Couple of coding standards things. * Add the use of the class back in. --- islandora.services.yml | 5 + src/Event/StompHeaderEvent.php | 97 +++++++++++++++++++ src/Event/StompHeaderEventException.php | 8 ++ src/Event/StompHeaderEventInterface.php | 56 +++++++++++ src/EventGenerator/EmitEvent.php | 51 +++++----- src/EventSubscriber/JwtEventSubscriber.php | 15 +++ .../StompHeaderEventSubscriber.php | 63 ++++++++++++ .../Action/AbstractGenerateDerivativeBase.php | 18 ++-- src/Plugin/Action/EmitFileEvent.php | 73 ++------------ src/Plugin/Action/EmitMediaEvent.php | 80 +++------------ 10 files changed, 299 insertions(+), 167 deletions(-) create mode 100644 src/Event/StompHeaderEvent.php create mode 100644 src/Event/StompHeaderEventException.php create mode 100644 src/Event/StompHeaderEventInterface.php create mode 100644 src/EventSubscriber/StompHeaderEventSubscriber.php diff --git a/islandora.services.yml b/islandora.services.yml index 39ebbab0..4b3a9d16 100644 --- a/islandora.services.yml +++ b/islandora.services.yml @@ -54,3 +54,8 @@ services: arguments: ['@entity_type.manager', '@entity_field.manager', '@context.manager', '@flysystem_factory', '@language_manager'] islandora.entity_mapper: class: Islandora\Crayfish\Commons\EntityMapper\EntityMapper + islandora.stomp.auth_header_listener: + class: Drupal\islandora\EventSubscriber\StompHeaderEventSubscriber + arguments: ['@jwt.authentication.jwt'] + tags: + - { name: event_subscriber } diff --git a/src/Event/StompHeaderEvent.php b/src/Event/StompHeaderEvent.php new file mode 100644 index 00000000..d6d93c22 --- /dev/null +++ b/src/Event/StompHeaderEvent.php @@ -0,0 +1,97 @@ +entity = $entity; + $this->user = $user; + $this->data = $data; + $this->configuration = $configuration; + $this->headers = new ParameterBag(); + } + + /** + * {@inheritdoc} + */ + public function getEntity() { + return $this->entity; + } + + /** + * {@inheritdoc} + */ + public function getUser() { + return $this->user; + } + + /** + * {@inheritdoc} + */ + public function getData() { + return $this->data; + } + + /** + * {@inheritdoc} + */ + public function getHeaders() { + return $this->headers; + } + + /** + * {@inheritdoc} + */ + public function getConfiguration() { + return $this->configuration; + } + +} diff --git a/src/Event/StompHeaderEventException.php b/src/Event/StompHeaderEventException.php new file mode 100644 index 00000000..c9ee3878 --- /dev/null +++ b/src/Event/StompHeaderEventException.php @@ -0,0 +1,8 @@ +account = $account; $this->entityTypeManager = $entity_type_manager; $this->eventGenerator = $event_generator; $this->stomp = $stomp; - $this->auth = $auth; $this->messenger = $messenger; + $this->eventDispatcher = $event_dispatcher; } /** @@ -119,8 +121,8 @@ abstract class EmitEvent extends ConfigurableActionBase implements ContainerFact $container->get('entity_type.manager'), $container->get('islandora.eventgenerator'), $container->get('islandora.stomp'), - $container->get('jwt.authentication.jwt'), - $container->get('messenger') + $container->get('messenger'), + $container->get('event_dispatcher') ); } @@ -128,29 +130,26 @@ abstract class EmitEvent extends ConfigurableActionBase implements ContainerFact * {@inheritdoc} */ public function execute($entity = NULL) { - - // Include a token for later authentication in the message. - $token = $this->auth->generateToken(); - if (empty($token)) { - // JWT isn't properly configured. Log and notify user. - \Drupal::logger('islandora')->error( - $this->t('Error getting JWT token for message. Check JWT Configuration.') - ); - $this->messenger->addMessage( - $this->t('Error getting JWT token for message. Check JWT Configuration.'), 'error' - ); - return; - } - // Generate event as stomp message. try { $user = $this->entityTypeManager->getStorage('user')->load($this->account->id()); $data = $this->generateData($entity); + + $event = $this->eventDispatcher->dispatch( + StompHeaderEvent::EVENT_NAME, + new StompHeaderEvent($entity, $user, $data, $this->getConfiguration()) + ); + $message = new Message( $this->eventGenerator->generateEvent($entity, $user, $data), - ['Authorization' => "Bearer $token"] + $event->getHeaders()->all() ); } + catch (StompHeaderEventException $e) { + \Drupal::logger('islandora')->error($e->getMessage()); + $this->messenger->addMessage($e->getMessage(), 'error'); + return; + } catch (\RuntimeException $e) { // Notify the user the event couldn't be generated and abort. \Drupal::logger('islandora')->error( diff --git a/src/EventSubscriber/JwtEventSubscriber.php b/src/EventSubscriber/JwtEventSubscriber.php index 438bfb4b..153187f6 100644 --- a/src/EventSubscriber/JwtEventSubscriber.php +++ b/src/EventSubscriber/JwtEventSubscriber.php @@ -19,6 +19,8 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; */ class JwtEventSubscriber implements EventSubscriberInterface { + const AUDIENCE = 'islandora'; + /** * User storage to load users. * @@ -100,6 +102,7 @@ class JwtEventSubscriber implements EventSubscriberInterface { $event->addClaim('sub', $this->currentUser->getAccountName()); $event->addClaim('roles', $this->currentUser->getRoles(FALSE)); + $event->addClaim('aud', [static::AUDIENCE]); } /** @@ -111,6 +114,18 @@ class JwtEventSubscriber implements EventSubscriberInterface { public function validate(JwtAuthValidateEvent $event) { $token = $event->getToken(); + $aud = $token->getClaim('aud'); + + if (!$aud) { + // Deprecation cycle: Avoid invalidating if there's no "aud" claim, to + // allow tokens in flight before the introduction of this claim to remain + // valid. + } + elseif (!in_array(static::AUDIENCE, $aud, TRUE)) { + $event->invalidate('Missing audience entry.'); + return; + } + $uid = $token->getClaim('webid'); $name = $token->getClaim('sub'); $roles = $token->getClaim('roles'); diff --git a/src/EventSubscriber/StompHeaderEventSubscriber.php b/src/EventSubscriber/StompHeaderEventSubscriber.php new file mode 100644 index 00000000..47792efd --- /dev/null +++ b/src/EventSubscriber/StompHeaderEventSubscriber.php @@ -0,0 +1,63 @@ +auth = $auth; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + return [ + StompHeaderEventInterface::EVENT_NAME => ['baseAuth', -100], + ]; + } + + /** + * Event callback; generate and add base authorization header if none is set. + */ + public function baseAuth(StompHeaderEventInterface $stomp_event) { + $headers = $stomp_event->getHeaders(); + if (!$headers->has('Authorization')) { + $token = $this->auth->generateToken(); + if (empty($token)) { + // JWT does not seem to be properly configured. + // phpcs:ignore DrupalPractice.General.ExceptionT.ExceptionT + throw new StompHeaderEventException($this->t('Error getting JWT token for message. Check JWT Configuration.')); + } + else { + $headers->set('Authorization', "Bearer $token"); + } + } + + } + +} diff --git a/src/Plugin/Action/AbstractGenerateDerivativeBase.php b/src/Plugin/Action/AbstractGenerateDerivativeBase.php index 8c83c2c7..3c7ff5b6 100644 --- a/src/Plugin/Action/AbstractGenerateDerivativeBase.php +++ b/src/Plugin/Action/AbstractGenerateDerivativeBase.php @@ -11,10 +11,10 @@ use Drupal\islandora\IslandoraUtils; use Drupal\islandora\EventGenerator\EmitEvent; use Drupal\islandora\EventGenerator\EventGeneratorInterface; use Drupal\islandora\MediaSource\MediaSourceService; -use Drupal\jwt\Authentication\Provider\JwtAuth; use Drupal\token\TokenInterface; use Stomp\StatefulStomp; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * A base class for constructor/creator derivative generators. @@ -80,8 +80,6 @@ class AbstractGenerateDerivativeBase extends EmitEvent { * EventGenerator service to serialize AS2 events. * @param \Stomp\StatefulStomp $stomp * Stomp client. - * @param \Drupal\jwt\Authentication\Provider\JwtAuth $auth - * JWT Auth client. * @param \Drupal\islandora\IslandoraUtils $utils * Islandora utility functions. * @param \Drupal\islandora\MediaSource\MediaSourceService $media_source @@ -94,6 +92,8 @@ class AbstractGenerateDerivativeBase extends EmitEvent { * The system file config. * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager * Field Manager service. + * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher + * Event dispatcher service. */ public function __construct( array $configuration, @@ -103,13 +103,13 @@ class AbstractGenerateDerivativeBase extends EmitEvent { EntityTypeManagerInterface $entity_type_manager, EventGeneratorInterface $event_generator, StatefulStomp $stomp, - JwtAuth $auth, IslandoraUtils $utils, MediaSourceService $media_source, TokenInterface $token, MessengerInterface $messenger, ConfigFactoryInterface $config, - EntityFieldManagerInterface $entity_field_manager + EntityFieldManagerInterface $entity_field_manager, + EventDispatcherInterface $event_dispatcher ) { $this->utils = $utils; $this->mediaSource = $media_source; @@ -125,8 +125,8 @@ class AbstractGenerateDerivativeBase extends EmitEvent { $entity_type_manager, $event_generator, $stomp, - $auth, - $messenger + $messenger, + $event_dispatcher ); } @@ -142,13 +142,13 @@ class AbstractGenerateDerivativeBase extends EmitEvent { $container->get('entity_type.manager'), $container->get('islandora.eventgenerator'), $container->get('islandora.stomp'), - $container->get('jwt.authentication.jwt'), $container->get('islandora.utils'), $container->get('islandora.media_source_service'), $container->get('token'), $container->get('messenger'), $container->get('config.factory'), - $container->get('entity_field.manager') + $container->get('entity_field.manager'), + $container->get('event_dispatcher') ); } diff --git a/src/Plugin/Action/EmitFileEvent.php b/src/Plugin/Action/EmitFileEvent.php index cd20b3d5..f8d028d4 100644 --- a/src/Plugin/Action/EmitFileEvent.php +++ b/src/Plugin/Action/EmitFileEvent.php @@ -2,17 +2,12 @@ namespace Drupal\islandora\Plugin\Action; +use Drupal\islandora\EventGenerator\EmitEvent; + use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\File\FileSystemInterface; -use Drupal\Core\Messenger\MessengerInterface; -use Drupal\Core\Session\AccountInterface; use Drupal\Core\Site\Settings; use Drupal\Core\StreamWrapper\StreamWrapperManager; -use Drupal\jwt\Authentication\Provider\JwtAuth; -use Drupal\islandora\EventGenerator\EmitEvent; -use Drupal\islandora\EventGenerator\EventGeneratorInterface; -use Stomp\StatefulStomp; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -34,52 +29,9 @@ class EmitFileEvent extends EmitEvent { protected $fileSystem; /** - * Constructs a EmitEvent action. - * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin_id for the plugin instance. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\Core\Session\AccountInterface $account - * Current user. - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * Entity type manager. - * @param \Drupal\islandora\EventGenerator\EventGeneratorInterface $event_generator - * EventGenerator service to serialize AS2 events. - * @param \Stomp\StatefulStomp $stomp - * Stomp client. - * @param \Drupal\jwt\Authentication\Provider\JwtAuth $auth - * JWT Auth client. - * @param \Drupal\Core\File\FileSystemInterface $file_system - * File system service. - * @param \Drupal\Core\Messenger\MessengerInterface $messenger - * The messenger. + * Setter for the file system service. */ - public function __construct( - array $configuration, - $plugin_id, - $plugin_definition, - AccountInterface $account, - EntityTypeManagerInterface $entity_type_manager, - EventGeneratorInterface $event_generator, - StatefulStomp $stomp, - JwtAuth $auth, - FileSystemInterface $file_system, - MessengerInterface $messenger - ) { - parent::__construct( - $configuration, - $plugin_id, - $plugin_definition, - $account, - $entity_type_manager, - $event_generator, - $stomp, - $auth, - $messenger - ); + public function setFileSystemService(FileSystemInterface $file_system) { $this->fileSystem = $file_system; } @@ -87,18 +39,11 @@ class EmitFileEvent extends EmitEvent { * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('current_user'), - $container->get('entity_type.manager'), - $container->get('islandora.eventgenerator'), - $container->get('islandora.stomp'), - $container->get('jwt.authentication.jwt'), - $container->get('file_system'), - $container->get('messenger') - ); + $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); + + $instance->setFileSystemService($container->get('file_system')); + + return $instance; } /** diff --git a/src/Plugin/Action/EmitMediaEvent.php b/src/Plugin/Action/EmitMediaEvent.php index 275dd510..294f9aea 100644 --- a/src/Plugin/Action/EmitMediaEvent.php +++ b/src/Plugin/Action/EmitMediaEvent.php @@ -3,14 +3,8 @@ namespace Drupal\islandora\Plugin\Action; use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Messenger\MessengerInterface; -use Drupal\Core\Session\AccountInterface; -use Drupal\jwt\Authentication\Provider\JwtAuth; use Drupal\islandora\EventGenerator\EmitEvent; -use Drupal\islandora\EventGenerator\EventGeneratorInterface; use Drupal\islandora\MediaSource\MediaSourceService; -use Stomp\StatefulStomp; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -31,72 +25,15 @@ class EmitMediaEvent extends EmitEvent { */ protected $mediaSource; - /** - * Constructs a EmitEvent action. - * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin_id for the plugin instance. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\Core\Session\AccountInterface $account - * Current user. - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * Entity type manager. - * @param \Drupal\islandora\EventGenerator\EventGeneratorInterface $event_generator - * EventGenerator service to serialize AS2 events. - * @param \Stomp\StatefulStomp $stomp - * Stomp client. - * @param \Drupal\jwt\Authentication\Provider\JwtAuth $auth - * JWT Auth client. - * @param \Drupal\islandora\MediaSource\MediaSourceService $media_source - * Media source service. - * @param \Drupal\Core\Messenger\MessengerInterface $messenger - * The messenger. - */ - public function __construct( - array $configuration, - $plugin_id, - $plugin_definition, - AccountInterface $account, - EntityTypeManagerInterface $entity_type_manager, - EventGeneratorInterface $event_generator, - StatefulStomp $stomp, - JwtAuth $auth, - MediaSourceService $media_source, - MessengerInterface $messenger - ) { - parent::__construct( - $configuration, - $plugin_id, - $plugin_definition, - $account, - $entity_type_manager, - $event_generator, - $stomp, - $auth, - $messenger - ); - $this->mediaSource = $media_source; - } - /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('current_user'), - $container->get('entity_type.manager'), - $container->get('islandora.eventgenerator'), - $container->get('islandora.stomp'), - $container->get('jwt.authentication.jwt'), - $container->get('islandora.media_source_service'), - $container->get('messenger') - ); + $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); + + $instance->setMediaSourceService($container->get('islandora.media_source_service')); + + return $instance; } /** @@ -108,4 +45,11 @@ class EmitMediaEvent extends EmitEvent { return $data; } + /** + * Setter for the media source service. + */ + public function setMediaSourceService(MediaSourceService $media_source) { + $this->mediaSource = $media_source; + } + }