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

UHF-10559: Add support for secondary key authentication #175

Merged
merged 5 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 3 additions & 29 deletions documentation/pubsub-messaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ Provides an integration to [Azure's Web PubSub service](https://azure.microsoft.

## Configuration

You must define a [JSON Vault item](/documentation/api-accounts.md#managing-external-api-credentials) to use this feature. The data field should be a JSON string containing `endpoint`, `hub`, `group` and `access_key`:
You must define a [JSON Vault item](/documentation/api-accounts.md#managing-external-api-credentials) to use this feature. The data field should be a JSON string containing `endpoint`, `hub`, `group` and `access_key` and optional `secondary_access_key`:

```json
{"endpoint": "<endpoint>", "hub": "<hub>", "group": "<group>", "access_key": "<access-key>"}
{"endpoint": "<endpoint>", "hub": "<hub>", "group": "<group>", "access_key": "<access-key>", "secondary_access_key": "<secondary-access-key>"}
```

## Usage
Expand Down Expand Up @@ -85,34 +85,8 @@ $pubsub_account = [
'hub' => '<hub>',
'group' => '<group>',
'access_key' => '<access-key>',
'secondary_access_key' => '<secondary-access-key>',
]),
];
$config['helfi_api_base.api_accounts']['vault'][] = $pubsub_account;
```

## Solving pubsub related problems

If menus or news or other content doesn't update normally, you can verify that the pubsub service is working correctly

### Artemis is not up on etusivu-instance
- See that frontpage production has artemis pod up and running

#### If the pod is not running
- See if there is a pipeline to get it up again OR
- Contact HiQ


### Pubsub-process is not running
- Go to any production site's cron pod
- run `ps aux`, you should see pubsub related process on the list

#### If the process is not running
- Short term solution is to run `drush cr` to force the site to fetch new data.
- You can run production deployment to get it running again.


### Bad credentials
- Go to any cron pod and look for authorization error

#### Update the credentials
- Go and update the pubsub-vault credentials
6 changes: 0 additions & 6 deletions drush.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ services:
- '@datetime.time'
tags:
- { name: drush.command }
helfi_api_base.pubsub_commands:
class: \Drupal\helfi_api_base\Commands\PubSubCommands
arguments:
- '@helfi_api_base.pubsub_manager'
tags:
- { name: drush.command }
helfi_api_base.deploy_commands:
class: \Drupal\helfi_api_base\Commands\DeployCommands
arguments: ['@event_dispatcher']
Expand Down
58 changes: 11 additions & 47 deletions helfi_api_base.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,53 +108,17 @@ services:
arguments:
- '@config.factory'

Drupal\helfi_api_base\Azure\PubSub\SettingsFactory: '@helfi_api_base.pubsub_settings_factory'
helfi_api_base.pubsub_settings_factory:
class: Drupal\helfi_api_base\Azure\PubSub\SettingsFactory
arguments:
- '@helfi_api_base.vault_manager'

Drupal\helfi_api_base\Azure\PubSub\PubSubClientFactory: '@helfi_api_base.pubsub_client_factory'
helfi_api_base.pubsub_client_factory:
class: Drupal\helfi_api_base\Azure\PubSub\PubSubClientFactory

helfi_api_base.pubsub_client:
public: false
class: \WebSocket\Client
factory: ['@helfi_api_base.pubsub_client_factory', 'create']
arguments:
- '@helfi_api_base.pubsub_settings'
- '@datetime.time'

Drupal\helfi_api_base\Azure\PubSub\Settings: '@helfi_api_base.pubsub_settings'
helfi_api_base.pubsub_settings:
class: Drupal\helfi_api_base\Azure\PubSub\Settings
factory: ['@helfi_api_base.pubsub_settings_factory', 'create']

Drupal\helfi_api_base\Azure\PubSub\PubSubManager: '@helfi_api_base.pubsub_manager'
helfi_api_base.pubsub_manager:
class: Drupal\helfi_api_base\Azure\PubSub\PubSubManager
arguments:
- '@helfi_api_base.pubsub_client'
- '@event_dispatcher'
- '@datetime.time'
- '@helfi_api_base.pubsub_settings'

Drupal\helfi_api_base\Cache\CacheTagInvalidatorInterface: '@helfi_api_base.cache_tag_invalidator'
Drupal\helfi_api_base\Cache\CacheTagInvalidator: '@helfi_api_base.cache_tag_invalidator'
helfi_api_base.cache_tag_invalidator:
class: Drupal\helfi_api_base\Cache\CacheTagInvalidator
arguments:
- '@helfi_api_base.pubsub_manager'

Drupal\helfi_api_base\EventSubscriber\CacheTagInvalidatorSubscriber: '@helfi_api_base.cache_tag_invalidator_subscriber'
helfi_api_base.cache_tag_invalidator_subscriber:
class: Drupal\helfi_api_base\EventSubscriber\CacheTagInvalidatorSubscriber
arguments:
- '@cache_tags.invalidator'
- '@helfi_api_base.environment_resolver'
tags:
- { name: event_subscriber }
Drupal\helfi_api_base\Azure\PubSub\PubSubClientFactory: ~
Drupal\helfi_api_base\Azure\PubSub\PubSubClientFactoryInterface: '@Drupal\helfi_api_base\Azure\PubSub\PubSubClientFactory'
Drupal\helfi_api_base\Azure\PubSub\SettingsFactory: ~
Drupal\helfi_api_base\Azure\PubSub\Settings:
factory: ['@Drupal\helfi_api_base\Azure\PubSub\SettingsFactory', 'create']
Drupal\helfi_api_base\Azure\PubSub\PubSubManager: ~
Drupal\helfi_api_base\Azure\PubSub\PubSubManagerInterface: '@Drupal\helfi_api_base\Azure\PubSub\PubSubManager'
Drupal\helfi_api_base\Cache\CacheTagInvalidatorInterface: '@Drupal\helfi_api_base\Cache\CacheTagInvalidator'
Drupal\helfi_api_base\Cache\CacheTagInvalidator: ~

Drupal\helfi_api_base\EventSubscriber\CacheTagInvalidatorSubscriber: ~

Drupal\helfi_api_base\Entity\Revision\RevisionManager: '@helfi_api_base.revision_manager'
helfi_api_base.revision_manager:
Expand Down
30 changes: 21 additions & 9 deletions src/Azure/PubSub/PubSubClientFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,43 @@
/**
* A Web socket client factory.
*/
final class PubSubClientFactory {
final class PubSubClientFactory implements PubSubClientFactoryInterface {

/**
* Constructs a new websocket client object.
* Constructs a new instance.
*
* @param \Drupal\helfi_api_base\Azure\PubSub\Settings $settings
* The settings.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time interface.
* @param \Drupal\helfi_api_base\Azure\PubSub\Settings $settings
* The PubSub settings.
*/
public function __construct(
private readonly TimeInterface $time,
private readonly Settings $settings,
) {
}

/**
* Constructs a new websocket client object.
*
* @param string $accessKey
* The access key.
*
* @return \WebSocket\Client
* The client.
*/
public function create(Settings $settings, TimeInterface $time) : Client {
$url = sprintf('wss://%s/client/hubs/%s', rtrim($settings->endpoint, '/'), $settings->hub);
public function create(string $accessKey) : Client {
$url = sprintf('wss://%s/client/hubs/%s', rtrim($this->settings->endpoint, '/'), $this->settings->hub);

$authorizationToken = JWT::encode([
'aud' => $url,
'iat' => $time->getCurrentTime(),
'exp' => $time->getCurrentTime() + 3600,
'iat' => $this->time->getCurrentTime(),
'exp' => $this->time->getCurrentTime() + 3600,
'role' => [
'webpubsub.sendToGroup',
'webpubsub.joinLeaveGroup',
],
], $settings->accessKey, 'HS256');
], $accessKey, 'HS256');

return new Client($url, [
'headers' => [
Expand Down
25 changes: 25 additions & 0 deletions src/Azure/PubSub/PubSubClientFactoryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Drupal\helfi_api_base\Azure\PubSub;

use WebSocket\Client;

/**
* A Web socket client factory.
*/
interface PubSubClientFactoryInterface {

/**
* Constructs a new websocket client object.
*
* @param string $accessKey
* The access key.
*
* @return \WebSocket\Client
* The client.
*/
public function create(string $accessKey): Client;

}
107 changes: 49 additions & 58 deletions src/Azure/PubSub/PubSubManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
namespace Drupal\helfi_api_base\Azure\PubSub;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Utility\Error;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use WebSocket\Client;
use WebSocket\ConnectionException;
Expand All @@ -15,29 +18,32 @@
final class PubSubManager implements PubSubManagerInterface {

/**
* A flag indicating whether we've joined the group.
* The websocket client.
*
* @var bool
* @var \WebSocket\Client|null
*/
private bool $joinedGroup = FALSE;
private ?Client $client = NULL;

/**
* Constructs a new instance.
*
* @param \WebSocket\Client $client
* The websocket client.
* @param \Drupal\helfi_api_base\Azure\PubSub\PubSubClientFactoryInterface $clientFactory
* The client factory.
* @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher
* The event dispatcher service.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The datetime service.
* @param \Drupal\helfi_api_base\Azure\PubSub\Settings $settings
* The PubSub settings.
* @param \Psr\Log\LoggerInterface $logger
* The logger.
*/
public function __construct(
private readonly Client $client,
private readonly PubSubClientFactoryInterface $clientFactory,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly TimeInterface $time,
private readonly Settings $settings,
#[Autowire(service: 'logger.channel.helfi_api_base')] private readonly LoggerInterface $logger,
) {
}

Expand All @@ -48,31 +54,45 @@ public function __construct(
* @throws \WebSocket\ConnectionException
* @throws \WebSocket\TimeoutException
*/
private function joinGroup() : void {
if ($this->joinedGroup) {
private function initializeClient() : void {
if ($this->client) {
return;
}
$this->client->text(
$this->encodeMessage([
'type' => 'joinGroup',
'group' => $this->settings->group,
])
);
$client = $exception = NULL;

// Initialize client with primary key, fallback to secondary key.
foreach ($this->settings->accessKeys as $key) {
$exception = NULL;

try {
$client = $this->clientFactory->create($key);
$client->text($this->encodeMessage([
'type' => 'joinGroup',
'group' => $this->settings->group,
]));
}
catch (ConnectionException $exception) {
Error::logException($this->logger, $exception);
}
}

if ($exception instanceof ConnectionException) {
throw $exception;
}

try {
// Wait until we've actually joined the group.
$message = $this->decodeMessage((string) $this->client->receive());
$message = $this->decodeMessage((string) $client->receive());

if (isset($message['event']) && $message['event'] === 'connected') {
$this->joinedGroup = TRUE;
$this->client = $client;

return;
}
}
catch (\JsonException) {
}

throw new ConnectionException('Failed to join a group.');
throw new ConnectionException('Failed to initialize the client.');
}

/**
Expand Down Expand Up @@ -105,57 +125,28 @@ private function decodeMessage(string $message) : array {
return json_decode($message, TRUE, flags: JSON_THROW_ON_ERROR);
}

/**
* {@inheritdoc}
*/
public function setTimeout(int $timeout) : self {
$this->client->setTimeout($timeout);
return $this;
}

/**
* Asserts the settings.
*
* This is used to exit early if required settings are not populated.
*/
private function assertSettings() : void {
$vars = get_object_vars($this->settings);

foreach ($vars as $key => $value) {
if (empty($this->settings->{$key})) {
throw new ConnectionException("Azure PubSub '$key' is not configured.");
}
}
}

/**
* {@inheritdoc}
*/
public function sendMessage(array $message) : self {
$this->assertSettings();
$this->joinGroup();

$this->client
->text(
$this->encodeMessage([
'type' => 'sendToGroup',
'group' => $this->settings->group,
'dataType' => 'json',
'data' => $message + [
'timestamp' => $this->time->getCurrentTime(),
],
])
);

$this->initializeClient();

$this->client->text($this->encodeMessage([
'type' => 'sendToGroup',
'group' => $this->settings->group,
'dataType' => 'json',
'data' => $message + [
'timestamp' => $this->time->getCurrentTime(),
],
]));
return $this;
}

/**
* {@inheritdoc}
*/
public function receive() : string {
$this->assertSettings();
$this->joinGroup();
$this->initializeClient();

$message = (string) $this->client->receive();
$json = $this->decodeMessage($message);
Expand Down
11 changes: 0 additions & 11 deletions src/Azure/PubSub/PubSubManagerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,4 @@ public function sendMessage(array $message): self;
*/
public function receive(): string;

/**
* Sets the client timeout.
*
* @param int $timeout
* The timeout in seconds.
*
* @return self
* The self.
*/
public function setTimeout(int $timeout): self;

}
Loading