Skip to content

Commit

Permalink
Merge pull request #77 from adam-vessey/feature/stomp-headers
Browse files Browse the repository at this point in the history
More flexible headers for STOMP messages
  • Loading branch information
Daniel Aitken authored Jun 29, 2021
2 parents d7cd5db + 050bfce commit a3ebd6c
Show file tree
Hide file tree
Showing 10 changed files with 321 additions and 220 deletions.
5 changes: 5 additions & 0 deletions islandora.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,8 @@ services:
islandora.gemini.lookup:
class: Drupal\islandora\GeminiLookup
arguments: ['@islandora.gemini.client', '@jwt.authentication.jwt', '@islandora.media_source_service', '@http_client', '@logger.channel.islandora']
islandora.stomp.auth_header_listener:
class: Drupal\islandora\EventSubscriber\StompHeaderEventSubscriber
arguments: ['@jwt.authentication.jwt']
tags:
- { name: event_subscriber }
97 changes: 97 additions & 0 deletions src/Event/StompHeaderEvent.php
Original file line number Diff line number Diff line change
@@ -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 changes: 8 additions & 0 deletions src/Event/StompHeaderEventException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Drupal\islandora\Event;

/**
* Typification for handling exceptions specific to STOMP header generation.
*/
class StompHeaderEventException extends \Exception {}
56 changes: 56 additions & 0 deletions src/Event/StompHeaderEventInterface.php
Original file line number Diff line number Diff line change
@@ -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();

}
47 changes: 23 additions & 24 deletions src/EventGenerator/EmitEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
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.
Expand Down Expand Up @@ -49,11 +51,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;

/**
* Constructs a EmitEvent action.
Expand All @@ -72,8 +74,8 @@ 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 \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* Event dispatcher service.
*/
public function __construct(
array $configuration,
Expand All @@ -83,14 +85,14 @@ public function __construct(
EntityTypeManagerInterface $entity_type_manager,
EventGeneratorInterface $event_generator,
StatefulStomp $stomp,
JwtAuth $auth
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->eventDispatcher = $event_dispatcher;
}

/**
Expand All @@ -105,37 +107,34 @@ public static function create(ContainerInterface $container, array $configuratio
$container->get('entity_type.manager'),
$container->get('islandora.eventgenerator'),
$container->get('islandora.stomp'),
$container->get('jwt.authentication.jwt')
$container->get('event_dispatcher')
);
}

/**
* {@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(
t('Error getting JWT token for message. Check JWT Configuration.')
);
drupal_set_message(
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());
drupal_set_message($e->getMessage(), 'error');
return;
}
catch (\RuntimeException $e) {
// Notify the user the event couldn't be generated and abort.
\Drupal::logger('islandora')->error(
Expand Down
15 changes: 15 additions & 0 deletions src/EventSubscriber/JwtEventSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
*/
class JwtEventSubscriber implements EventSubscriberInterface {

const AUDIENCE = 'islandora';

/**
* User storage to load users.
*
Expand Down Expand Up @@ -100,6 +102,7 @@ public function setIslandoraClaims(JwtAuthGenerateEvent $event) {
$event->addClaim('sub', $this->currentUser->getAccountName());
$event->addClaim('roles', $this->currentUser->getRoles(FALSE));

$event->addClaim('aud', [static::AUDIENCE]);
}

/**
Expand All @@ -111,6 +114,18 @@ public function setIslandoraClaims(JwtAuthGenerateEvent $event) {
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');
Expand Down
63 changes: 63 additions & 0 deletions src/EventSubscriber/StompHeaderEventSubscriber.php
Original file line number Diff line number Diff line change
@@ -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");
}
}

}

}
Loading

0 comments on commit a3ebd6c

Please sign in to comment.