Browse Source

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.
pull/851/head
Adam 3 years ago committed by GitHub
parent
commit
d800748653
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      islandora.services.yml
  2. 97
      src/Event/StompHeaderEvent.php
  3. 8
      src/Event/StompHeaderEventException.php
  4. 56
      src/Event/StompHeaderEventInterface.php
  5. 51
      src/EventGenerator/EmitEvent.php
  6. 15
      src/EventSubscriber/JwtEventSubscriber.php
  7. 63
      src/EventSubscriber/StompHeaderEventSubscriber.php
  8. 18
      src/Plugin/Action/AbstractGenerateDerivativeBase.php
  9. 73
      src/Plugin/Action/EmitFileEvent.php
  10. 80
      src/Plugin/Action/EmitMediaEvent.php

5
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 }

97
src/Event/StompHeaderEvent.php

@ -0,0 +1,97 @@
<?php
namespace Drupal\islandora\Event;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\EventDispatcher\Event;
/**
* Event used to build headers for STOMP.
*/
class StompHeaderEvent extends Event implements StompHeaderEventInterface {
/**
* Stashed entity, for context.
*
* @var \Drupal\Core\Entity\EntityInterface
*/
protected $entity;
/**
* Stashed user info, for context.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $user;
/**
* An array of data to be sent with the STOMP request, for context.
*
* @var array
*/
protected $data;
/**
* An array of configuration used to generate $data, for context.
*
* @var array
*/
protected $configuration;
/**
* The set of headers.
*
* @var \Symfony\Component\HttpFoundation\ParameterBag
*/
protected $headers;
/**
* Constructor.
*/
public function __construct(EntityInterface $entity, AccountInterface $user, array $data, array $configuration) {
$this->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;
}
}

8
src/Event/StompHeaderEventException.php

@ -0,0 +1,8 @@
<?php
namespace Drupal\islandora\Event;
/**
* Typification for handling exceptions specific to STOMP header generation.
*/
class StompHeaderEventException extends \Exception {}

56
src/Event/StompHeaderEventInterface.php

@ -0,0 +1,56 @@
<?php
namespace Drupal\islandora\Event;
/**
* Contract for representing an event to build headers for STOMP messages.
*/
interface StompHeaderEventInterface {
const EVENT_NAME = 'islandora.stomp.header_event';
/**
* Get the headers being built for STOMP.
*
* XXX: Ironically, using ParameterBag instead of HeaderBag due to case-
* sensitivity: In the context of HTTP, headers are case insensitive (and is
* what HeaderBag is intended; however, STOMP headers are case sensitive.
*
* @return \Symfony\Component\HttpFoundation\ParameterBag
* The headers
*/
public function getHeaders();
/**
* Fetch the entity provided as context.
*
* @return \Drupal\Core\Entity\EntityInterface
* The entity provided as context.
*/
public function getEntity();
/**
* Fetch the user provided as context.
*
* @return \Drupal\Core\Session\AccountInterface
* The user provided as context.
*/
public function getUser();
/**
* Fetch the data to be sent in the body of the request.
*
* @return array
* The array of data.
*/
public function getData();
/**
* Fetch the configuration of the action, for context.
*
* @return array
* The array of configuration for the upstream action.
*/
public function getConfiguration();
}

51
src/EventGenerator/EmitEvent.php

@ -11,11 +11,13 @@ use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\jwt\Authentication\Provider\JwtAuth;
use Drupal\islandora\Event\StompHeaderEvent;
use Drupal\islandora\Event\StompHeaderEventException;
use Stomp\Exception\StompException;
use Stomp\StatefulStomp;
use Stomp\Transport\Message;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Configurable action base for actions that publish messages to queues.
@ -52,11 +54,11 @@ abstract class EmitEvent extends ConfigurableActionBase implements ContainerFact
protected $stomp;
/**
* The JWT Auth Service.
* Event dispatcher service.
*
* @var \Drupal\jwt\Authentication\Provider\JwtAuth
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $auth;
protected $eventDispatcher;
/**
* The messenger.
@ -82,10 +84,10 @@ abstract class EmitEvent extends ConfigurableActionBase implements ContainerFact
* 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\Messenger\MessengerInterface $messenger
* The messenger.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* Event dispatcher service.
*/
public function __construct(
array $configuration,
@ -95,16 +97,16 @@ abstract class EmitEvent extends ConfigurableActionBase implements ContainerFact
EntityTypeManagerInterface $entity_type_manager,
EventGeneratorInterface $event_generator,
StatefulStomp $stomp,
JwtAuth $auth,
MessengerInterface $messenger
MessengerInterface $messenger,
EventDispatcherInterface $event_dispatcher
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->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(

15
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');

63
src/EventSubscriber/StompHeaderEventSubscriber.php

@ -0,0 +1,63 @@
<?php
namespace Drupal\islandora\EventSubscriber;
use Drupal\islandora\Event\StompHeaderEventInterface;
use Drupal\jwt\Authentication\Provider\JwtAuth;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Base STOMP header listener.
*/
class StompHeaderEventSubscriber implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* The JWT auth service.
*
* @var \Drupal\jwt\Authentication\Provider\JwtAuth
*/
protected $auth;
/**
* Constructor.
*/
public function __construct(
JwtAuth $auth
) {
$this->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");
}
}
}
}

18
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')
);
}

73
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;
}
/**

80
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;
}
}

Loading…
Cancel
Save