Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Event-based STOMP header generation #839

Merged
merged 10 commits into from
Sep 29, 2021
5 changes: 5 additions & 0 deletions islandora.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 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();

}
51 changes: 25 additions & 26 deletions src/EventGenerator/EmitEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
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.
Expand Down Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -95,16 +97,16 @@ public function __construct(
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;
}

/**
Expand All @@ -119,38 +121,35 @@ 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('messenger')
$container->get('messenger'),
$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(
$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(
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