<?php declare(strict_types=1); /** * LDAP wrapper service. * * @see https://github.com/Adldap2/Adldap2 */ namespace Dbp\Relay\LdapPersonProviderBundle\Service; use Adldap\Adldap; use Adldap\Connections\Provider; use Adldap\Connections\ProviderInterface; use Adldap\Models\User; use Adldap\Query\Builder; use Dbp\Relay\BasePersonBundle\Entity\Person; use Dbp\Relay\CoreBundle\API\UserSessionInterface; use Dbp\Relay\CoreBundle\Exception\ApiError; use Dbp\Relay\CoreBundle\Helpers\Tools as CoreTools; use Psr\Cache\CacheItemPoolInterface; use Psr\Container\ContainerInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Psr16Cache; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Contracts\Service\ServiceSubscriberInterface; class LDAPApi implements LoggerAwareInterface, ServiceSubscriberInterface { use LoggerAwareTrait; private $PAGESIZE = 50; /** * @var Adldap */ private $ad; private $cachePool; private $personCache; private $cacheTTL; /** * @var Person|null */ private $currentPerson; private $providerConfig; private $deploymentEnv; private $locator; private $params; private $identifierAttributeName; private $givenNameAttributeName; private $familyNameAttributeName; private $emailAttributeName; private $birthdayAttributeName; public function __construct(ContainerInterface $locator, ParameterBagInterface $params) { $this->ad = new Adldap(); $this->cacheTTL = 0; $this->currentPerson = null; $this->params = $params; // $this->providerConfig = [ // 'hosts' => [$this->params->get('app.ldap.host') ?? ''], // 'base_dn' => $this->params->get('app.ldap.base_dn') ?? '', // 'username' => $this->params->get('app.ldap.username') ?? '', // 'password' => $this->params->get('app.ldap.password') ?? '', // 'use_tls' => true, // ]; $this->locator = $locator; $this->deploymentEnv = 'production'; // $this->setPersonCache(new FilesystemAdapter('app-core-auth-person', 60, (string) $this->params->get('app.cache.person-cache-path'))); // $this->setLDAPCache(new FilesystemAdapter('app-core-ldap', 360, (string) $this->params->get('app.cache.ldap-cache-path')), 360); // $this->identifierAttributeName = $this->params->get('app.ldap.attributes.identifier') ?? 'cn'; // $this->givenNameAttributeName = $this->params->get('app.ldap.attributes.given_name') ?? 'givenName'; // $this->familyNameAttributeName = $this->params->get('app.ldap.attributes.family_name') ?? 'sn'; // $this->emailAttributeName = $this->params->get('app.ldap.attributes.email') ?? ''; // $this->birthdayAttributeName = $this->params->get('app.ldap.attributes.birthday') ?? ''; } public function setConfig(array $config) { $this->identifierAttributeName = $config['ldap']['attributes']['identifier'] ?? 'cn'; $this->givenNameAttributeName = $config['ldap']['attributes']['given_name'] ?? 'givenName'; $this->familyNameAttributeName = $config['ldap']['attributes']['family_name'] ?? 'sn'; $this->emailAttributeName = $config['ldap']['attributes']['email'] ?? ''; $this->birthdayAttributeName = $config['ldap']['attributes']['birthday'] ?? ''; $this->providerConfig = [ 'hosts' => [$config['ldap']['host'] ?? ''], 'base_dn' => $config['ldap']['base_dn'] ?? '', 'username' => $config['ldap']['username'] ?? '', 'password' => $config['ldap']['password'] ?? '', 'use_tls' => true, ]; } public function setDeploymentEnvironment(string $env) { $this->deploymentEnv = $env; } public function setLDAPCache(?CacheItemPoolInterface $cachePool, int $ttl) { $this->cachePool = $cachePool; $this->cacheTTL = $ttl; } public function setPersonCache(?CacheItemPoolInterface $cachePool) { $this->personCache = $cachePool; } private function getProvider(): ProviderInterface { if ($this->logger !== null) { Adldap::setLogger($this->logger); } $ad = new Adldap(); $ad->addProvider($this->providerConfig); $provider = $ad->connect(); assert($provider instanceof Provider); if ($this->cachePool !== null) { $provider->setCache(new Psr16Cache($this->cachePool)); } return $provider; } private function getCachedBuilder(ProviderInterface $provider): Builder { // FIXME: https://github.com/Adldap2/Adldap2/issues/786 // return $provider->search()->cache($until=$this->cacheTTL); // We depend on the default TTL of the cache for now... /** * @var Builder $builder */ $builder = $provider->search()->cache(); return $builder; } private function getPeopleUserItems(array $filters): array { try { $provider = $this->getProvider(); $builder = $this->getCachedBuilder($provider); $search = $builder ->where('objectClass', '=', $provider->getSchema()->person()); if (isset($filters['search'])) { $items = explode(' ', $filters['search']); // search for all substrings foreach ($items as $item) { $search->whereContains('fullName', $item); } } return $search->sortBy($this->familyNameAttributeName, 'asc')->paginate($this->PAGESIZE)->getResults(); } catch (\Adldap\Auth\BindException $e) { // There was an issue binding / connecting to the server. throw new ApiError(Response::HTTP_BAD_GATEWAY, sprintf('People could not be loaded! Message: %s', CoreTools::filterErrorMessage($e->getMessage()))); } } public function getPersons(array $filters): array { $persons = []; $items = $this->getPeopleUserItems($filters); foreach ($items as $item) { $person = $this->personFromUserItem($item, false); $persons[] = $person; } return $persons; } /** * @return Person[] */ public function getPersonsByNameAndBirthDate(string $givenName, string $familyName, string $birthDate): array { if ($this->birthdayAttributeName === '') { return []; } try { $provider = $this->getProvider(); $builder = $this->getCachedBuilder($provider); /** @var User[] $users */ $users = $builder ->where('objectClass', '=', $provider->getSchema()->person()) ->whereEquals($this->givenNameAttributeName, $givenName) ->whereEquals($this->familyNameAttributeName, $familyName) ->whereEquals($this->birthdayAttributeName, $birthDate) // (e.g. 1981-07-18) ->sortBy($this->familyNameAttributeName, 'asc')->paginate($this->PAGESIZE)->getResults(); $people = []; foreach ($users as $user) { $people[] = $this->personFromUserItem($user, true); } return $people; } catch (\Adldap\Auth\BindException $e) { // There was an issue binding / connecting to the server. throw new ApiError(Response::HTTP_BAD_GATEWAY, sprintf('Persons could not be loaded! Message: %s', CoreTools::filterErrorMessage($e->getMessage()))); } } public function getPersonUserItem(string $identifier): ?User { try { $provider = $this->getProvider(); $builder = $this->getCachedBuilder($provider); /** @var User $user */ $user = $builder ->where('objectClass', '=', $provider->getSchema()->person()) ->whereEquals($this->identifierAttributeName, $identifier) ->first(); if ($user === null) { throw new NotFoundHttpException(sprintf("Person with id '%s' could not be found!", $identifier)); } return $user; } catch (\Adldap\Auth\BindException $e) { // There was an issue binding / connecting to the server. throw new ApiError(Response::HTTP_BAD_GATEWAY, sprintf("Person with id '%s' could not be loaded! Message: %s", $identifier, CoreTools::filterErrorMessage($e->getMessage()))); } } public function personFromUserItem(User $user, bool $full): Person { $identifier = $user->getFirstAttribute($this->identifierAttributeName); $person = new Person(); $person->setIdentifier($identifier); $person->setGivenName($user->getFirstAttribute($this->givenNameAttributeName)); $person->setFamilyName($user->getFirstAttribute($this->familyNameAttributeName)); if ($this->emailAttributeName !== '') { $person->setEmail($user->getFirstAttribute($this->emailAttributeName)); } $birthDateString = $this->birthdayAttributeName !== '' ? trim($user->getFirstAttribute($this->birthdayAttributeName) ?? '') : ''; if ($birthDateString !== '') { $matches = []; if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $birthDateString, $matches)) { $person->setBirthDate("{$matches[1]}-{$matches[2]}-{$matches[3]}"); } } // TODO: Add code to decide what roles a user has (or just depend on the roles from CustomUserRoles) $roles = ['ROLE_SCOPE_GREENLIGHT']; $person->setExtraData('ldap-roles', $roles); // TODO: Allow injection of this setting $campusOnlineIdentifierAttribute = (string) $this->params->get('app.campusonline.person.identifier') ?? ''; // Used in \Dbp\Relay\LdapPersonProviderBundle\Service\CampusonlinePersonPhotoProvider::getPhotoData if ($campusOnlineIdentifierAttribute !== '' && $user->hasAttribute($campusOnlineIdentifierAttribute)) { $person->setExtraData($campusOnlineIdentifierAttribute, $user->getAttribute($campusOnlineIdentifierAttribute)[0]); } return $person; } public function getRolesForCurrentPerson(): array { $person = $this->getCurrentPerson(); if ($person !== null) { $roles = $person->getExtraData('ldap-roles'); assert(is_array($roles)); return $roles; } return []; } public function getPerson(string $id): Person { $id = str_replace('/people/', '', $id); $session = $this->getUserSession(); $currentIdentifier = $session->getUserIdentifier(); if ($currentIdentifier !== null && $currentIdentifier === $id) { // fast path: getCurrentPerson() does some caching $person = $this->getCurrentPerson(); assert($person !== null); } else { $user = $this->getPersonUserItem($id); $person = $this->personFromUserItem($user, true); } return $person; } public function getPersonForExternalService(string $service, string $serviceID): Person { throw new BadRequestHttpException("Unknown service: $service"); } private function getUserSession(): UserSessionInterface { return $this->locator->get(UserSessionInterface::class); } public function getCurrentPersonCached(): Person { $session = $this->getUserSession(); $currentIdentifier = $session->getUserIdentifier(); assert($currentIdentifier !== null); $cache = $this->personCache; $cacheKey = $session->getSessionCacheKey().'-'.$currentIdentifier; // make sure the cache is longer than the session, so just double it. $cacheTTL = $session->getSessionTTL() * 2; $item = $cache->getItem($cacheKey); if ($item->isHit()) { $person = $item->get(); if ($person === null) { throw new NotFoundHttpException(); } return $person; } else { try { $user = $this->getPersonUserItem($currentIdentifier); $person = $this->personFromUserItem($user, true); } catch (NotFoundHttpException $e) { $person = null; } $item->set($person); $item->expiresAfter($cacheTTL); $cache->save($item); if ($person === null) { throw new NotFoundHttpException(); } return $person; } } public function getCurrentPerson(): ?Person { $session = $this->getUserSession(); $currentIdentifier = $session->getUserIdentifier(); if ($currentIdentifier === null) { return null; } if ($this->currentPerson !== null && $this->currentPerson->getIdentifier() !== $currentIdentifier) { $this->currentPerson = null; } if ($this->currentPerson === null) { $this->currentPerson = $this->getCurrentPersonCached(); } return $this->currentPerson; } public static function getSubscribedServices() { return [ UserSessionInterface::class, ]; } }