From 194ddf58b6942892fc9000a01476e99a6aafdc78 Mon Sep 17 00:00:00 2001 From: Chad Sikorra Date: Mon, 25 May 2026 14:01:56 -0400 Subject: [PATCH] Refactor ServerProtocolHandlerFactory so it's constructing less of its dependencies. Moved more of it to the container. --- src/FreeDSx/Ldap/Container.php | 64 +++- .../Factory/ProtocolHandlerProvider.php | 165 ++++++++++ .../ProtocolHandlerProviderInterface.php | 32 ++ .../Factory/ServerProtocolHandlerFactory.php | 190 +----------- .../Middleware/Pipeline/HandlerInvoker.php | 6 +- .../PasswordPolicyComponentFactory.php | 89 ++++++ .../Ldap/Server/ServerProtocolFactory.php | 26 +- .../Factory/ProtocolHandlerProviderTest.php | 281 ++++++++++++++++++ .../ServerProtocolHandlerFactoryTest.php | 236 ++------------- .../Protocol/ServerProtocolHandlerTest.php | 12 +- .../unit/Server/ServerProtocolFactoryTest.php | 57 +++- 11 files changed, 747 insertions(+), 411 deletions(-) create mode 100644 src/FreeDSx/Ldap/Protocol/Factory/ProtocolHandlerProvider.php create mode 100644 src/FreeDSx/Ldap/Protocol/Factory/ProtocolHandlerProviderInterface.php create mode 100644 src/FreeDSx/Ldap/Server/PasswordPolicy/PasswordPolicyComponentFactory.php create mode 100644 tests/unit/Protocol/Factory/ProtocolHandlerProviderTest.php diff --git a/src/FreeDSx/Ldap/Container.php b/src/FreeDSx/Ldap/Container.php index 020f1cfe..1f02560f 100644 --- a/src/FreeDSx/Ldap/Container.php +++ b/src/FreeDSx/Ldap/Container.php @@ -29,8 +29,11 @@ use FreeDSx\Ldap\Server\PasswordPolicy\Constraint\PasswordChangeConstraintChain; use FreeDSx\Ldap\Server\PasswordPolicy\Constraint\QualityConstraint; use FreeDSx\Ldap\Server\PasswordPolicy\Constraint\SafeModifyConstraint; +use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyComponentFactory; use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyEngine; use FreeDSx\Ldap\Server\Backend\Auth\PasswordHashService; +use FreeDSx\Ldap\Server\Backend\Write\WriteOperationDispatcher; +use FreeDSx\Ldap\Server\PasswordModify\PasswordModifyTargetResolver; use FreeDSx\Ldap\Server\RequestHandler\HandlerFactory; use FreeDSx\Ldap\Server\ServerProtocolFactory; use FreeDSx\Ldap\Server\ServerRunner\PcntlServerRunner; @@ -55,7 +58,6 @@ class Container private const FACTORY_ONLY = [ HandlerFactoryInterface::class, ServerAuthorization::class, - ServerProtocolHandlerFactory::class, ]; /** @@ -174,6 +176,26 @@ className: ClockInterface::class, className: PasswordPolicyEngine::class, factory: $this->makePasswordPolicyEngine(...), ); + $this->registerFactory( + className: ServerProtocolHandlerFactory::class, + factory: $this->makeServerProtocolHandlerFactory(...), + ); + $this->registerFactory( + className: PasswordModifyTargetResolver::class, + factory: $this->makePasswordModifyTargetResolver(...), + ); + $this->registerFactory( + className: PasswordHashService::class, + factory: $this->makePasswordHashService(...), + ); + $this->registerFactory( + className: WriteOperationDispatcher::class, + factory: $this->makeWriteOperationDispatcher(...), + ); + $this->registerFactory( + className: PasswordPolicyComponentFactory::class, + factory: $this->makePasswordPolicyComponentFactory(...), + ); } private function makePasswordPolicyEngine(): PasswordPolicyEngine @@ -250,6 +272,46 @@ private function makeServerProtocolFactory(): ServerProtocolFactory options: $this->get(ServerOptions::class), serverAuthorization: $this->get(ServerAuthorization::class), passwordPolicyEngine: $this->get(PasswordPolicyEngine::class), + routeResolver: $this->get(ServerProtocolHandlerFactory::class), + targetResolver: $this->get(PasswordModifyTargetResolver::class), + hashService: $this->get(PasswordHashService::class), + writeDispatcher: $this->get(WriteOperationDispatcher::class), + policyComponentFactory: $this->get(PasswordPolicyComponentFactory::class), + ); + } + + private function makeServerProtocolHandlerFactory(): ServerProtocolHandlerFactory + { + return new ServerProtocolHandlerFactory($this->get(ServerOptions::class)); + } + + private function makePasswordModifyTargetResolver(): PasswordModifyTargetResolver + { + $handlerFactory = $this->get(HandlerFactoryInterface::class); + + return new PasswordModifyTargetResolver( + $handlerFactory->makeBackend(), + $handlerFactory->makeIdentityResolverChain(), + ); + } + + private function makePasswordHashService(): PasswordHashService + { + return new PasswordHashService($this->get(ServerOptions::class)->getPasswordHashScheme()); + } + + private function makeWriteOperationDispatcher(): WriteOperationDispatcher + { + return $this->get(HandlerFactoryInterface::class)->makeWriteDispatcher(); + } + + private function makePasswordPolicyComponentFactory(): PasswordPolicyComponentFactory + { + return new PasswordPolicyComponentFactory( + handlerFactory: $this->get(HandlerFactoryInterface::class), + options: $this->get(ServerOptions::class), + writeDispatcher: $this->get(WriteOperationDispatcher::class), + passwordPolicyEngine: $this->get(PasswordPolicyEngine::class), ); } diff --git a/src/FreeDSx/Ldap/Protocol/Factory/ProtocolHandlerProvider.php b/src/FreeDSx/Ldap/Protocol/Factory/ProtocolHandlerProvider.php new file mode 100644 index 00000000..d97caec2 --- /dev/null +++ b/src/FreeDSx/Ldap/Protocol/Factory/ProtocolHandlerProvider.php @@ -0,0 +1,165 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FreeDSx\Ldap\Protocol\Factory; + +use FreeDSx\Ldap\Control\ControlBag; +use FreeDSx\Ldap\Operation\Request\RequestInterface; +use FreeDSx\Ldap\Protocol\Queue\ServerQueue; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerProtocolHandlerInterface; +use FreeDSx\Ldap\Server\Backend\Auth\PasswordHashService; +use FreeDSx\Ldap\Server\Backend\Write\WriteOperationDispatcher; +use FreeDSx\Ldap\Server\HandlerFactoryInterface; +use FreeDSx\Ldap\Server\Logging\EventLogger; +use FreeDSx\Ldap\Server\PasswordModify\PasswordModifyService; +use FreeDSx\Ldap\Server\PasswordModify\PasswordModifyTargetResolver; +use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyComponentFactory; +use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyContext; +use FreeDSx\Ldap\Server\RequestHistory; +use FreeDSx\Ldap\ServerOptions; + +/** + * Builds the per-request protocol handler, wiring per-connection state to shared services. + * + * @internal + * @author Chad Sikorra + */ +final readonly class ProtocolHandlerProvider implements ProtocolHandlerProviderInterface +{ + public function __construct( + private HandlerRouteResolverInterface $routeResolver, + private HandlerFactoryInterface $handlerFactory, + private ServerOptions $options, + private PasswordModifyTargetResolver $targetResolver, + private PasswordHashService $hashService, + private WriteOperationDispatcher $writeDispatcher, + private PasswordPolicyComponentFactory $policyComponentFactory, + private ServerQueue $queue, + private EventLogger $eventLogger, + private RequestHistory $requestHistory, + private ?PasswordPolicyContext $passwordPolicyContext = null, + ) {} + + public function get( + RequestInterface $request, + ControlBag $controls, + ): ServerProtocolHandlerInterface { + return match ($this->routeResolver->routeIdFor($request, $controls)) { + HandlerId::Abandon => new ServerProtocolHandler\ServerAbandonHandler(), + HandlerId::Cancel => new ServerProtocolHandler\ServerCancelHandler($this->queue), + HandlerId::WhoAmI => new ServerProtocolHandler\ServerWhoAmIHandler($this->queue), + HandlerId::PasswordModify => $this->getPasswordModifyHandler(), + HandlerId::StartTls => $this->getStartTlsHandler(), + HandlerId::UnsupportedExtended => new ServerProtocolHandler\ServerUnsupportedExtendedHandler($this->queue), + HandlerId::RootDse => $this->getRootDseHandler(), + HandlerId::Subschema => $this->getSubschemaHandler(), + HandlerId::Paging => $this->getPagingHandler(), + HandlerId::Search => $this->getSearchHandler(), + HandlerId::Unbind => new ServerProtocolHandler\ServerUnbindHandler($this->queue), + HandlerId::Dispatch => $this->getDispatchHandler(), + }; + } + + private function getPasswordModifyHandler(): ServerProtocolHandler\ServerPasswordModifyHandler + { + return new ServerProtocolHandler\ServerPasswordModifyHandler( + queue: $this->queue, + service: new PasswordModifyService( + targetResolver: $this->targetResolver, + accessControl: $this->options->getAccessControl(), + writeDispatcher: $this->writeDispatcher, + hashService: $this->hashService, + changeGuard: $this->policyComponentFactory->makeChangeGuard( + $this->eventLogger, + $this->passwordPolicyContext, + ), + passwordPolicyContext: $this->passwordPolicyContext, + ), + eventLogger: $this->eventLogger, + passwordPolicyContext: $this->passwordPolicyContext, + ); + } + + private function getStartTlsHandler(): ServerProtocolHandler\ServerStartTlsHandler + { + return new ServerProtocolHandler\ServerStartTlsHandler( + options: $this->options, + queue: $this->queue, + eventLogger: $this->eventLogger, + ); + } + + private function getSubschemaHandler(): ServerProtocolHandler\ServerSubschemaHandler + { + return new ServerProtocolHandler\ServerSubschemaHandler( + options: $this->options, + queue: $this->queue, + ); + } + + private function getSearchHandler(): ServerProtocolHandler\ServerSearchHandler + { + return new ServerProtocolHandler\ServerSearchHandler( + queue: $this->queue, + backend: $this->handlerFactory->makeBackend(), + filterEvaluator: $this->handlerFactory->makeFilterEvaluator(), + accessControl: $this->options->getAccessControl(), + schema: $this->options->getSchema(), + limits: $this->options->makeSearchLimits(), + eventLogger: $this->eventLogger, + ); + } + + private function getDispatchHandler(): ServerProtocolHandler\ServerDispatchHandler + { + $policyWriteHandler = $this->policyComponentFactory->makeWriteHandler( + $this->eventLogger, + $this->passwordPolicyContext, + ); + + return new ServerProtocolHandler\ServerDispatchHandler( + queue: $this->queue, + backend: $this->handlerFactory->makeBackend(), + writeDispatcher: $policyWriteHandler !== null + ? $this->handlerFactory->makeWriteDispatcher($policyWriteHandler) + : $this->writeDispatcher, + accessControl: $this->options->getAccessControl(), + eventRecorder: new ServerProtocolHandler\DispatchEventRecorder($this->eventLogger), + passwordPolicyContext: $this->passwordPolicyContext, + ); + } + + private function getRootDseHandler(): ServerProtocolHandler\ServerRootDseHandler + { + return new ServerProtocolHandler\ServerRootDseHandler( + options: $this->options, + queue: $this->queue, + rootDseHandler: $this->handlerFactory->makeRootDseHandler(), + ); + } + + private function getPagingHandler(): ServerProtocolHandler\ServerPagingHandler + { + return new ServerProtocolHandler\ServerPagingHandler( + queue: $this->queue, + backend: $this->handlerFactory->makeBackend(), + filterEvaluator: $this->handlerFactory->makeFilterEvaluator(), + accessControl: $this->options->getAccessControl(), + requestHistory: $this->requestHistory, + schema: $this->options->getSchema(), + limits: $this->options->makeSearchLimits(), + eventLogger: $this->eventLogger, + ); + } +} diff --git a/src/FreeDSx/Ldap/Protocol/Factory/ProtocolHandlerProviderInterface.php b/src/FreeDSx/Ldap/Protocol/Factory/ProtocolHandlerProviderInterface.php new file mode 100644 index 00000000..518e113a --- /dev/null +++ b/src/FreeDSx/Ldap/Protocol/Factory/ProtocolHandlerProviderInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FreeDSx\Ldap\Protocol\Factory; + +use FreeDSx\Ldap\Control\ControlBag; +use FreeDSx\Ldap\Operation\Request\RequestInterface; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerProtocolHandlerInterface; + +/** + * Builds the protocol handler for a resolved request route. + * + * @internal + * @author Chad Sikorra + */ +interface ProtocolHandlerProviderInterface +{ + public function get( + RequestInterface $request, + ControlBag $controls, + ): ServerProtocolHandlerInterface; +} diff --git a/src/FreeDSx/Ldap/Protocol/Factory/ServerProtocolHandlerFactory.php b/src/FreeDSx/Ldap/Protocol/Factory/ServerProtocolHandlerFactory.php index 631e8c00..e725c93c 100644 --- a/src/FreeDSx/Ldap/Protocol/Factory/ServerProtocolHandlerFactory.php +++ b/src/FreeDSx/Ldap/Protocol/Factory/ServerProtocolHandlerFactory.php @@ -15,66 +15,21 @@ use FreeDSx\Ldap\Control\Control; use FreeDSx\Ldap\Control\ControlBag; -use FreeDSx\Ldap\Exception\RuntimeException; use FreeDSx\Ldap\Operation\Request\AbandonRequest; use FreeDSx\Ldap\Operation\Request\ExtendedRequest; use FreeDSx\Ldap\Operation\Request\RequestInterface; use FreeDSx\Ldap\Operation\Request\SearchRequest; use FreeDSx\Ldap\Operation\Request\UnbindRequest; -use FreeDSx\Ldap\Protocol\Queue\ServerQueue; -use FreeDSx\Ldap\Protocol\ServerProtocolHandler; -use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerProtocolHandlerInterface; -use FreeDSx\Ldap\Server\Backend\Auth\PasswordHashService; -use FreeDSx\Ldap\Server\Backend\Write\PasswordPolicyWriteHandler; -use FreeDSx\Ldap\Server\Backend\Write\SystemChangeWriter; -use FreeDSx\Ldap\Server\Backend\Write\WriteHandlerInterface; -use FreeDSx\Ldap\Server\HandlerFactoryInterface; -use FreeDSx\Ldap\Server\Logging\EventLogger; -use FreeDSx\Ldap\Server\PasswordModify\PasswordModifyService; -use FreeDSx\Ldap\Server\PasswordModify\PasswordModifyTargetResolver; -use FreeDSx\Ldap\Server\PasswordPolicy\Guard\PasswordPolicyChangeGuard; -use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyContext; -use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyEngine; -use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyResolver; -use FreeDSx\Ldap\Server\RequestHistory; use FreeDSx\Ldap\ServerOptions; /** - * Determines the correct handler for the request. + * Classifies a request to the handler route that will process it. * * @author Chad Sikorra */ readonly class ServerProtocolHandlerFactory implements HandlerRouteResolverInterface { - public function __construct( - private HandlerFactoryInterface $handlerFactory, - private ServerOptions $options, - private RequestHistory $requestHistory, - private ServerQueue $queue, - private EventLogger $eventLogger = new EventLogger(null), - private ?PasswordPolicyEngine $passwordPolicyEngine = null, - private ?PasswordPolicyContext $passwordPolicyContext = null, - ) {} - - public function get( - RequestInterface $request, - ControlBag $controls, - ): ServerProtocolHandlerInterface { - return match ($this->routeIdFor($request, $controls)) { - HandlerId::Abandon => new ServerProtocolHandler\ServerAbandonHandler(), - HandlerId::Cancel => new ServerProtocolHandler\ServerCancelHandler($this->queue), - HandlerId::WhoAmI => new ServerProtocolHandler\ServerWhoAmIHandler($this->queue), - HandlerId::PasswordModify => $this->getPasswordModifyHandler(), - HandlerId::StartTls => $this->getStartTlsHandler(), - HandlerId::UnsupportedExtended => new ServerProtocolHandler\ServerUnsupportedExtendedHandler($this->queue), - HandlerId::RootDse => $this->getRootDseHandler(), - HandlerId::Subschema => $this->getSubschemaHandler(), - HandlerId::Paging => $this->getPagingHandler(), - HandlerId::Search => $this->getSearchHandler(), - HandlerId::Unbind => new ServerProtocolHandler\ServerUnbindHandler($this->queue), - HandlerId::Dispatch => $this->getDispatchHandler(), - }; - } + public function __construct(private ServerOptions $options) {} public function routeIdFor( RequestInterface $request, @@ -96,122 +51,6 @@ public function routeIdFor( }; } - private function getPasswordModifyHandler(): ServerProtocolHandler\ServerPasswordModifyHandler - { - return new ServerProtocolHandler\ServerPasswordModifyHandler( - queue: $this->queue, - service: new PasswordModifyService( - targetResolver: new PasswordModifyTargetResolver( - $this->handlerFactory->makeBackend(), - $this->handlerFactory->makeIdentityResolverChain(), - ), - accessControl: $this->options->getAccessControl(), - writeDispatcher: $this->handlerFactory->makeWriteDispatcher(), - hashService: new PasswordHashService($this->options->getPasswordHashScheme()), - changeGuard: $this->makePasswordPolicyChangeGuard(), - passwordPolicyContext: $this->passwordPolicyContext, - ), - eventLogger: $this->eventLogger, - passwordPolicyContext: $this->passwordPolicyContext, - ); - } - - private function getStartTlsHandler(): ServerProtocolHandler\ServerStartTlsHandler - { - return new ServerProtocolHandler\ServerStartTlsHandler( - options: $this->options, - queue: $this->queue, - eventLogger: $this->eventLogger, - ); - } - - private function getSubschemaHandler(): ServerProtocolHandler\ServerSubschemaHandler - { - return new ServerProtocolHandler\ServerSubschemaHandler( - options: $this->options, - queue: $this->queue, - ); - } - - private function getSearchHandler(): ServerProtocolHandler\ServerSearchHandler - { - return new ServerProtocolHandler\ServerSearchHandler( - queue: $this->queue, - backend: $this->handlerFactory->makeBackend(), - filterEvaluator: $this->handlerFactory->makeFilterEvaluator(), - accessControl: $this->options->getAccessControl(), - schema: $this->options->getSchema(), - limits: $this->options->makeSearchLimits(), - eventLogger: $this->eventLogger, - ); - } - - private function getDispatchHandler(): ServerProtocolHandler\ServerDispatchHandler - { - $policyWriteHandler = $this->makePasswordPolicyWriteHandler(); - - return new ServerProtocolHandler\ServerDispatchHandler( - queue: $this->queue, - backend: $this->handlerFactory->makeBackend(), - writeDispatcher: $policyWriteHandler !== null - ? $this->handlerFactory->makeWriteDispatcher($policyWriteHandler) - : $this->handlerFactory->makeWriteDispatcher(), - accessControl: $this->options->getAccessControl(), - eventRecorder: new ServerProtocolHandler\DispatchEventRecorder($this->eventLogger), - passwordPolicyContext: $this->passwordPolicyContext, - ); - } - - private function makePasswordPolicyWriteHandler(): ?PasswordPolicyWriteHandler - { - $guard = $this->makePasswordPolicyChangeGuard(); - if ($guard === null) { - return null; - } - - $backend = $this->handlerFactory->makeBackend(); - if (!$backend instanceof WriteHandlerInterface) { - throw new RuntimeException( - 'A backend implementing WriteHandlerInterface is required to enforce policy on userPassword modifications.', - ); - } - - return new PasswordPolicyWriteHandler( - $backend, - $guard, - new SystemChangeWriter($this->handlerFactory->makeWriteDispatcher()), - ); - } - - private function makePasswordPolicyChangeGuard(): ?PasswordPolicyChangeGuard - { - if (!$this->isPasswordPolicyActive()) { - return null; - } - - return new PasswordPolicyChangeGuard( - $this->passwordPolicyEngine, - new PasswordPolicyResolver( - $this->handlerFactory->makeBackend(), - $this->options->getDefaultPasswordPolicyDn(), - $this->options->getPasswordPolicy(), - ), - $this->passwordPolicyContext, - $this->eventLogger, - ); - } - - /** - * @phpstan-assert-if-true !null $this->passwordPolicyEngine - * @phpstan-assert-if-true !null $this->passwordPolicyContext - */ - private function isPasswordPolicyActive(): bool - { - return $this->passwordPolicyEngine !== null - && $this->passwordPolicyContext !== null - && $this->options->isPasswordPolicyEnabled(); - } - private function isSubschemaSearch(RequestInterface $request): bool { if (!$request instanceof SearchRequest) { @@ -240,29 +79,4 @@ private function isPagingSearch( return $request instanceof SearchRequest && $controls->has(Control::OID_PAGING); } - - private function getRootDseHandler(): ServerProtocolHandler\ServerRootDseHandler - { - $rootDseHandler = $this->handlerFactory->makeRootDseHandler(); - - return new ServerProtocolHandler\ServerRootDseHandler( - options: $this->options, - queue: $this->queue, - rootDseHandler: $rootDseHandler, - ); - } - - private function getPagingHandler(): ServerProtocolHandlerInterface - { - return new ServerProtocolHandler\ServerPagingHandler( - queue: $this->queue, - backend: $this->handlerFactory->makeBackend(), - filterEvaluator: $this->handlerFactory->makeFilterEvaluator(), - accessControl: $this->options->getAccessControl(), - requestHistory: $this->requestHistory, - schema: $this->options->getSchema(), - limits: $this->options->makeSearchLimits(), - eventLogger: $this->eventLogger, - ); - } } diff --git a/src/FreeDSx/Ldap/Server/Middleware/Pipeline/HandlerInvoker.php b/src/FreeDSx/Ldap/Server/Middleware/Pipeline/HandlerInvoker.php index a3003a8f..075d7c59 100644 --- a/src/FreeDSx/Ldap/Server/Middleware/Pipeline/HandlerInvoker.php +++ b/src/FreeDSx/Ldap/Server/Middleware/Pipeline/HandlerInvoker.php @@ -13,7 +13,7 @@ namespace FreeDSx\Ldap\Server\Middleware\Pipeline; -use FreeDSx\Ldap\Protocol\Factory\ServerProtocolHandlerFactory; +use FreeDSx\Ldap\Protocol\Factory\ProtocolHandlerProviderInterface; /** * Terminal handler that resolves and invokes the per-request protocol handler. @@ -23,11 +23,11 @@ */ final readonly class HandlerInvoker implements MiddlewareHandlerInterface { - public function __construct(private ServerProtocolHandlerFactory $protocolHandlerFactory) {} + public function __construct(private ProtocolHandlerProviderInterface $protocolHandlerProvider) {} public function handle(ServerRequestContext $context): void { - $handler = $this->protocolHandlerFactory->get( + $handler = $this->protocolHandlerProvider->get( $context->message->getRequest(), $context->message->controls(), ); diff --git a/src/FreeDSx/Ldap/Server/PasswordPolicy/PasswordPolicyComponentFactory.php b/src/FreeDSx/Ldap/Server/PasswordPolicy/PasswordPolicyComponentFactory.php new file mode 100644 index 00000000..0e9ed371 --- /dev/null +++ b/src/FreeDSx/Ldap/Server/PasswordPolicy/PasswordPolicyComponentFactory.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FreeDSx\Ldap\Server\PasswordPolicy; + +use FreeDSx\Ldap\Exception\RuntimeException; +use FreeDSx\Ldap\Server\Backend\Write\PasswordPolicyWriteHandler; +use FreeDSx\Ldap\Server\Backend\Write\SystemChangeWriter; +use FreeDSx\Ldap\Server\Backend\Write\WriteHandlerInterface; +use FreeDSx\Ldap\Server\Backend\Write\WriteOperationDispatcher; +use FreeDSx\Ldap\Server\HandlerFactoryInterface; +use FreeDSx\Ldap\Server\Logging\EventLogger; +use FreeDSx\Ldap\Server\PasswordPolicy\Guard\PasswordPolicyChangeGuard; +use FreeDSx\Ldap\ServerOptions; + +/** + * Builds the password-policy write-enforcement components from shared services plus per-connection state. + * + * @internal + * @author Chad Sikorra + */ +final readonly class PasswordPolicyComponentFactory +{ + public function __construct( + private HandlerFactoryInterface $handlerFactory, + private ServerOptions $options, + private WriteOperationDispatcher $writeDispatcher, + private PasswordPolicyEngine $passwordPolicyEngine, + ) {} + + /** + * @throws RuntimeException when policy is active but the backend cannot enforce writes. + */ + public function makeWriteHandler( + EventLogger $eventLogger, + ?PasswordPolicyContext $passwordPolicyContext, + ): ?PasswordPolicyWriteHandler { + $guard = $this->makeChangeGuard( + $eventLogger, + $passwordPolicyContext, + ); + if ($guard === null) { + return null; + } + + $backend = $this->handlerFactory->makeBackend(); + if (!$backend instanceof WriteHandlerInterface) { + throw new RuntimeException( + 'A backend implementing WriteHandlerInterface is required to enforce policy on userPassword modifications.', + ); + } + + return new PasswordPolicyWriteHandler( + $backend, + $guard, + new SystemChangeWriter($this->writeDispatcher), + ); + } + + public function makeChangeGuard( + EventLogger $eventLogger, + ?PasswordPolicyContext $passwordPolicyContext, + ): ?PasswordPolicyChangeGuard { + if ($passwordPolicyContext === null || !$this->options->isPasswordPolicyEnabled()) { + return null; + } + + return new PasswordPolicyChangeGuard( + $this->passwordPolicyEngine, + new PasswordPolicyResolver( + $this->handlerFactory->makeBackend(), + $this->options->getDefaultPasswordPolicyDn(), + $this->options->getPasswordPolicy(), + ), + $passwordPolicyContext, + $eventLogger, + ); + } +} diff --git a/src/FreeDSx/Ldap/Server/ServerProtocolFactory.php b/src/FreeDSx/Ldap/Server/ServerProtocolFactory.php index 94b776ee..06787a36 100644 --- a/src/FreeDSx/Ldap/Server/ServerProtocolFactory.php +++ b/src/FreeDSx/Ldap/Server/ServerProtocolFactory.php @@ -25,8 +25,13 @@ use FreeDSx\Sasl\Mechanism\MechanismName; use FreeDSx\Sasl\Options\SaslOptions; use FreeDSx\Sasl\Sasl; +use FreeDSx\Ldap\Protocol\Factory\ProtocolHandlerProvider; use FreeDSx\Ldap\Protocol\Factory\ServerProtocolHandlerFactory; use FreeDSx\Ldap\Protocol\Queue\ServerQueue; +use FreeDSx\Ldap\Server\Backend\Auth\PasswordHashService; +use FreeDSx\Ldap\Server\Backend\Write\WriteOperationDispatcher; +use FreeDSx\Ldap\Server\PasswordModify\PasswordModifyTargetResolver; +use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyComponentFactory; use FreeDSx\Ldap\Protocol\ServerAuthorization; use FreeDSx\Ldap\Protocol\ServerProtocolHandler; use FreeDSx\Ldap\Server\Backend\Auth\PasswordPolicyAwareAuthenticator; @@ -55,6 +60,11 @@ public function __construct( private readonly ServerOptions $options, private readonly ServerAuthorization $serverAuthorization, private readonly PasswordPolicyEngine $passwordPolicyEngine, + private readonly ServerProtocolHandlerFactory $routeResolver, + private readonly PasswordModifyTargetResolver $targetResolver, + private readonly PasswordHashService $hashService, + private readonly WriteOperationDispatcher $writeDispatcher, + private readonly PasswordPolicyComponentFactory $policyComponentFactory, ) {} public function make( @@ -128,13 +138,17 @@ public function make( ), ); - $protocolHandlerFactory = new ServerProtocolHandlerFactory( + $handlerProvider = new ProtocolHandlerProvider( + routeResolver: $this->routeResolver, handlerFactory: $this->handlerFactory, options: $this->options, - requestHistory: new RequestHistory(), + targetResolver: $this->targetResolver, + hashService: $this->hashService, + writeDispatcher: $this->writeDispatcher, + policyComponentFactory: $this->policyComponentFactory, queue: $serverQueue, eventLogger: $eventLogger, - passwordPolicyEngine: $this->passwordPolicyEngine, + requestHistory: new RequestHistory(), passwordPolicyContext: $policyContext, ); @@ -142,14 +156,14 @@ public function make( queue: $serverQueue, requestPipeline: new MiddlewareChain( [ - new CriticalControlMiddleware($protocolHandlerFactory), + new CriticalControlMiddleware($this->routeResolver), new OperationAuthorizationMiddleware( - $protocolHandlerFactory, + $this->routeResolver, $this->options->getAccessControl(), $eventLogger, ), ], - new HandlerInvoker($protocolHandlerFactory), + new HandlerInvoker($handlerProvider), ), authorizer: $this->serverAuthorization, authenticator: new Authenticator($authenticators), diff --git a/tests/unit/Protocol/Factory/ProtocolHandlerProviderTest.php b/tests/unit/Protocol/Factory/ProtocolHandlerProviderTest.php new file mode 100644 index 00000000..762ebeee --- /dev/null +++ b/tests/unit/Protocol/Factory/ProtocolHandlerProviderTest.php @@ -0,0 +1,281 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Unit\FreeDSx\Ldap\Protocol\Factory; + +use FreeDSx\Ldap\Control\ControlBag; +use FreeDSx\Ldap\Control\PagingControl; +use FreeDSx\Ldap\Entry\Entry; +use FreeDSx\Ldap\Exception\RuntimeException; +use FreeDSx\Ldap\Operation\Request\ExtendedRequest; +use FreeDSx\Ldap\Operations; +use FreeDSx\Ldap\Protocol\Factory\ProtocolHandlerProvider; +use FreeDSx\Ldap\Protocol\Factory\ServerProtocolHandlerFactory; +use FreeDSx\Ldap\Protocol\Queue\ServerQueue; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerDispatchHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerPagingHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerPasswordModifyHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerRootDseHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerSearchHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerStartTlsHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerSubschemaHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerUnbindHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerUnsupportedExtendedHandler; +use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerWhoAmIHandler; +use FreeDSx\Ldap\Search\Filter\EqualityFilter; +use FreeDSx\Ldap\Server\Backend\Auth\NameResolver\DnBindNameResolver; +use FreeDSx\Ldap\Server\Backend\Auth\PasswordHashService; +use FreeDSx\Ldap\Server\Backend\LdapBackendInterface; +use FreeDSx\Ldap\Server\Backend\Storage\Adapter\InMemoryStorage; +use FreeDSx\Ldap\Server\Backend\Storage\FilterEvaluator; +use FreeDSx\Ldap\Server\Backend\Storage\WritableStorageBackend; +use FreeDSx\Ldap\Server\Backend\Write\WriteOperationDispatcher; +use FreeDSx\Ldap\Server\Clock\SystemClock; +use FreeDSx\Ldap\Server\HandlerFactoryInterface; +use FreeDSx\Ldap\Server\Logging\EventLogger; +use FreeDSx\Ldap\Server\PasswordModify\PasswordModifyTargetResolver; +use FreeDSx\Ldap\Server\PasswordPolicy\Constraint\PasswordChangeConstraintChain; +use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicy; +use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyComponentFactory; +use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyContext; +use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyEngine; +use FreeDSx\Ldap\Server\RequestHistory; +use FreeDSx\Ldap\ServerOptions; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +final class ProtocolHandlerProviderTest extends TestCase +{ + private ServerQueue&MockObject $mockQueue; + + private HandlerFactoryInterface&MockObject $mockHandlerFactory; + + private ProtocolHandlerProvider $subject; + + protected function setUp(): void + { + $this->mockQueue = $this->createMock(ServerQueue::class); + $this->mockHandlerFactory = $this->createMock(HandlerFactoryInterface::class); + $backend = new WritableStorageBackend(new InMemoryStorage()); + + $this->mockHandlerFactory + ->method('makeBackend') + ->willReturn($backend); + $this->mockHandlerFactory + ->method('makeFilterEvaluator') + ->willReturn(new FilterEvaluator()); + $this->mockHandlerFactory + ->method('makeWriteDispatcher') + ->willReturn(new WriteOperationDispatcher()); + $this->mockHandlerFactory + ->method('makeIdentityResolverChain') + ->willReturn(new DnBindNameResolver()); + + $options = new ServerOptions(); + $writeDispatcher = new WriteOperationDispatcher(); + + $this->subject = new ProtocolHandlerProvider( + routeResolver: new ServerProtocolHandlerFactory($options), + handlerFactory: $this->mockHandlerFactory, + options: $options, + targetResolver: new PasswordModifyTargetResolver( + $backend, + new DnBindNameResolver(), + ), + hashService: new PasswordHashService(), + writeDispatcher: $writeDispatcher, + policyComponentFactory: new PasswordPolicyComponentFactory( + $this->mockHandlerFactory, + $options, + $writeDispatcher, + new PasswordPolicyEngine( + new SystemClock(), + new PasswordChangeConstraintChain([]), + ), + ), + queue: $this->mockQueue, + eventLogger: new EventLogger(null), + requestHistory: new RequestHistory(), + passwordPolicyContext: null, + ); + } + + public function test_it_throws_when_password_policy_is_enabled_but_the_backend_is_not_writable(): void + { + $options = (new ServerOptions())->setPasswordPolicy(new PasswordPolicy()); + $handlerFactory = $this->createMock(HandlerFactoryInterface::class); + $handlerFactory + ->method('makeBackend') + ->willReturn($this->createMock(LdapBackendInterface::class)); + $writeDispatcher = new WriteOperationDispatcher(); + + $provider = new ProtocolHandlerProvider( + routeResolver: new ServerProtocolHandlerFactory($options), + handlerFactory: $handlerFactory, + options: $options, + targetResolver: new PasswordModifyTargetResolver( + $handlerFactory->makeBackend(), + new DnBindNameResolver(), + ), + hashService: new PasswordHashService(), + writeDispatcher: $writeDispatcher, + policyComponentFactory: new PasswordPolicyComponentFactory( + $handlerFactory, + $options, + $writeDispatcher, + new PasswordPolicyEngine( + new SystemClock(), + new PasswordChangeConstraintChain([]), + ), + ), + queue: $this->mockQueue, + eventLogger: new EventLogger(null), + requestHistory: new RequestHistory(), + passwordPolicyContext: new PasswordPolicyContext(), + ); + + $this->expectException(RuntimeException::class); + + $provider->get( + Operations::delete('cn=foo,dc=bar'), + new ControlBag(), + ); + } + + public function test_it_should_get_a_password_modify_handler(): void + { + self::assertInstanceOf( + ServerPasswordModifyHandler::class, + $this->subject->get( + Operations::extended(ExtendedRequest::OID_PWD_MODIFY), + new ControlBag(), + ), + ); + } + + public function test_it_should_get_a_start_tls_handler(): void + { + self::assertInstanceOf( + ServerStartTlsHandler::class, + $this->subject->get( + Operations::extended(ExtendedRequest::OID_START_TLS), + new ControlBag(), + ), + ); + } + + public function test_it_should_get_a_whoami_handler(): void + { + self::assertInstanceOf( + ServerWhoAmIHandler::class, + $this->subject->get( + Operations::whoami(), + new ControlBag(), + ), + ); + } + + public function test_it_should_get_a_search_handler(): void + { + self::assertInstanceOf( + ServerSearchHandler::class, + $this->subject->get( + Operations::list(new EqualityFilter('foo', 'bar'), 'cn=foo'), + new ControlBag(), + ), + ); + } + + public function test_it_should_get_a_paging_handler_when_a_paging_control_is_present(): void + { + self::assertInstanceOf( + ServerPagingHandler::class, + $this->subject->get( + Operations::list(new EqualityFilter('foo', 'bar'), 'cn=foo'), + new ControlBag(new PagingControl(10)), + ), + ); + } + + public function test_it_should_get_a_root_dse_handler(): void + { + self::assertInstanceOf( + ServerRootDseHandler::class, + $this->subject->get( + Operations::read(''), + new ControlBag(), + ), + ); + } + + public function test_it_should_get_a_subschema_handler(): void + { + self::assertInstanceOf( + ServerSubschemaHandler::class, + $this->subject->get( + Operations::read('cn=Subschema'), + new ControlBag(), + ), + ); + } + + public function test_it_should_get_the_unsupported_extended_handler_for_an_unknown_oid(): void + { + self::assertInstanceOf( + ServerUnsupportedExtendedHandler::class, + $this->subject->get( + Operations::extended('1.2.3.4.5.6.7.8.9'), + new ControlBag(), + ), + ); + } + + public function test_it_should_get_an_unbind_handler(): void + { + self::assertInstanceOf( + ServerUnbindHandler::class, + $this->subject->get( + Operations::unbind(), + new ControlBag(), + ), + ); + } + + public function test_it_should_get_the_dispatch_handler_for_common_requests(): void + { + self::assertInstanceOf( + ServerDispatchHandler::class, + $this->subject->get(Operations::delete('cn=foo'), new ControlBag()), + ); + self::assertInstanceOf( + ServerDispatchHandler::class, + $this->subject->get(Operations::add(Entry::fromArray('cn=foo')), new ControlBag()), + ); + self::assertInstanceOf( + ServerDispatchHandler::class, + $this->subject->get(Operations::compare('cn=foo', 'foo', 'bar'), new ControlBag()), + ); + self::assertInstanceOf( + ServerDispatchHandler::class, + $this->subject->get(Operations::modify('cn=foo'), new ControlBag()), + ); + self::assertInstanceOf( + ServerDispatchHandler::class, + $this->subject->get(Operations::move('cn=foo', 'foo=bar'), new ControlBag()), + ); + self::assertInstanceOf( + ServerDispatchHandler::class, + $this->subject->get(Operations::rename('cn=foo', 'cn=foo'), new ControlBag()), + ); + } +} diff --git a/tests/unit/Protocol/Factory/ServerProtocolHandlerFactoryTest.php b/tests/unit/Protocol/Factory/ServerProtocolHandlerFactoryTest.php index bb36d37d..3f44d724 100644 --- a/tests/unit/Protocol/Factory/ServerProtocolHandlerFactoryTest.php +++ b/tests/unit/Protocol/Factory/ServerProtocolHandlerFactoryTest.php @@ -15,230 +15,58 @@ use FreeDSx\Ldap\Control\ControlBag; use FreeDSx\Ldap\Control\PagingControl; -use FreeDSx\Ldap\Entry\Entry; -use FreeDSx\Ldap\Exception\RuntimeException; +use FreeDSx\Ldap\Operation\Request\AbandonRequest; +use FreeDSx\Ldap\Operation\Request\CancelRequest; use FreeDSx\Ldap\Operation\Request\ExtendedRequest; +use FreeDSx\Ldap\Operation\Request\RequestInterface; use FreeDSx\Ldap\Operations; +use FreeDSx\Ldap\Protocol\Factory\HandlerId; use FreeDSx\Ldap\Protocol\Factory\ServerProtocolHandlerFactory; -use FreeDSx\Ldap\Protocol\Queue\ServerQueue; -use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerDispatchHandler; -use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerPagingHandler; -use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerPasswordModifyHandler; -use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerRootDseHandler; -use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerSearchHandler; -use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerStartTlsHandler; -use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerSubschemaHandler; -use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerUnbindHandler; -use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerUnsupportedExtendedHandler; -use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerWhoAmIHandler; -use FreeDSx\Ldap\Server\Backend\Auth\NameResolver\DnBindNameResolver; use FreeDSx\Ldap\Search\Filter\EqualityFilter; -use FreeDSx\Ldap\Server\Backend\LdapBackendInterface; -use FreeDSx\Ldap\Server\Backend\Storage\Adapter\InMemoryStorage; -use FreeDSx\Ldap\Server\Backend\Storage\WritableStorageBackend; -use FreeDSx\Ldap\Server\Backend\Write\WriteOperationDispatcher; -use FreeDSx\Ldap\Server\Clock\SystemClock; -use FreeDSx\Ldap\Server\HandlerFactoryInterface; -use FreeDSx\Ldap\Server\PasswordPolicy\Constraint\PasswordChangeConstraintChain; -use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicy; -use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyContext; -use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyEngine; -use FreeDSx\Ldap\Server\RequestHistory; -use FreeDSx\Ldap\Server\Backend\Storage\FilterEvaluator; use FreeDSx\Ldap\ServerOptions; -use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; final class ServerProtocolHandlerFactoryTest extends TestCase { private ServerProtocolHandlerFactory $subject; - private ServerQueue&MockObject $mockQueue; - - private HandlerFactoryInterface&MockObject $mockHandlerFactory; - protected function setUp(): void { - $this->mockQueue = $this->createMock(ServerQueue::class); - $this->mockHandlerFactory = $this->createMock(HandlerFactoryInterface::class); - - $this->mockHandlerFactory - ->method('makeBackend') - ->willReturn(new WritableStorageBackend(new InMemoryStorage())); - - $this->mockHandlerFactory - ->method('makeFilterEvaluator') - ->willReturn(new FilterEvaluator()); - - $this->mockHandlerFactory - ->method('makeWriteDispatcher') - ->willReturn(new WriteOperationDispatcher()); - - $this->mockHandlerFactory - ->method('makeIdentityResolverChain') - ->willReturn(new DnBindNameResolver()); - - $this->subject = new ServerProtocolHandlerFactory( - $this->mockHandlerFactory, - new ServerOptions(), - new RequestHistory(), - $this->mockQueue, - ); - } - - public function test_it_throws_when_password_policy_is_enabled_but_the_backend_is_not_writable(): void - { - $handlerFactory = $this->createMock(HandlerFactoryInterface::class); - $handlerFactory - ->method('makeBackend') - ->willReturn($this->createMock(LdapBackendInterface::class)); - - $factory = new ServerProtocolHandlerFactory( - $handlerFactory, - (new ServerOptions())->setPasswordPolicy(new PasswordPolicy()), - new RequestHistory(), - $this->mockQueue, - passwordPolicyEngine: new PasswordPolicyEngine( - new SystemClock(), - new PasswordChangeConstraintChain([]), - ), - passwordPolicyContext: new PasswordPolicyContext(), - ); - - $this->expectException(RuntimeException::class); - - $factory->get( - Operations::delete('cn=foo,dc=bar'), - new ControlBag(), - ); + $this->subject = new ServerProtocolHandlerFactory(new ServerOptions()); } - public function test_it_should_get_a_password_modify_handler(): void - { - self::assertInstanceOf( - ServerPasswordModifyHandler::class, - $this->subject->get( - Operations::extended(ExtendedRequest::OID_PWD_MODIFY), - new ControlBag(), - ), - ); - } - - public function test_it_should_get_a_start_tls_hanlder(): void - { - self::assertInstanceof( - ServerStartTlsHandler::class, - $this->subject->get( - Operations::extended(ExtendedRequest::OID_START_TLS), - new ControlBag(), - ), - ); - } - - public function test_it_should_get_a_whoami_handler(): void - { - self::assertInstanceof( - ServerWhoAmIHandler::class, - $this->subject->get( - Operations::whoami(), - new ControlBag(), - ), - ); - } - - public function test_it_should_get_a_search_handler(): void - { - self::assertInstanceof( - ServerSearchHandler::class, - $this->subject->get( - Operations::list(new EqualityFilter('foo', 'bar'), 'cn=foo'), - new ControlBag(), - ), - ); - } - - public function test_it_should_get_a_paging_handler_when_a_paging_control_is_present(): void - { - $controls = new ControlBag(new PagingControl(10)); - - self::assertInstanceOf( - ServerPagingHandler::class, - $this->subject->get( - Operations::list(new EqualityFilter('foo', 'bar'), 'cn=foo'), + #[DataProvider('routes')] + public function test_it_resolves_the_route_for_a_request( + RequestInterface $request, + ControlBag $controls, + HandlerId $expected, + ): void { + self::assertSame( + $expected, + $this->subject->routeIdFor( + $request, $controls, ), ); } - public function test_it_should_get_a_root_dse_handler(): void - { - self::assertInstanceOf( - ServerRootDseHandler::class, - $this->subject->get( - Operations::read(''), - new ControlBag(), - ), - ); - } - - public function test_it_should_get_a_subschema_handler(): void - { - self::assertInstanceOf( - ServerSubschemaHandler::class, - $this->subject->get( - Operations::read('cn=Subschema'), - new ControlBag(), - ), - ); - } - - public function test_it_should_get_the_unsupported_extended_handler_for_an_unknown_oid(): void + /** + * @return iterable + */ + public static function routes(): iterable { - self::assertInstanceOf( - ServerUnsupportedExtendedHandler::class, - $this->subject->get( - Operations::extended('1.2.3.4.5.6.7.8.9'), - new ControlBag(), - ), - ); - } - - public function test_it_should_get_an_unbind_handler(): void - { - self::assertInstanceOf( - ServerUnbindHandler::class, - $this->subject->get( - Operations::unbind(), - new ControlBag(), - ), - ); - } - - public function test_it_should_get_the_dispatch_handler_for_common_requests(): void - { - self::assertInstanceOf( - ServerDispatchHandler::class, - $this->subject->get(Operations::delete('cn=foo'), new ControlBag()), - ); - self::assertInstanceOf( - ServerDispatchHandler::class, - $this->subject->get(Operations::add(Entry::fromArray('cn=foo')), new ControlBag()), - ); - self::assertInstanceOf( - ServerDispatchHandler::class, - $this->subject->get(Operations::compare('cn=foo', 'foo', 'bar'), new ControlBag()), - ); - self::assertInstanceOf( - ServerDispatchHandler::class, - $this->subject->get(Operations::modify('cn=foo'), new ControlBag()), - ); - self::assertInstanceOf( - ServerDispatchHandler::class, - $this->subject->get(Operations::move('cn=foo', 'foo=bar'), new ControlBag()), - ); - self::assertInstanceOf( - ServerDispatchHandler::class, - $this->subject->get(Operations::rename('cn=foo', 'cn=foo'), new ControlBag()), - ); + yield 'abandon' => [new AbandonRequest(1), new ControlBag(), HandlerId::Abandon]; + yield 'cancel' => [new CancelRequest(1), new ControlBag(), HandlerId::Cancel]; + yield 'whoami' => [Operations::whoami(), new ControlBag(), HandlerId::WhoAmI]; + yield 'password modify' => [Operations::extended(ExtendedRequest::OID_PWD_MODIFY), new ControlBag(), HandlerId::PasswordModify]; + yield 'start tls' => [Operations::extended(ExtendedRequest::OID_START_TLS), new ControlBag(), HandlerId::StartTls]; + yield 'unsupported extended' => [Operations::extended('1.2.3.4.5.6.7.8.9'), new ControlBag(), HandlerId::UnsupportedExtended]; + yield 'root dse' => [Operations::read(''), new ControlBag(), HandlerId::RootDse]; + yield 'subschema' => [Operations::read('cn=Subschema'), new ControlBag(), HandlerId::Subschema]; + yield 'paging' => [Operations::list(new EqualityFilter('foo', 'bar'), 'cn=foo'), new ControlBag(new PagingControl(10)), HandlerId::Paging]; + yield 'search' => [Operations::list(new EqualityFilter('foo', 'bar'), 'cn=foo'), new ControlBag(), HandlerId::Search]; + yield 'unbind' => [Operations::unbind(), new ControlBag(), HandlerId::Unbind]; + yield 'delete dispatch' => [Operations::delete('cn=foo'), new ControlBag(), HandlerId::Dispatch]; } } diff --git a/tests/unit/Protocol/ServerProtocolHandlerTest.php b/tests/unit/Protocol/ServerProtocolHandlerTest.php index 91611e9a..4d615558 100644 --- a/tests/unit/Protocol/ServerProtocolHandlerTest.php +++ b/tests/unit/Protocol/ServerProtocolHandlerTest.php @@ -33,7 +33,7 @@ use FreeDSx\Ldap\Operation\ResultCode; use FreeDSx\Ldap\Protocol\Authenticator; use FreeDSx\Ldap\Protocol\Authorization\DispatchAuthorizer; -use FreeDSx\Ldap\Protocol\Factory\ServerProtocolHandlerFactory; +use FreeDSx\Ldap\Protocol\Factory\ProtocolHandlerProviderInterface; use FreeDSx\Ldap\Protocol\LdapMessageRequest; use FreeDSx\Ldap\Protocol\LdapMessageResponse; use FreeDSx\Ldap\Protocol\Queue\ServerQueue; @@ -58,7 +58,7 @@ final class ServerProtocolHandlerTest extends TestCase private ServerQueue&MockObject $mockQueue; - private ServerProtocolHandlerFactory&MockObject $mockProtocolHandlerFactory; + private ProtocolHandlerProviderInterface&MockObject $mockProtocolHandlerProvider; private MiddlewareHandlerInterface $requestPipeline; @@ -69,7 +69,7 @@ final class ServerProtocolHandlerTest extends TestCase protected function setUp(): void { $this->mockQueue = $this->createMock(ServerQueue::class); - $this->mockProtocolHandlerFactory = $this->createMock(ServerProtocolHandlerFactory::class); + $this->mockProtocolHandlerProvider = $this->createMock(ProtocolHandlerProviderInterface::class); $this->mockAuthenticator = $this->createMock(Authenticator::class); $this->mockProtocolHandler = $this->createMock(ServerProtocolHandler\ServerProtocolHandlerInterface::class); @@ -84,13 +84,13 @@ protected function setUp(): void ->method('sendMessage') ->willReturnSelf(); - $this->mockProtocolHandlerFactory + $this->mockProtocolHandlerProvider ->method('get') ->willReturn($this->mockProtocolHandler); $this->requestPipeline = new MiddlewareChain( [], - new HandlerInvoker($this->mockProtocolHandlerFactory), + new HandlerInvoker($this->mockProtocolHandlerProvider), ); $authorizer = new ServerAuthorization(new ServerOptions()); @@ -130,7 +130,7 @@ public function test_it_should_enforce_anonymous_bind_requirements(): void ), )); - $this->mockProtocolHandlerFactory + $this->mockProtocolHandlerProvider ->expects(self::never()) ->method('get'); diff --git a/tests/unit/Server/ServerProtocolFactoryTest.php b/tests/unit/Server/ServerProtocolFactoryTest.php index 2c7b7bd2..0606d258 100644 --- a/tests/unit/Server/ServerProtocolFactoryTest.php +++ b/tests/unit/Server/ServerProtocolFactoryTest.php @@ -13,8 +13,13 @@ namespace Tests\Unit\FreeDSx\Ldap\Server; +use FreeDSx\Ldap\Protocol\Factory\ServerProtocolHandlerFactory; use FreeDSx\Ldap\Protocol\ServerAuthorization; use FreeDSx\Ldap\Server\Backend\Auth\NameResolver\BindNameResolverInterface; +use FreeDSx\Ldap\Server\Backend\Auth\NameResolver\DnBindNameResolver; +use FreeDSx\Ldap\Server\Backend\Auth\PasswordHashService; +use FreeDSx\Ldap\Server\PasswordModify\PasswordModifyTargetResolver; +use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyComponentFactory; use FreeDSx\Ldap\Server\Backend\Auth\PasswordAuthenticatableInterface; use FreeDSx\Ldap\Server\Backend\Storage\Adapter\InMemoryStorage; use FreeDSx\Ldap\Server\Backend\Storage\WritableStorageBackend; @@ -52,11 +57,27 @@ protected function setUp(): void ->method('makeFilterEvaluator') ->willReturn(new FilterEvaluator()); + $options = new ServerOptions(); + $writeDispatcher = new WriteOperationDispatcher(); + $this->subject = new ServerProtocolFactory( $this->mockHandlerFactory, - new ServerOptions(), + $options, $this->mockServerAuthorization, $this->passwordPolicyEngine(), + new ServerProtocolHandlerFactory($options), + new PasswordModifyTargetResolver( + $this->mockHandlerFactory->makeBackend(), + new DnBindNameResolver(), + ), + new PasswordHashService(), + $writeDispatcher, + new PasswordPolicyComponentFactory( + $this->mockHandlerFactory, + $options, + $writeDispatcher, + $this->passwordPolicyEngine(), + ), ); } @@ -86,11 +107,26 @@ public function test_it_includes_sasl_when_mechanisms_are_configured(): void $mockSocket = $this->createMock(Socket::class); + $options = (new ServerOptions())->setSaslMechanisms(ServerOptions::SASL_PLAIN); + $writeDispatcher = new WriteOperationDispatcher(); $subject = new ServerProtocolFactory( $this->mockHandlerFactory, - (new ServerOptions())->setSaslMechanisms(ServerOptions::SASL_PLAIN), + $options, $this->mockServerAuthorization, $this->passwordPolicyEngine(), + new ServerProtocolHandlerFactory($options), + new PasswordModifyTargetResolver( + $this->mockHandlerFactory->makeBackend(), + new DnBindNameResolver(), + ), + new PasswordHashService(), + $writeDispatcher, + new PasswordPolicyComponentFactory( + $this->mockHandlerFactory, + $options, + $writeDispatcher, + $this->passwordPolicyEngine(), + ), ); $subject->make($mockSocket); @@ -112,11 +148,26 @@ public function test_it_wraps_the_authenticator_when_password_policy_is_enabled( ->method('makeWriteDispatcher') ->willReturn(new WriteOperationDispatcher()); + $options = (new ServerOptions())->setPasswordPolicy(new PasswordPolicy()); + $writeDispatcher = new WriteOperationDispatcher(); $subject = new ServerProtocolFactory( $this->mockHandlerFactory, - (new ServerOptions())->setPasswordPolicy(new PasswordPolicy()), + $options, $this->mockServerAuthorization, $this->passwordPolicyEngine(), + new ServerProtocolHandlerFactory($options), + new PasswordModifyTargetResolver( + $this->mockHandlerFactory->makeBackend(), + new DnBindNameResolver(), + ), + new PasswordHashService(), + $writeDispatcher, + new PasswordPolicyComponentFactory( + $this->mockHandlerFactory, + $options, + $writeDispatcher, + $this->passwordPolicyEngine(), + ), ); $subject->make($mockSocket);