diff --git a/doc/setup.rst b/doc/setup.rst index 96fa2957..503a0c74 100644 --- a/doc/setup.rst +++ b/doc/setup.rst @@ -13,31 +13,47 @@ Only the ``authUrl`` is mandatory to create the client. But you will have to pro credentials to each service you create. So it is recommended to provide them when creating the client which would propagate these options to each service. +Authenticate +------------ + There are different ways to provide the authentication credentials. See the :doc:`services/identity/v3/tokens` section for the full list of options. You should provide credentials to the ``OpenStack`` constructor as an array the same way you provide options to ``generateToken`` method of the ``Identity`` service. -Authenticate with username --------------------------- +By username +~~~~~~~~~~~ The most common way to authenticate is using the username and password of the user. You should also provide the Domain ID as usernames will not be unique across an entire OpenStack installation .. sample:: Setup/username.php -Authenticate with user ID -------------------------- +By user ID +~~~~~~~~~~ .. sample:: Setup/user_id.php -Authenticate application credential ID --------------------------------------- +By application credential ID +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. sample:: Setup/application_credential_id.php -Authenticate using token ID ---------------------------- +By token ID +~~~~~~~~~~~ If you already have a valid token, you can use it to authenticate. .. sample:: Setup/token_id.php + +Other options +------------- + +For production environments it is recommended to decrease error reporting not to expose sensitive information. It can be done +by setting the ``errorVerbosity`` key to ``0`` in the options array. It is set to 2 by default. + +.. code-block:: php + + $openstack = new OpenStack\OpenStack([ + 'errorVerbosity' => 0, + // other options + ]); \ No newline at end of file diff --git a/src/Common/Auth/IdentityService.php b/src/Common/Auth/IdentityService.php index 949b1103..c9f86849 100644 --- a/src/Common/Auth/IdentityService.php +++ b/src/Common/Auth/IdentityService.php @@ -9,7 +9,7 @@ interface IdentityService /** * Authenticates and retrieves back a token and catalog. * - * @return array The FIRST key is {@see Token} instance, the SECOND key is a {@see Catalog} instance + * @return array{0: \OpenStack\Common\Auth\Token, 1: string} The FIRST key is {@see Token} instance, the SECOND key is a URL of the service */ public function authenticate(array $options): array; } diff --git a/src/Common/Error/Builder.php b/src/Common/Error/Builder.php index 796415a2..7de83710 100644 --- a/src/Common/Error/Builder.php +++ b/src/Common/Error/Builder.php @@ -66,30 +66,41 @@ private function linkIsValid(string $link): bool /** * @codeCoverageIgnore */ - public function str(MessageInterface $message): string + public function str(MessageInterface $message, int $verbosity = 0): string { if ($message instanceof RequestInterface) { - $msg = trim($message->getMethod().' ' - .$message->getRequestTarget()) - .' HTTP/'.$message->getProtocolVersion(); + $msg = trim($message->getMethod().' '.$message->getRequestTarget()); + $msg .= ' HTTP/'.$message->getProtocolVersion(); if (!$message->hasHeader('host')) { $msg .= "\r\nHost: ".$message->getUri()->getHost(); } - } elseif ($message instanceof ResponseInterface) { - $msg = 'HTTP/'.$message->getProtocolVersion().' ' - .$message->getStatusCode().' ' - .$message->getReasonPhrase(); + } else { + if ($message instanceof ResponseInterface) { + $msg = 'HTTP/'.$message->getProtocolVersion().' ' + .$message->getStatusCode().' ' + .$message->getReasonPhrase(); + } else { + throw new \InvalidArgumentException('Unknown message type'); + } + } + + if ($verbosity < 1) { + return $msg; } foreach ($message->getHeaders() as $name => $values) { $msg .= "\r\n{$name}: ".implode(', ', $values); } + if ($verbosity < 2) { + return $msg; + } + if (ini_get('memory_limit') < 0 || $message->getBody()->getSize() < ini_get('memory_limit')) { $msg .= "\r\n\r\n".$message->getBody(); } - return $msg; + return trim($msg); } /** @@ -98,7 +109,7 @@ public function str(MessageInterface $message): string * @param RequestInterface $request The faulty request * @param ResponseInterface $response The error-filled response */ - public function httpError(RequestInterface $request, ResponseInterface $response): BadResponseError + public function httpError(RequestInterface $request, ResponseInterface $response, int $verbosity = 0): BadResponseError { $message = $this->header('HTTP Error'); @@ -109,10 +120,10 @@ public function httpError(RequestInterface $request, ResponseInterface $response ); $message .= $this->header('Request'); - $message .= trim($this->str($request)).PHP_EOL.PHP_EOL; + $message .= $this->str($request, $verbosity).PHP_EOL.PHP_EOL; $message .= $this->header('Response'); - $message .= trim($this->str($response)).PHP_EOL.PHP_EOL; + $message .= $this->str($response, $verbosity).PHP_EOL.PHP_EOL; $message .= $this->header('Further information'); $message .= $this->getStatusCodeMessage($response->getStatusCode()); diff --git a/src/Common/Service/Builder.php b/src/Common/Service/Builder.php index e9cb2559..f63eefc1 100644 --- a/src/Common/Service/Builder.php +++ b/src/Common/Service/Builder.php @@ -7,11 +7,8 @@ use GuzzleHttp\Client; use GuzzleHttp\ClientInterface; use GuzzleHttp\HandlerStack; -use GuzzleHttp\Middleware as GuzzleMiddleware; use OpenStack\Common\Auth\IdentityService; -use OpenStack\Common\Auth\Token; use OpenStack\Common\Transport\HandlerStackFactory; -use OpenStack\Common\Transport\Middleware; use OpenStack\Common\Transport\Utils; /** @@ -88,33 +85,18 @@ private function stockHttpClient(array &$options, string $serviceName): void if (!isset($options['httpClient']) || !($options['httpClient'] instanceof ClientInterface)) { if (false !== stripos($serviceName, 'identity')) { $baseUrl = $options['authUrl']; - $stack = $this->getStack($options['authHandler']); + $token = null; } else { - [$token, $baseUrl] = $options['identityService']->authenticate($options); - $stack = $this->getStack($options['authHandler'], $token); + [$token, $baseUrl] = $options['identityService']->authenticate($options); } + $stack = HandlerStackFactory::createWithOptions(array_merge($options, ['token' => $token])); $microVersion = $options['microVersion'] ?? null; - $this->addDebugMiddleware($options, $stack); - $options['httpClient'] = $this->httpClient($baseUrl, $stack, $options['catalogType'], $microVersion); } } - /** - * @codeCoverageIgnore - */ - private function addDebugMiddleware(array $options, HandlerStack &$stack): void - { - if (!empty($options['debugLog']) - && !empty($options['logger']) - && !empty($options['messageFormatter']) - ) { - $stack->push(GuzzleMiddleware::log($options['logger'], $options['messageFormatter'])); - } - } - /** * @codeCoverageIgnore */ @@ -127,14 +109,6 @@ private function stockAuthHandler(array &$options): void } } - private function getStack(callable $authHandler, Token $token = null): HandlerStack - { - $stack = HandlerStackFactory::create(); - $stack->push(Middleware::authHandler($authHandler, $token)); - - return $stack; - } - private function httpClient(string $baseUrl, HandlerStack $stack, string $serviceType = null, string $microVersion = null): ClientInterface { $clientOptions = [ diff --git a/src/Common/Transport/HandlerStack.php b/src/Common/Transport/HandlerStack.php index da28157b..fd500be5 100644 --- a/src/Common/Transport/HandlerStack.php +++ b/src/Common/Transport/HandlerStack.php @@ -7,6 +7,9 @@ */ class HandlerStack { + /** + * @deprecated use \OpenStack\Common\Transport\HandlerStackFactory::createWithOptions instead + */ public static function create(callable $handler = null): \GuzzleHttp\HandlerStack { return HandlerStackFactory::create($handler); diff --git a/src/Common/Transport/HandlerStackFactory.php b/src/Common/Transport/HandlerStackFactory.php index a0579a32..a2324d58 100644 --- a/src/Common/Transport/HandlerStackFactory.php +++ b/src/Common/Transport/HandlerStackFactory.php @@ -5,10 +5,14 @@ namespace OpenStack\Common\Transport; use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware as GuzzleMiddleware; use GuzzleHttp\Utils; class HandlerStackFactory { + /** + * @deprecated use \OpenStack\Common\Transport\HandlerStackFactory::createWithOptions instead + */ public static function create(callable $handler = null): HandlerStack { $stack = new HandlerStack($handler ?: Utils::chooseHandler()); @@ -17,4 +21,38 @@ public static function create(callable $handler = null): HandlerStack return $stack; } + + /** + * Creates a new HandlerStack with the given options. + * + * @param array{ + * handler: callable, + * authHandler: callable, + * token: \OpenStack\Common\Auth\Token, + * errorVerbosity: int, + * debugLog: bool, + * logger: \Psr\Log\LoggerInterface, + * messageFormatter: \GuzzleHttp\MessageFormatter + * } $options + */ + public static function createWithOptions(array $options): HandlerStack + { + $stack = new HandlerStack($options['handler'] ?? Utils::chooseHandler()); + $stack->push(Middleware::httpErrors($options['errorVerbosity'] ?? 0), 'http_errors'); + $stack->push(GuzzleMiddleware::prepareBody(), 'prepare_body'); + + if (!empty($options['authHandler'])) { + $stack->push(Middleware::authHandler($options['authHandler'], $options['token'] ?? null)); + } + + if (!empty($options['debugLog']) + && !empty($options['logger']) + && !empty($options['messageFormatter']) + ) { + $logMiddleware = GuzzleMiddleware::log($options['logger'], $options['messageFormatter']); + $stack->push($logMiddleware, 'logger'); + } + + return $stack; + } } diff --git a/src/Common/Transport/Middleware.php b/src/Common/Transport/Middleware.php index 86f701ed..d11667ef 100644 --- a/src/Common/Transport/Middleware.php +++ b/src/Common/Transport/Middleware.php @@ -15,16 +15,16 @@ final class Middleware { - public static function httpErrors(): callable + public static function httpErrors(int $verbosity = 0): callable { - return function (callable $handler) { - return function ($request, array $options) use ($handler) { + return function (callable $handler) use ($verbosity) { + return function ($request, array $options) use ($handler, $verbosity) { return $handler($request, $options)->then( - function (ResponseInterface $response) use ($request) { + function (ResponseInterface $response) use ($request, $verbosity) { if ($response->getStatusCode() < 400) { return $response; } - throw (new Builder())->httpError($request, $response); + throw (new Builder())->httpError($request, $response, $verbosity); } ); }; diff --git a/src/Identity/v3/Service.php b/src/Identity/v3/Service.php index 1f67f28b..5760436f 100644 --- a/src/Identity/v3/Service.php +++ b/src/Identity/v3/Service.php @@ -45,7 +45,7 @@ public function authenticate(array $options): array $name = $options['catalogName']; $type = $options['catalogType']; $region = $options['region']; - $interface = isset($options['interface']) ? $options['interface'] : Enum::INTERFACE_PUBLIC; + $interface = $options['interface'] ?? Enum::INTERFACE_PUBLIC; if ($baseUrl = $token->catalog->getServiceUrl($name, $type, $region, $interface)) { return [$token, $baseUrl]; diff --git a/src/OpenStack.php b/src/OpenStack.php index 05ba65ac..4eecd642 100644 --- a/src/OpenStack.php +++ b/src/OpenStack.php @@ -5,7 +5,6 @@ namespace OpenStack; use GuzzleHttp\Client; -use GuzzleHttp\Middleware as GuzzleMiddleware; use OpenStack\Common\Service\Builder; use OpenStack\Common\Transport\HandlerStackFactory; use OpenStack\Common\Transport\Utils; @@ -36,6 +35,9 @@ class OpenStack */ public function __construct(array $options = [], Builder $builder = null) { + $defaults = ['errorVerbosity' => 2]; + $options = array_merge($defaults, $options); + if (!isset($options['identityService'])) { $options['identityService'] = $this->getDefaultIdentityService($options); } @@ -49,15 +51,7 @@ private function getDefaultIdentityService(array $options): Service throw new \InvalidArgumentException("'authUrl' is a required option"); } - $stack = HandlerStackFactory::create(); - - if (!empty($options['debugLog']) - && !empty($options['logger']) - && !empty($options['messageFormatter']) - ) { - $logMiddleware = GuzzleMiddleware::log($options['logger'], $options['messageFormatter']); - $stack->push($logMiddleware, 'logger'); - } + $stack = HandlerStackFactory::createWithOptions(array_merge($options, ['token' => null])); $clientOptions = [ 'base_uri' => Utils::normalizeUrl($options['authUrl']), diff --git a/tests/sample/Compute/v2/TestCase.php b/tests/sample/Compute/v2/TestCase.php index ee57ecaa..8f186918 100644 --- a/tests/sample/Compute/v2/TestCase.php +++ b/tests/sample/Compute/v2/TestCase.php @@ -68,7 +68,7 @@ protected function createServer(): Server ] ); - $server->waitUntilActive(120); + $server->waitUntilActive(300); $this->assertEquals('ACTIVE', $server->status); return $server; diff --git a/tests/sample/Identity/v3/TokenTest.php b/tests/sample/Identity/v3/TokenTest.php index 85e8ce8c..692dc542 100644 --- a/tests/sample/Identity/v3/TokenTest.php +++ b/tests/sample/Identity/v3/TokenTest.php @@ -2,7 +2,10 @@ namespace OpenStack\Sample\Identity\v3; +use GuzzleHttp\Exception\ClientException; +use OpenStack\Common\Error\BadResponseError; use OpenStack\Identity\v3\Models\Token; +use OpenStack\OpenStack; class TokenTest extends TestCase { @@ -73,4 +76,37 @@ public function testRevoke(): void $this->assertFalse($token->validate()); } + public function testInvalidPassword() + { + $options = $this->getAuthOpts(); + $password = $options['user']['password'] . $this->randomStr(); + $options['user']['id'] = $password; + $options['user']['password'] = $password; + + $openstack = new OpenStack($options); + $this->expectException(BadResponseError::class); + + $openstack->objectStoreV1(); + } + + public function testInvalidPasswordHidesPassword() + { + $options = $this->getAuthOpts(); + + $password = $options['user']['password'] . $this->randomStr(); + $options['user']['id'] = $password; + $options['user']['password'] = $password; + + $openstack = new OpenStack(array_merge($options, ['errorVerbosity' => 0])); + $this->expectException(BadResponseError::class); + + try { + $openstack->objectStoreV1(); + } catch (BadResponseError $e) { + $this->assertStringNotContainsString($password, $e->getMessage()); + + throw $e; + } + + } } \ No newline at end of file diff --git a/tests/unit/Common/Error/BuilderTest.php b/tests/unit/Common/Error/BuilderTest.php index 7d44eb82..51ce95fb 100644 --- a/tests/unit/Common/Error/BuilderTest.php +++ b/tests/unit/Common/Error/BuilderTest.php @@ -27,13 +27,16 @@ public function test_it_injects_client() self::assertInstanceOf(Builder::class, new Builder($this->client->reveal())); } - public function test_it_builds_http_errors() + /** + * @dataProvider verbosityProvider + */ + public function test_it_builds_http_errors(int $verbosity) { $request = new Request('POST', '/servers'); $response = new Response(400, [], Utils::streamFor('Invalid parameters')); - $requestStr = trim($this->builder->str($request)); - $responseStr = trim($this->builder->str($response)); + $requestStr = $this->builder->str($request, $verbosity); + $responseStr = $this->builder->str($response, $verbosity); $errorMessage = <<setRequest($request); $e->setResponse($response); - self::assertEquals($e, $this->builder->httpError($request, $response)); + self::assertEquals($e, $this->builder->httpError($request, $response, $verbosity)); + } + + /** + * Provides different verbosity levels. + */ + public function verbosityProvider(): array + { + return [ + [0], + [1], + [2], + ]; } public function test_it_builds_user_input_errors() diff --git a/tests/unit/Identity/v3/ServiceTest.php b/tests/unit/Identity/v3/ServiceTest.php index 0719838f..bf82c460 100644 --- a/tests/unit/Identity/v3/ServiceTest.php +++ b/tests/unit/Identity/v3/ServiceTest.php @@ -10,7 +10,6 @@ use OpenStack\Identity\v3\Service; use OpenStack\Test\TestCase; use Prophecy\Argument; -use Psr\Http\Message\ResponseInterface; class ServiceTest extends TestCase {