Skip to content
Draft
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
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"php-http/discovery": "^1.0",
"psr/http-client-implementation": "^1.0",
"php-http/message-factory": "^1.0",
"psr/http-message-implementation": "^1.0"
"psr/http-message-implementation": "^1.0",
"private-packagist/oidc-identities": "^1.0.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.0",
Expand Down
15 changes: 14 additions & 1 deletion src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,27 @@
use PrivatePackagist\ApiClient\HttpClient\Plugin\ExceptionThrower;
use PrivatePackagist\ApiClient\HttpClient\Plugin\PathPrepend;
use PrivatePackagist\ApiClient\HttpClient\Plugin\RequestSignature;
use PrivatePackagist\ApiClient\HttpClient\Plugin\TrustedPublishingTokenExchange;
use PrivatePackagist\OIDC\Identities\TokenGenerator;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;

class Client
{
/** @var HttpPluginClientBuilder */
private $httpClientBuilder;
/** @var ResponseMediator */
private $responseMediator;
/** @var LoggerInterface */
private $logger;

/** @param string $privatePackagistUrl */
public function __construct(?HttpPluginClientBuilder $httpClientBuilder = null, $privatePackagistUrl = null, ?ResponseMediator $responseMediator = null)
public function __construct(?HttpPluginClientBuilder $httpClientBuilder = null, $privatePackagistUrl = null, ?ResponseMediator $responseMediator = null, ?LoggerInterface $logger = null)
{
$this->httpClientBuilder = $builder = $httpClientBuilder ?: new HttpPluginClientBuilder();
$privatePackagistUrl = $privatePackagistUrl ? : 'https://packagist.com';
$this->responseMediator = $responseMediator ? : new ResponseMediator();
$this->logger = $logger ? : new NullLogger();

$builder->addPlugin(new Plugin\AddHostPlugin(Psr17FactoryDiscovery::findUriFactory()->createUri($privatePackagistUrl)));
$builder->addPlugin(new PathPrepend('/api'));
Expand Down Expand Up @@ -58,6 +65,12 @@ public function authenticate(
$this->httpClientBuilder->addPlugin(new RequestSignature($key, $secret));
}

public function authenticateWithTrustedPublishing(string $organizationUrlName, string $packageName)
{
$this->httpClientBuilder->removePlugin(TrustedPublishingTokenExchange::class);
$this->httpClientBuilder->addPlugin(new TrustedPublishingTokenExchange($organizationUrlName, $packageName, $this->getHttpClientBuilder(), new TokenGenerator($this->logger, $this->getHttpClientBuilder()->getHttpClientWithoutPlugins())));
}

public function credentials()
{
return new Api\Credentials($this, $this->responseMediator);
Expand Down
9 changes: 9 additions & 0 deletions src/HttpClient/HttpPluginClientBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,13 @@ public function getHttpClient()

return $this->pluginClient;
}

public function getHttpClientWithoutPlugins(): HttpMethodsClient
{
return new HttpMethodsClient(
$this->httpClient,
$this->requestFactory,
$this->streamFactory
);
}
}
65 changes: 65 additions & 0 deletions src/HttpClient/Plugin/TrustedPublishingTokenExchange.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php declare(strict_types=1);

/**
* (c) Packagist Conductors GmbH <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace PrivatePackagist\ApiClient\HttpClient\Plugin;

use Http\Client\Common\Plugin;
use PrivatePackagist\ApiClient\HttpClient\HttpPluginClientBuilder;
use PrivatePackagist\OIDC\Identities\TokenGeneratorInterface;
use Psr\Http\Message\RequestInterface;

/**
* @internal
*/
final class TrustedPublishingTokenExchange implements Plugin
{
use Plugin\VersionBridgePlugin;

/** @var string */
private $organizationUrlName;
/** @var string */
private $packageName;
/** @var HttpPluginClientBuilder $httpPluginClientBuilder */
private $httpPluginClientBuilder;
/** @var TokenGeneratorInterface */
private $tokenGenerator;

public function __construct(string $organizationUrlName, string $packageName, HttpPluginClientBuilder $httpPluginClientBuilder, TokenGeneratorInterface $tokenGenerator)
{
$this->organizationUrlName = $organizationUrlName;
$this->packageName = $packageName;
$this->httpPluginClientBuilder = $httpPluginClientBuilder;
$this->tokenGenerator = $tokenGenerator;
}

protected function doHandleRequest(RequestInterface $request, callable $next, callable $first)
{
$this->httpPluginClientBuilder->removePlugin(self::class);

$privatePackagistHttpclient = $this->httpPluginClientBuilder->getHttpClient();
$audience = json_decode((string) $privatePackagistHttpclient->get('/oidc/audience')->getBody(), true);
if (!isset($audience['audience'])) {
throw new \RuntimeException('Unable to get audience');
}

$token = $this->tokenGenerator->generate($audience['audience']);
if (!$token) {
throw new \RuntimeException('Unable to generate OIDC token');
}

$apiCredentials = json_decode((string) $privatePackagistHttpclient->post('/oidc/token-exchange/' . $this->organizationUrlName . '/' . $this->packageName, ['Authorization' => 'Bearer ' . $token->token])->getBody(), true);
if (!isset($apiCredentials['key'], $apiCredentials['secret'])) {
throw new \RuntimeException('Unable to exchange token');
}

$this->httpPluginClientBuilder->addPlugin($requestSignature = new RequestSignature($apiCredentials['key'], $apiCredentials['secret']));

return $requestSignature->handleRequest($request, $next, $first);
}
}
12 changes: 1 addition & 11 deletions tests/HttpClient/Plugin/PathPrependTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,17 @@
namespace PrivatePackagist\ApiClient\HttpClient\Plugin;

use GuzzleHttp\Psr7\Request;
use Http\Promise\FulfilledPromise;
use PHPUnit\Framework\TestCase;

class PathPrependTest extends TestCase
class PathPrependTest extends PluginTestCase
{
/** @var PathPrepend */
private $plugin;
private $next;
private $first;

protected function setUp(): void
{
parent::setUp();

$this->plugin = new PathPrepend('/api');
$this->next = function (Request $request) {
return new FulfilledPromise($request);
};
$this->first = function () {
throw new \RuntimeException('Did not expect plugin to call first');
};
}

/**
Expand Down
34 changes: 34 additions & 0 deletions tests/HttpClient/Plugin/PluginTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php declare(strict_types=1);

/**
* (c) Packagist Conductors GmbH <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace PrivatePackagist\ApiClient\HttpClient\Plugin;

use GuzzleHttp\Psr7\Request;
use Http\Promise\FulfilledPromise;
use PHPUnit\Framework\TestCase;

class PluginTestCase extends TestCase
{
/** @var \Closure */
protected $next;
/** @var \Closure */
protected $first;

protected function setUp(): void
{
parent::setUp();

$this->next = function (Request $request) {
return new FulfilledPromise($request);
};
$this->first = function () {
throw new \RuntimeException('Did not expect plugin to call first');
};
}
}
14 changes: 3 additions & 11 deletions tests/HttpClient/Plugin/RequestSignatureTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,26 @@
namespace PrivatePackagist\ApiClient\HttpClient\Plugin;

use GuzzleHttp\Psr7\Request;
use Http\Promise\FulfilledPromise;
use PHPUnit\Framework\TestCase;

class RequestSignatureTest extends TestCase
class RequestSignatureTest extends PluginTestCase
{
/** @var RequestSignature */
private $plugin;
private $next;
private $first;
private $key;
private $secret;
private $timestamp;
private $nonce;

protected function setUp(): void
{
parent::setUp();

$this->key = 'token';
$this->secret = 'secret';
$this->timestamp = 1518721253;
$this->nonce = '78b9869e96cf58b5902154e0228f8576f042e5ac';
$this->plugin = new RequestSignatureMock($this->key, $this->secret);
$this->plugin->init($this->timestamp, $this->nonce);
$this->next = function (Request $request) {
return new FulfilledPromise($request);
};
$this->first = function () {
throw new \RuntimeException('Did not expect plugin to call first');
};
}

public function testPrefixRequestPath()
Expand Down
79 changes: 79 additions & 0 deletions tests/HttpClient/Plugin/TrustedPublishingTokenExchangeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php declare(strict_types=1);

/**
* (c) Packagist Conductors GmbH <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace PrivatePackagist\ApiClient\HttpClient\Plugin;

use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Http\Mock\Client;
use PHPUnit\Framework\MockObject\MockObject;
use PrivatePackagist\ApiClient\HttpClient\HttpPluginClientBuilder;
use PrivatePackagist\OIDC\Identities\Token;
use PrivatePackagist\OIDC\Identities\TokenGeneratorInterface;

class TrustedPublishingTokenExchangeTest extends PluginTestCase
{
/** @var TrustedPublishingTokenExchange */
private $plugin;
/** @var Client */
private $httpClient;
/** @var TokenGeneratorInterface&MockObject */
private $tokenGenerator;

protected function setUp(): void
{
parent::setUp();

$this->plugin = new TrustedPublishingTokenExchange(
'organization',
'acme/package',
new HttpPluginClientBuilder($this->httpClient = new Client()),
$this->tokenGenerator = $this->createMock(TokenGeneratorInterface::class)
);
}

public function testTokenExchange(): void
{
$request = new Request('GET', '/api/packages/acme/package');

$this->tokenGenerator
->expects($this->once())
->method('generate')
->with($this->identicalTo('test'))
->willReturn(Token::fromTokenString('test.test.test'));

$this->httpClient->addResponse(new Response(200, [], json_encode(['audience' => 'test'])));
$this->httpClient->addResponse(new Response(200, [], json_encode(['key' => 'key', 'secret' => 'secret'])));

$this->plugin->handleRequest($request, $this->next, $this->first);

$requests = $this->httpClient->getRequests();
$this->assertCount(2, $requests);
$this->assertSame('/oidc/audience', (string) $requests[0]->getUri());
$this->assertSame('/oidc/token-exchange/organization/acme/package', (string) $requests[1]->getUri());
}

public function testNoTokenGenerated(): void
{
$request = new Request('GET', '/api/packages/acme/package');

$this->tokenGenerator
->expects($this->once())
->method('generate')
->with($this->identicalTo('test'))
->willReturn(null);

$this->httpClient->addResponse(new Response(200, [], json_encode(['audience' => 'test'])));

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Unable to generate OIDC token');

$this->plugin->handleRequest($request, $this->next, $this->first);
}
}