Skip to content

Commit

Permalink
feat: implement NTLM support
Browse files Browse the repository at this point in the history
  • Loading branch information
brotkrueml committed Aug 28, 2024
1 parent f36b709 commit a24e3f7
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 14 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 31 additions & 4 deletions docs/usage.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
<?php
use JobRouter\AddOn\RestClient\Client\RestClient;
use JobRouter\AddOn\RestClient\Configuration\ClientConfiguration;
use JobRouter\AddOn\RestClient\Exception\ExceptionInterface;

require_once 'vendor/autoload.php';

$configuration = new ClientConfiguration(
'https://example.org/jobrouter/',
useNtlm: true,
);

$client = new RestClient($configuration);

// Do NOT call the authenticate() method, as with NTLM the user is authenticated
// automatically!
```

> Have a look into the JobRouter Administration manual on how to configure
> JobRouter REST API with NTLM.
## Retrieve the JobRouter version

Expand Down
16 changes: 15 additions & 1 deletion src/Client/RestClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Check warning on line 52 in src/Client/RestClient.php

View workflow job for this annotation

GitHub Actions / Code Quality

Escaped Mutant for Mutator "MethodCallRemoval": @@ @@ { $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())); if (!$this->useNtlm) { $stack->push((new AuthorisationMiddleware())($this->authorisationToken));
$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(),
Expand All @@ -57,6 +63,10 @@ public function __construct(
'synchronous' => true,

Check warning on line 63 in src/Client/RestClient.php

View workflow job for this annotation

GitHub Actions / Code Quality

Escaped Mutant for Mutator "TrueValue": @@ @@ if (!$this->useNtlm) { $stack->push((new AuthorisationMiddleware())($this->authorisationToken)); } - $options = [...$this->configuration->getClientOptions()->toArray(), ...['base_uri' => $configuration->getJobRouterSystem()->getBaseUrl(), 'handler' => $stack, 'synchronous' => true]]; + $options = [...$this->configuration->getClientOptions()->toArray(), ...['base_uri' => $configuration->getJobRouterSystem()->getBaseUrl(), 'handler' => $stack, 'synchronous' => false]]; if ($this->useNtlm) { $options['auth'] = ['', '', 'ntlm']; }
],
];
if ($this->useNtlm) {
$options['auth'] = ['', '', 'ntlm'];
}

$this->client = new Client($options);

$this->routeContentTypeMapper = new RouteContentTypeMapper();
Expand All @@ -70,6 +80,10 @@ public function __construct(
*/
public function authenticate(): self
{
if ($this->useNtlm) {
throw AuthenticationException::fromActivatedNtlm();
}

$this->authorisationToken = '';

$options = [
Expand Down
31 changes: 25 additions & 6 deletions src/Configuration/ClientConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
Expand Down Expand Up @@ -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
*
Expand Down
11 changes: 11 additions & 0 deletions src/Exception/AuthenticationException.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
}
39 changes: 39 additions & 0 deletions tests/Unit/Client/RestClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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']);
}
}
25 changes: 24 additions & 1 deletion tests/Unit/Configuration/ClientConfigurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
{
Expand Down
17 changes: 16 additions & 1 deletion tests/Unit/Exception/AuthenticationExceptionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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());
}
}

0 comments on commit a24e3f7

Please sign in to comment.