Skip to content

Commit

Permalink
ci: commit oat-sa/environment-management#
Browse files Browse the repository at this point in the history
  • Loading branch information
github-actions committed Nov 8, 2024
1 parent 2a48800 commit 530973b
Show file tree
Hide file tree
Showing 13 changed files with 631 additions and 79 deletions.
40 changes: 0 additions & 40 deletions .github/workflows/sonar.yml

This file was deleted.

4 changes: 4 additions & 0 deletions EventSubscriber/HttpRequestSecuritySubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ public function validateRouteOauth2Scopes(RequestEvent $event): void

$scopes = $token->claims()->get('scopes');

if (!is_array($scopes)) {
$scopes = explode(' ', $scopes);
}

if (count(array_intersect($scopes, $allowedScopes)) === 0) {
throw new UnauthorizedHttpException('Bearer', 'Invalid scope(s)');
}
Expand Down
147 changes: 147 additions & 0 deletions Http/Client/EnvironmentManagementAwareHttpClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php

declare(strict_types=1);

namespace OAT\Bundle\EnvironmentManagementClientBundle\Http\Client;

use InvalidArgumentException;
use Psr\Cache\InvalidArgumentException as CacheInvalidArgumentException;
use RuntimeException;
use Symfony\Component\Mime\Part\Multipart\FormDataPart;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;

class EnvironmentManagementAwareHttpClient implements HttpClientInterface
{
public const OPTION_TENANT_ID = 'tenantId';
public const OPTION_SCOPES = 'scopes';
public const OPTION_AUTH_SERVER_REQUEST_TIMEOUT = 'authServerRequestTimeout';
private const ACCESS_TOKEN_CACHE_KEY = 'em_http_client_access_token_%s';
private const DEFAULT_AUTH_SERVER_REQUEST_TIMEOUT = null;

public function __construct(
private CacheInterface $cache,
private HttpClientInterface $decoratedHttpClient,
private string $authServerHost,
private string $authServerTokenRequestPath,
private array $oauth2ClientCredentials,
) {}

public function stream($responses, float $timeout = null): ResponseStreamInterface
{
return $this->decoratedHttpClient->stream($responses, $timeout);
}

/**
* Requests an HTTP resource.
* @see HttpClientInterface::request()
*
* Two additional options are available:
* - EnvironmentManagementAwareHttpClient::OPTION_TENANT_ID: The tenant id to be used for the authentication
* - EnvironmentManagementAwareHttpClient::OPTION_SCOPES: The scopes to be used for the access token
*
* @throws CacheInvalidArgumentException
* @throws TransportExceptionInterface
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
if (!array_key_exists(self::OPTION_TENANT_ID, $options)) {
throw new InvalidArgumentException(sprintf(
'The following key is missing from the `options` parameter of the request: %s',
self::OPTION_TENANT_ID,
));
}

$tenantId = $options[self::OPTION_TENANT_ID];

$scopes = array_key_exists(self::OPTION_SCOPES, $options)
? is_array($options[self::OPTION_SCOPES]) ? $options[self::OPTION_SCOPES] : []
: [];

$authServerRequestTimeout = array_key_exists(self::OPTION_AUTH_SERVER_REQUEST_TIMEOUT, $options)
? (float) $options[self::OPTION_AUTH_SERVER_REQUEST_TIMEOUT]
: self::DEFAULT_AUTH_SERVER_REQUEST_TIMEOUT;

unset($options[self::OPTION_TENANT_ID]);
unset($options[self::OPTION_SCOPES]);
unset($options[self::OPTION_AUTH_SERVER_REQUEST_TIMEOUT]);

return $this->decoratedHttpClient->request($method, $url, array_merge($options, [
'headers' => [
'Authorization' => sprintf('Bearer %s', $this->getToken($tenantId, $scopes, $authServerRequestTimeout)),
],
]));
}

/**
* @throws CacheInvalidArgumentException
*/
private function getToken(string $tenantId, array $scopes = [], ?float $authServerRequestTimeout = null): string
{
return $this->cache->get(
sprintf(self::ACCESS_TOKEN_CACHE_KEY, $tenantId),
function(ItemInterface $item) use ($tenantId, $scopes, $authServerRequestTimeout) {
$oauth2Credentials = current(array_filter($this->oauth2ClientCredentials, function (array $credentials) use ($tenantId) {
return ($credentials['tenantId'] ?? null) === $tenantId;
}));

if (!$oauth2Credentials) {
throw new InvalidArgumentException(
sprintf(
'No OAuth2 credentials found for tenant %s',
$tenantId
)
);
}

if (!array_key_exists('clientId', $oauth2Credentials) || !array_key_exists('clientSecret', $oauth2Credentials)) {
throw new InvalidArgumentException(
sprintf(
'No OAuth2 client ID and/or client secret found for tenant %s',
$tenantId
)
);
}

$formFields = [
'grant_type' => 'client_credentials',
'client_id' => $oauth2Credentials['clientId'],
'client_secret' => $oauth2Credentials['clientSecret'],
'scope' => implode(' ', $scopes),
];

$formData = new FormDataPart($formFields);

$response = $this->decoratedHttpClient->request(
'POST',
$this->authServerHost . $this->authServerTokenRequestPath,
[
'body' => $formData->bodyToIterable(),
'headers' => $formData->getPreparedHeaders()->toArray(),
'timeout' => $authServerRequestTimeout,
],
);

$responsePayload = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR);

if ($response->getStatusCode() !== 200) {
throw new RuntimeException(
sprintf(
'Failed to get access token for tenant %s. Reason: %s',
$tenantId,
$responsePayload['message'] ?? 'Unknown'
)
);
}

$item->expiresAfter($responsePayload['expires_in']);

return $responsePayload['access_token'];
}
);
}
}
41 changes: 41 additions & 0 deletions Http/Client/EnvironmentManagementAwareHttpClientFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace OAT\Bundle\EnvironmentManagementClientBundle\Http\Client;

use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

class EnvironmentManagementAwareHttpClientFactory
{
public function __construct(
private CacheInterface $cache,
) {}

/**
* Creates a new HttpClient that decorates the one passed as parameter. The decorated HttpClient will
* - authenticates against the Environment Management's Auth Server based on the provided tenant id and scopes
* - caches the access tokens
*
* @param HttpClientInterface $decoratedHttpClient
* @param string $authServerHost
* @param string $authServerTokenRequestPath
* @param array $oauth2ClientCredentials
* @return EnvironmentManagementAwareHttpClient
*/
public function create(
HttpClientInterface $decoratedHttpClient,
string $authServerHost,
string $authServerTokenRequestPath,
array $oauth2ClientCredentials,
): EnvironmentManagementAwareHttpClient {
return new EnvironmentManagementAwareHttpClient(
$this->cache,
$decoratedHttpClient,
$authServerHost,
$authServerTokenRequestPath,
$oauth2ClientCredentials,
);
}
}
4 changes: 4 additions & 0 deletions Http/ResponseHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public function withAuthorizationDetailsMarker(
string $refreshTokenId,
string $userIdentifier = null,
string $userRole = null,
string $cookieDomain = null,
string $ltiToken = null,
string $mode = AuthorizationDetailsMarkerInterface::MODE_COOKIE,
): Response {
$psrResponse = $this->authorizationDetailsHeaderMarker->withAuthDetails(
Expand All @@ -39,6 +41,8 @@ public function withAuthorizationDetailsMarker(
$refreshTokenId,
$userIdentifier,
$userRole,
$cookieDomain,
$ltiToken,
$mode,
);

Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ Symfony Bundle wrapper for Environment Management Client library.
$ composer require oat-sa/bundle-em-client
```

## Environment variables

| Variable | Description | Default |
|------------------------------------|----------------------------------|---------|
| `LTI_GATEWAY_URL` | LTI Gateway URL | |
| `EM_AUTH_SERVER_GRPC_GATEWAY_HOST` | EM Auth Server gRPC Gateway Host | |
| `EM_CACHE_TTL` | Cache TTL in seconds | `300` |

## Tests

To run provided tests:
Expand Down
Loading

0 comments on commit 530973b

Please sign in to comment.