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

Added Azure Key Vault certificate provider #3

Closed
wants to merge 1 commit into from
Closed
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

* [PR-3](https://github.com/OS2web/os2web_key/pull/3)
Added Azure Key Vault certificate provider
* [PR-2](https://github.com/OS2web/os2web_key/pull/2)
Updated documentation
* [PR-1](https://github.com/OS2web/os2web_key/pull/1)
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"php": "^8.1",
"ext-openssl": "*",
"drupal/core": "^9 || ^10",
"drupal/key": "^1.17"
"drupal/key": "^1.17",
"itk-dev/serviceplatformen": "dev-feature/guzzle6-adapter"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
Expand Down
204 changes: 204 additions & 0 deletions src/Plugin/KeyProvider/AzureKeyVaultKeyProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
<?php

namespace Drupal\os2web_key\Plugin\KeyProvider;

use Drupal\Core\Form\FormStateInterface;
use Drupal\key\KeyInterface;
use Drupal\key\Plugin\KeyPluginFormInterface;
use Drupal\key\Plugin\KeyProviderBase;
use GuzzleHttp\Client;
use Http\Adapter\Guzzle6\Client as GuzzleAdapter;
use Http\Factory\Guzzle\RequestFactory;
use ItkDev\AzureKeyVault\Authorisation\VaultToken;
use ItkDev\AzureKeyVault\KeyVault\VaultSecret;
use ItkDev\Serviceplatformen\Certificate\AzureKeyVaultCertificateLocator;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Adds a key provider for Azure Key Vault.
*
* @KeyProvider(
* id = "os2web_azure_key_vault",
* label = @Translation("Azure Key Vault"),
* description = @Translation("Azure Key Vault"),
* storage_method = "remote",
* key_value = {
* "accepted" = FALSE,
* "required" = FALSE
* }
* )
*/
final class AzureKeyVaultKeyProvider extends KeyProviderBase implements KeyPluginFormInterface {

private const TENANT_ID = 'tenant_id';
private const APPLICATION_ID = 'application_id';
private const CLIENT_SECRET = 'client_secret';
private const NAME = 'name';
private const SECRET = 'secret';
private const VERSION = 'version';

/**
* Constructor.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The unique ID of this plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Psr\Log\LoggerInterface $logger
* The logger for this module.
*/
public function __construct(
array $configuration,
string $plugin_id,
$plugin_definition,
private readonly LoggerInterface $logger,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}

/**
* {@inheritdoc}
*/
public static function create(
ContainerInterface $container,
array $configuration,
$plugin_id,
$plugin_definition,
) {
/** @var \Psr\Log\LoggerInterface $logger */
$logger = $container->get('logger.channel.os2web_key');

return new static(
$configuration,
$plugin_id,
$plugin_definition,
$logger
);
}

/**
* {@inheritdoc}
*/
public function defaultConfiguration(): array {
return [
self::TENANT_ID => '',
self::APPLICATION_ID => '',
self::CLIENT_SECRET => '',
self::NAME => '',
self::SECRET => '',
self::VERSION => '',
];
}

/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
$configuration = $this->getConfiguration();

$settings = [
self::TENANT_ID => ['title' => $this->t('Tenant id')],
self::APPLICATION_ID => ['title' => $this->t('Application id')],
self::CLIENT_SECRET => ['title' => $this->t('Client secret')],
self::NAME => ['title' => $this->t('Name')],
self::SECRET => ['title' => $this->t('Secret')],
self::VERSION => ['title' => $this->t('Version')],
];

foreach ($settings as $key => $info) {
$form[$key] = [
'#type' => 'textfield',
'#title' => $info['title'],
'#default_value' => $configuration[$key] ?? NULL,
'#required' => TRUE,
];
}

return $form;
}

/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state): void {
try {
$this->getCertificate();
}
catch (\Throwable $throwable) {
$form_state->setError($form, $this->t('Error getting certificate: %message', ['%message' => $throwable->getMessage()]));
}
}

/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state): void {
$this->setConfiguration($form_state->getValues());
}

/**
* {@inheritdoc}
*/
public function getKeyValue(KeyInterface $key): ?string {
try {
return $this->getCertificate();
}
catch (\Throwable $throwable) {
}

return NULL;
}

/**
* Get certificate.
*
* @return string
* The certificate.
*
* @throws \ItkDev\AzureKeyVault\Exception\TokenException
* @throws \ItkDev\Serviceplatformen\Certificate\Exception\AzureKeyVaultCertificateLocatorException
*/
private function getCertificate(): string {
try {
$httpClient = new GuzzleAdapter(new Client());
$requestFactory = new RequestFactory();

$vaultToken = new VaultToken($httpClient, $requestFactory);

$options = $this->getConfiguration();

$token = $vaultToken->getToken(
$options[self::TENANT_ID],
$options[self::APPLICATION_ID],
$options[self::CLIENT_SECRET],
);

$vault = new VaultSecret(
$httpClient,
$requestFactory,
$options[self::NAME],
$token->getAccessToken()
);

$locator = new AzureKeyVaultCertificateLocator(
$vault,
$options[self::SECRET],
$options[self::VERSION],
);

return $locator->getCertificate();
}
catch (\Exception $exception) {
// Log the exception and re-throw it.
$this->logger->error('Error getting certificate: @message', [
'@message' => $exception->getMessage(),
'throwable' => $exception,
]);
throw $exception;
}
}

}
Loading