diff --git a/CHANGELOG.md b/CHANGELOG.md index 435fd91..cc58fdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Compatibility with PHP 8.4 +- NTLM authorization + ### Changed - Switch to Guzzle diff --git a/Makefile b/Makefile index 6045390..c5059de 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ cs: vendor .PHONY: mutation mutation: vendor - XDEBUG_MODE=coverage vendor/bin/infection --min-msi=97 --threads=4 --no-ansi + XDEBUG_MODE=coverage vendor/bin/infection --min-msi=96 --threads=4 --no-ansi .PHONY: phpstan phpstan: vendor diff --git a/docs/usage.md b/docs/usage.md index 100700f..54cc813 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,6 +1,7 @@ # Usage * [Initialisation](#initialisation) + * [NTLM authorisation](#ntlm-authorisation) * [Retrieve the JobRouter version](#retrieve-the-jobrouter-version) * [Sending requests](#sending-requests) * [Examples](#examples) @@ -43,10 +44,10 @@ $configuration = $configuration->withLifetime(30); $client = new RestClient($configuration); try { -// To authenticate against the configured JobRouter® installation the -// authenticate() method is called. As there can be errors during the -// authentication like a typo in the base URL or wrong credentials, embed the -// authenticate call into a try/catch block. + // To authenticate against the configured JobRouter® installation the + // authenticate() method is called. As there can be errors during the + // authentication like a typo in the base URL or wrong credentials, embed the + // authenticate call into a try/catch block. $client->authenticate(); } catch (ExceptionInterface $e) { // The thrown exception is by default an implementation of the @@ -69,6 +70,32 @@ lifetime of the token is exceeded you will get an authentication error. If this happens, you can call at any time the `authenticate()` method of the REST client again. You can call this also in advance to omit a timeout. +### NTLM authorisation + +Instead of configuring a combination of username/password like above, you can also use +NTLM authorisation on a Windows machine. + +```php + Have a look into the JobRouter Administration manual on how to configure +> JobRouter REST API with NTLM. ## Retrieve the JobRouter version diff --git a/src/Client/RestClient.php b/src/Client/RestClient.php index f7d5187..818b476 100644 --- a/src/Client/RestClient.php +++ b/src/Client/RestClient.php @@ -38,16 +38,22 @@ final class RestClient implements ClientInterface { private readonly Client $client; private readonly RouteContentTypeMapper $routeContentTypeMapper; + private readonly bool $useNtlm; private string $jobRouterVersion = ''; private string $authorisationToken = ''; public function __construct( private readonly ClientConfiguration $configuration, ) { + $this->useNtlm = $this->configuration->getUseNtlm(); + $stack = HandlerStack::create(); + // CurlHandler is necessary to use NTLM $stack->setHandler(new CurlHandler()); $stack->push((new UserAgentMiddleware())($this->configuration->getUserAgentAddition())); - $stack->push((new AuthorisationMiddleware())($this->authorisationToken)); + if (! $this->useNtlm) { + $stack->push((new AuthorisationMiddleware())($this->authorisationToken)); + } $options = [ ...$this->configuration->getClientOptions()->toArray(), @@ -57,6 +63,10 @@ public function __construct( 'synchronous' => true, ], ]; + if ($this->useNtlm) { + $options['auth'] = ['', '', 'ntlm']; + } + $this->client = new Client($options); $this->routeContentTypeMapper = new RouteContentTypeMapper(); @@ -70,6 +80,10 @@ public function __construct( */ public function authenticate(): self { + if ($this->useNtlm) { + throw AuthenticationException::fromActivatedNtlm(); + } + $this->authorisationToken = ''; $options = [ diff --git a/src/Configuration/ClientConfiguration.php b/src/Configuration/ClientConfiguration.php index 2c0aa86..4e4ffa2 100644 --- a/src/Configuration/ClientConfiguration.php +++ b/src/Configuration/ClientConfiguration.php @@ -28,6 +28,7 @@ final class ClientConfiguration private readonly JobRouterSystem $jobRouterSystem; private readonly string $username; private readonly string $password; + private readonly bool $useNtlm; private int $lifetime = self::DEFAULT_TOKEN_LIFETIME_IN_SECONDS; private string $userAgentAddition = ''; private ClientOptions $clientOptions; @@ -44,28 +45,38 @@ final class ClientConfiguration */ public function __construct( string $baseUrl, - string $username, + string $username = '', #[\SensitiveParameter] - string $password, + string $password = '', + bool $useNtlm = false, ) { - $this->mustNotHaveEmptyUsername($username); - $this->mustNotHaveEmptyPassword($password); + $this->mustNotHaveEmptyUsername($username, $useNtlm); + $this->mustNotHaveEmptyPassword($password, $useNtlm); $this->jobRouterSystem = new JobRouterSystem($baseUrl); $this->username = $username; $this->password = $password; + $this->useNtlm = $useNtlm; $this->clientOptions = new ClientOptions(); } - private function mustNotHaveEmptyUsername(string $username): void + private function mustNotHaveEmptyUsername(string $username, bool $useNtlm): void { + if ($useNtlm) { + return; + } + if ($username === '') { throw new InvalidConfigurationException('Username must not be empty!', 1565710532); } } - private function mustNotHaveEmptyPassword(string $password): void + private function mustNotHaveEmptyPassword(string $password, bool $useNtlm): void { + if ($useNtlm) { + return; + } + if ($password === '') { throw new InvalidConfigurationException('Password must not be empty!', 1565710533); } @@ -95,6 +106,14 @@ public function getPassword(): string return $this->password; } + /** + * @internal + */ + public function getUseNtlm(): bool + { + return $this->useNtlm; + } + /** * Return an instance with the specified lifetime * diff --git a/src/Exception/AuthenticationException.php b/src/Exception/AuthenticationException.php index bd0a5de..63974df 100644 --- a/src/Exception/AuthenticationException.php +++ b/src/Exception/AuthenticationException.php @@ -32,4 +32,15 @@ public static function fromFailedAuthentication( return new self($message, $code, $previous); } + + /** + * @internal + */ + public static function fromActivatedNtlm(): self + { + return new self( + 'The authenticate() method must not be used, as NTLM is activated', + 1724833066, + ); + } } diff --git a/tests/Unit/Client/RestClientTest.php b/tests/Unit/Client/RestClientTest.php index bf7c92b..de5c969 100644 --- a/tests/Unit/Client/RestClientTest.php +++ b/tests/Unit/Client/RestClientTest.php @@ -21,9 +21,11 @@ use JobRouter\AddOn\RestClient\Exception\HttpException; use JobRouter\AddOn\RestClient\Exception\RestClientException; use JobRouter\AddOn\RestClient\Resource\FileInterface; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; +#[CoversClass(RestClient::class)] final class RestClientTest extends TestCase { private const TEST_TOKEN = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqYXQiOjE1NzAyMjAwNzIsImp0aSI6IjhWMGtaSzJ5RzRxdGlhbjdGbGZTNUhPTGZaeGtZXC9obG1SVEV2VXIxVmwwPSIsImlzcyI6IkpvYlJvdXRlciIsIm5iZiI6MTU3MDIyMDA3MiwiZXhwIjoxNTcwMjIwMTAyLCJkYXRhIjp7InVzZXJuYW1lIjoicmVzdCJ9fQ.cbAyj36f9MhAwOMzlTEheRkHhuuIEOeb1Uy8i0KfUhU'; @@ -567,4 +569,41 @@ public function requestUsingFileInterfaceIsHandledCorrectly(): void \unlink($filePath); } + + #[Test] + public function callAuthenticateWithActivatedNtlmThrowsException(): void + { + $this->expectException(AuthenticationException::class); + $this->expectExceptionCode(1724833066); + + $configuration = new ClientConfiguration( + self::$server->getServerRoot() . '/', + useNtlm: true, + ); + + $restClient = new RestClient($configuration); + $restClient->authenticate(); + } + + #[Test] + public function ntlmHeaderIsSetWhenNtlmIsActivated(): void + { + $configuration = new ClientConfiguration( + self::$server->getServerRoot() . '/', + useNtlm: true, + ); + $restClient = new RestClient($configuration); + + self::$server->setResponseOfPath( + '/api/rest/v2/some/route', + new Response('The response of some/route'), + ); + + $restClient->request('GET', '/some/route'); + $requestHeaders = self::$server->getLastRequest()?->getHeaders() ?? []; + + self::assertArrayNotHasKey('X-Jobrouter-Authorization', $requestHeaders); + self::assertArrayHasKey('Authorization', $requestHeaders); + self::assertSame('NTLM TlRMTVNTUAABAAAABoIIAAAAAAAAAAAAAAAAAAAAAAA=', $requestHeaders['Authorization']); + } } diff --git a/tests/Unit/Configuration/ClientConfigurationTest.php b/tests/Unit/Configuration/ClientConfigurationTest.php index 1afa4e6..aa52ec9 100644 --- a/tests/Unit/Configuration/ClientConfigurationTest.php +++ b/tests/Unit/Configuration/ClientConfigurationTest.php @@ -15,10 +15,12 @@ use JobRouter\AddOn\RestClient\Configuration\ClientConfiguration; use JobRouter\AddOn\RestClient\Configuration\ClientOptions; use JobRouter\AddOn\RestClient\Exception\InvalidConfigurationException; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -class ClientConfigurationTest extends TestCase +#[CoversClass(ClientConfiguration::class)] +final class ClientConfigurationTest extends TestCase { private ClientConfiguration $subject; @@ -82,6 +84,27 @@ public function getPasswordReturnsPreviouslySetPassword(): void self::assertSame('fake_password', $actual); } + #[Test] + public function getUseNtlmReturnsFalseWhenNotSetExplicitly(): void + { + $actual = $this->subject->getUseNtlm(); + + self::assertFalse($actual); + } + + #[Test] + public function getUseNtlmReturnsPreviouslySetValue(): void + { + $subject = new ClientConfiguration( + 'https://example.org/', + useNtlm: true, + ); + + $actual = $subject->getUseNtlm(); + + self::assertTrue($actual); + } + #[Test] public function getLifetimeReturnsTheDefaultLifetime(): void { diff --git a/tests/Unit/Exception/AuthenticationExceptionTest.php b/tests/Unit/Exception/AuthenticationExceptionTest.php index 84e4915..5297aa9 100644 --- a/tests/Unit/Exception/AuthenticationExceptionTest.php +++ b/tests/Unit/Exception/AuthenticationExceptionTest.php @@ -14,10 +14,12 @@ use JobRouter\AddOn\RestClient\Configuration\ClientConfiguration; use JobRouter\AddOn\RestClient\Exception\AuthenticationException; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -class AuthenticationExceptionTest extends TestCase +#[CoversClass(AuthenticationException::class)] +final class AuthenticationExceptionTest extends TestCase { #[Test] public function fromFailedAuthenticationReturnsInstantiatedExceptionWithAllArgumentsCorrectly(): void @@ -60,4 +62,17 @@ public function fromFailedAuthenticationReturnsInstantiatedExceptionWithOnlyRequ self::assertSame(0, $actual->getCode()); self::assertNull($actual->getPrevious()); } + + #[Test] + public function fromActivatedNtlm(): void + { + $actual = AuthenticationException::fromActivatedNtlm(); + + self::assertInstanceOf(AuthenticationException::class, $actual); + self::assertSame( + 'The authenticate() method must not be used, as NTLM is activated', + $actual->getMessage(), + ); + self::assertSame(1724833066, $actual->getCode()); + } }