Skip to content
Snippets Groups Projects

Issue #3224250: Subuser management

Open lambic requested to merge issue/sendgrid_integration-3224250:8.x-2.x into 8.x-2.x
2 files
+ 590
0
Compare changes
  • Side-by-side
  • Inline
Files
2
src/Api.php 0 → 100644
+ 577
0
<?php
namespace Drupal\sendgrid_integration;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Cache\CacheFactoryInterface;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use \Exception;
/**
* Class SendGridReportsController.
*
* @package Drupal\sengrid_integration\Controller
*/
class Api {
/**
* Api Key of SendGrid.
*
* @var array|mixed|null
*/
protected $apiKey = NULL;
/**
* Cache bin of SendGrid Reports module.
*
* @var string
*/
protected $bin = 'sendgrid_integration';
/**
* Include the messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Logger service.
*
* @var \Drupal\Core\Logger\LoggerChannelFactory
*/
protected $loggerFactory;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The cache factory service.
*
* @var \Drupal\Core\Cache\CacheFactoryInterface
*/
protected $cacheFactory;
/**
* The subuser to perform API calls on behalf of.
*/
protected $subuser;
/**
* Api constructor.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* The logger factory.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* The module handler service.
* @param \Drupal\Core\Cache\CacheFactoryInterface $cacheFactory
* The cache factory service.
*/
public function __construct(ConfigFactoryInterface $config_factory, MessengerInterface $messenger, LoggerChannelFactoryInterface $logger_factory, ModuleHandlerInterface $moduleHandler, CacheFactoryInterface $cacheFactory) {
$this->configFactory = $config_factory;
$this->messenger = $messenger;
$this->loggerFactory = $logger_factory;
$this->moduleHandler = $moduleHandler;
$this->cacheFactory = $cacheFactory;
// Load key from variables and throw errors if not there.
$key_secret = $this->configFactory
->get('sendgrid_integration.settings')
->get('apikey');
if ($this->moduleHandler->moduleExists('key')) {
$key = \Drupal::service('key.repository')->getKey($key_secret);
if ($key && $key->getKeyValue()) {
$this->apiKey = $key->getKeyValue();
}
}
else {
$this->apiKey = $key_secret;
}
// Display message one time if api key is not set.
if (empty($this->apiKey)) {
$this->loggerFactory->get('sengrid_integration')
->warning(t('SendGrid Module is not setup with API key.'));
$this->messenger->addWarning('Sendgrid Module is not setup with an API key.');
}
}
/**
* Set the subuser to perform API calls on behalf of.
*
* @param string $subuser
* A valid subuser.
*
* @return \Drupal\sendgrid_integration\Api
* This object.
*/
public function setSubuser(string $subuser):Api {
$this->subuser = $subuser;
return $this;
}
/**
* Sets the cache to sengrid_integration bin.
*
* @param string $cid
* Cache Id.
* @param array $data
* The data should be cached.
*/
protected function setCache($cid, array $data) {
if (!empty($data)) {
$this->cacheFactory->get($this->bin)->set($cid, $data);
}
}
/**
* Get the guzzle client.
*
* @param bool $parent
* True if we want to use the parent user for this request.
*
* @return \Guzzlehttp\Client
* The Guzzle client object.
*/
protected function getClient($parent = FALSE) {
$headers['Authorization'] = 'Bearer ' . $this->apiKey;
if ($this->subuser && !$parent) {
$headers['on-behalf-of'] = $this->subuser;
}
$client = new Client([
'base_uri' => 'https://api.sendgrid.com/v3/',
'headers' => $headers,
]);
return $client;
}
/**
* Get request to SendGrid.
*
* @param string $path
* Part of SendGrid endpoint.
* @param array $query
* Query params to the request.
* @param bool $parent
* Perform the request as the parent user.
*
* @return bool|mixed
* Decoded json or FALSE.
*/
protected function get($path, array $query = [], $parent = FALSE) {
$client = $this->getClient($parent);
// Lets attempt the request and catch an error if it fails.
try {
$response = $client->get($path, ['query' => $query]);
}
catch (ClientException $e) {
$code = Xss::filter($e->getCode());
devel_debug($code);
$this->loggerFactory->get('sengrid_integration')
->error(t('SendGrid module failed to receive data. HTTP Error Code @errno', ['@errno' => $code]));
$this->messenger->addError(t('SendGrid module failed to receive data. See logs.'));
return FALSE;
}
// Sanitize return before using in Drupal.
$body = Xss::filter($response->getBody());
return json_decode($body);
}
/**
* Post request to SendGrid.
*
* @param string $path
* Part of SendGrid endpoint.
* @param array $data
* Query params to the request.
* @param bool $parent
* Perform the request as the parent user.
*
* @return bool|mixed
* Decoded json or FALSE.
*/
protected function post($path, array $data, $parent = FALSE) {
$client = $this->getClient($parent);
// Lets attempt the request and catch an error if it fails.
try {
$response = $client->post($path, ['json' => $data]);
}
catch (ClientException $e) {
$code = Xss::filter($e->getCode());
$this->loggerFactory->get('sengrid_integration')
->error(t('SendGrid module failed to post data. HTTP Error Code @errno', ['@errno' => $code]));
$this->messenger->addError(t('SendGrid module failed to post data. See logs.'));
return FALSE;
}
// Sanitize return before using in Drupal.
$body = Xss::filter($response->getBody());
return json_decode($body);
}
/**
* Delete request to SendGrid.
*
* @param string $path
* Part of SendGrid endpoint.
* @param string $data
* The id of the item to be deleted.
* @param bool $parent
* Perform the request as the parent user.
*
* @return bool|mixed
* Decoded json or FALSE.
*/
protected function delete($path, string $data, $parent = FALSE) {
$client = $this->getClient($parent);
// Lets attempt the request and catch an error if it fails.
try {
$response = $client->delete($path . '/' . $data);
}
catch (ClientException $e) {
$code = Xss::filter($e->getCode());
$this->loggerFactory->get('sengrid_integration')
->error(t('SendGrid module failed to receive data. HTTP Error Code @errno', ['@errno' => $code]));
$this->messenger->addError(t('SendGrid module failed to receive data. See logs.'));
return FALSE;
}
// Sanitize return before using in Drupal.
$body = Xss::filter($response->getBody());
return json_decode($body);
}
/**
* Get subuser info for the passed username.
*
* @param string $username
* A string to search usernames for.
*
* @return array
* Data relating to the subuser.
*/
public function getSubUser(string $username): array {
$data = [];
$data['username'] = $username;
$response = $this->get('subusers', $data);
return $response;
}
/**
* Create a subuser.
*
* @param string $username
* The username of the subuser being created.
* @param string $email
* A valid email address for the subuser.
* @param string $password
* The subuser password.
* @param array $ips
* An array of IP addresses to associated with the user.
* If this is not passed, the least currently used IP will be used.
*
* @return array
* Response from sendgrid.
*/
public function createSubUser(string $username, string $email, string $password, array $ips = []): array {
$existing = $this->getSubUser($username);
foreach ($existing as $subuser) {
if ($subuser->username == $username) {
// @todo use custom exception.
throw new Exception('Username already exists: ' . $username);
}
}
if (!$ips) {
$ips = [$this->getLeastUsedIp()];
}
$data = [
'username' => $username,
'email' => $email,
'password' => $password,
'ips' => $ips,
];
$response = $this->post('subusers', $data, TRUE);
// Response from sendgrid doesn't give the IPs, so add it here.
if (is_object($response)) {
$response->ips = $ips;
}
devel_debug($response);
return (array) $response;
}
/**
* Delete a subuser.
*
* @param string $username
* The username of the subuser being deleted.
*/
public function deleteSubuser(string $username) {
$existing = $this->getSubUser($username);
$response = FALSE;
foreach ($existing as $subuser) {
if ($subuser->username == $username) {
$response = $this->delete('subusers', $subuser->username);
}
}
if ($response === FALSE) {
throw new Exception('Subuser not found.');
}
}
/**
* Create an API key.
*
* @param $name
* A name for the API key.
* @param $perms
* The perms to assign to this API key. NULL means full access.
*
* @return array
* Response from sendgrid.
*/
public function createApiKey($name, $perms = NULL): array {
if (!$this->user) {
// @todo use a custom exception.
throw new Exception('Attempt to create an API key without a subuser set.');
}
if (!$perms) {
$perms = $this->fullAccess();
}
$data = [
'name' => $name,
'scopes' => $perms,
];
return $this->post('api_keys', $data);
}
/**
* Get a list of valid IP Addresses.
*
* @param array $mapping
* An optional array of mappings to use to filter the ip list.
*
* @return array
* Array of valid IP addresses.
*/
public function getIpAddresses(array $mappings = []): array {
$ips = $this->get('ips');
if (!empty($mapping)) {
foreach ($ips as $key => $ip) {
if (!isset($mapping[$ip->ip])) {
unset($ips[$key]);
}
}
}
return $ips;
}
/**
* Get the IP address with the least number of associated subusers.
*
* @param array $mapping
* An optional array of mappings to use to filter the ip list.
*
* @return string
* The IP address with the least associated subusers.
*/
public function getLeastUsedIp(array $mappings = []): string {
$ips = $this->getIpAddresses($mappings);
$least = $this->least($ips, 'ip');
return $least;
}
/**
* Get a list of available domains.
*
* @return array
* Array of valid send domains.
*/
public function getDomains() {
$domains = $this->get('whitelabel/domains', NULL, TRUE);
return $domains;
}
/**
* Get the domain with the least number of associated subusers.
*
* @return string
* The domain with the least associated subusers.
*/
public function getLeastUsedDomain() {
$domains = $this->getDomains();
$least = $this->least($domains, 'id');
return $least;
}
/**
* Associated a domain with a subuser.
*
* @param $domain
* Domain to associate. Least used domain will be picked if this is empty.
*/
public function associateDomain($domain = '') {
if (!$this->user) {
throw new Exception('Attempt to associated a domain without a subuser set.');
}
$domain = $domain ?? $this->getLeastUsedDomain();
$data = ['username' => $this->user];
$response = $this->post('whitelabel/domains/' . $domain . '/subuser', $data, TRUE);
return $response;
}
/**
* Get a list of available link brandings.
*
* @return array
* Array of available branding links.
*/
public function getLinks() {
$links = $this->get('whitelabel/links');
return $links;
}
/**
* Get the link with the least number of associated subusers.
*
* @return string
* Link with the least associated subusers.
*/
public function getLeastUsedLink() {
$links = $this->getLinks();
$least = $this->least($links, 'id');
return $least;
}
/**
* Associated a link with a subuser.
*/
public function associateLink($link) {
if (!$this->user) {
throw new Exception('Attempt to associated a link without a subuser set.');
}
$link = $link ?? $this->getLeastUsedLink();
$data = ['username' => $this->user];
$response = $this->post('whitelabel/links/' . $link . '/subuser', $data, TRUE);
return $response;
}
/**
* Helper function to find the least number of subusers in an array.
*
* @param array $list
* A list of objects as returned from the Sendgrid API.
* @param string $return
* The name of the objectproperty to return.
*
* @return string
* The value of $return for the least associated subusers.
*/
private function least($list, $return) {
$prev = 0;
$least = '';
foreach ($list as $row) {
$count = count($row->subusers);
if (!$prev || $count < $prev) {
$prev = $count;
$least = $row->$return;
}
}
return $least;
}
/**
* Helper functions for api key scopes.
*/
private function fullAccess() {
return [
"alerts.create",
"alerts.read",
"alerts.update",
"alerts.delete",
"ips.warmup.create",
"ips.warmup.read",
"ips.warmup.update",
"ips.warmup.delete",
"ips.pools.create",
"ips.pools.read",
"ips.pools.update",
"ips.pools.delete",
"ips.pools.ips.create",
"ips.pools.ips.read",
"ips.pools.ips.update",
"ips.pools.ips.delete",
"ips.read",
"mail.send",
"mail_settings.bcc.read",
"mail_settings.address_whitelist.read",
"mail_settings.footer.read",
"mail_settings.forward_spam.read",
"mail_settings.plain_content.read",
"mail_settings.spam_check.read",
"mail_settings.bounce_purge.read",
"mail_settings.forward_bounce.read",
"tracking_settings.click.read",
"tracking_settings.subscription.read",
"tracking_settings.open.read",
"tracking_settings.google_analytics.read",
"stats.read",
"stats.global.read",
"categories.stats.read",
"categories.stats.sums.read",
"devices.stats.read",
"clients.stats.read",
"clients.phone.stats.read",
"clients.tablet.stats.read",
"clients.webmail.stats.read",
"clients.desktop.stats.read",
"geo.stats.read",
"mailbox_providers.stats.read",
"browsers.stats.read",
"user.webhooks.parse.stats.read",
"api_keys.read",
"categories.create",
"categories.read",
"categories.update",
"categories.delete",
"mail.batch.create",
"mail.batch.read",
"mail.batch.update",
"mail.batch.delete",
"access_settings.whitelist.create",
"access_settings.whitelist.read",
"access_settings.whitelist.update",
"access_settings.whitelist.delete",
"access_settings.activity.read",
"whitelabel.create",
"whitelabel.read",
"whitelabel.update",
"whitelabel.delete",
"suppression.create",
"suppression.read",
"suppression.update",
"suppression.delete",
];
}
}
Loading