├── .gitignore ├── phpspec.yml.dist ├── .scrutinizer.yml ├── .php_cs ├── spec └── LdapTools │ └── Bundle │ └── LdapToolsBundle │ ├── Annotation │ └── LdapObjectSpec.php │ ├── DependencyInjection │ ├── ConfigurationSpec.php │ ├── Compiler │ │ ├── LdifUrlLoaderPassSpec.php │ │ └── EventRegisterPassSpec.php │ └── Security │ │ └── Factory │ │ └── LdapFormLoginFactorySpec.php │ ├── Factory │ └── LdapFactorySpec.php │ ├── Event │ ├── LdapLoginEventSpec.php │ ├── LoadUserEventSpec.php │ └── AuthenticationHandlerEventSpec.php │ ├── Command │ └── SslCertificateCommandSpec.php │ ├── Doctrine │ └── Type │ │ ├── LdapObjectTypeSpec.php │ │ └── LdapObjectCollectionTypeSpec.php │ ├── LdapToolsBundleSpec.php │ ├── Security │ ├── Firewall │ │ └── LdapFormLoginListenerSpec.php │ └── User │ │ ├── LdapUserCheckerSpec.php │ │ ├── LdapUserSpec.php │ │ ├── LdapRoleMapperSpec.php │ │ └── LdapUserProviderSpec.php │ ├── CacheWarmer │ └── LdapToolsCacheWarmerSpec.php │ ├── Form │ ├── Type │ │ └── LdapObjectTypeSpec.php │ └── ChoiceLoader │ │ ├── LegacyLdapChoiceLoaderSpec.php │ │ └── LdapObjectChoiceLoaderSpec.php │ ├── Log │ ├── LdapProfilerLoggerSpec.php │ └── LdapLoggerSpec.php │ └── DataCollector │ └── LdapToolsDataCollectorSpec.php ├── Doctrine └── Type │ ├── LdapObjectType.php │ └── LdapObjectCollectionType.php ├── Annotation └── LdapObject.php ├── .travis.yml ├── appveyor.yml ├── LICENSE ├── Factory └── LdapFactory.php ├── Security ├── User │ ├── LdapUserInterface.php │ ├── LdapUserChecker.php │ ├── LdapUser.php │ ├── LdapRoleMapper.php │ └── LdapUserProvider.php ├── LdapAuthenticationTrait.php ├── Authentication │ └── Provider │ │ └── LdapAuthenticationProvider.php └── Firewall │ └── LdapFormLoginListener.php ├── composer.json ├── Resources └── doc │ ├── Bundle-Event-Reference.md │ ├── LDAP-Authentication-With-The-FOSUserBundle.md │ ├── LDAP-Object-Form-Type.md │ ├── LDIF-Parser-URL-Loaders.md │ ├── LDAP-Events.md │ └── Save-LDAP-Users-to-the-Database-After-Login.md ├── Form ├── ChoiceLoader │ ├── LegacyLdapChoiceLoader.php │ ├── LdapObjectChoiceTrait.php │ └── LdapObjectChoiceLoader.php ├── ChoiceList │ └── LdapObjectChoiceList.php └── Type │ └── LdapObjectType.php ├── Event ├── LdapLoginEvent.php ├── LoadUserEvent.php └── AuthenticationHandlerEvent.php ├── DependencyInjection ├── Compiler │ ├── LdifUrlLoaderPass.php │ └── EventRegisterPass.php ├── Security │ └── Factory │ │ └── LdapFormLoginFactory.php └── LdapToolsExtension.php ├── Command └── SslCertificateCommand.php ├── Log ├── LdapProfilerLogger.php └── LdapLogger.php ├── LdapToolsBundle.php ├── CacheWarmer └── LdapToolsCacheWarmer.php ├── CHANGELOG.md ├── DataCollector └── LdapToolsDataCollector.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | composer.phar 3 | .idea 4 | vendor/ 5 | bin/ -------------------------------------------------------------------------------- /phpspec.yml.dist: -------------------------------------------------------------------------------- 1 | formatter.name: pretty 2 | extensions: 3 | Akeneo\SkipExampleExtension: ~ -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - php 3 | 4 | filter: 5 | excluded_paths: 6 | - spec/* 7 | - Resources/* -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | exclude('spec') 5 | ->in(__DIR__) 6 | ; 7 | 8 | return Symfony\CS\Config\Config::create() 9 | ->finder($finder) 10 | ->level(Symfony\CS\FixerInterface::PSR2_LEVEL) 11 | ; -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/Annotation/LdapObjectSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\Annotation; 12 | 13 | use PhpSpec\ObjectBehavior; 14 | 15 | class LdapObjectSpec extends ObjectBehavior 16 | { 17 | function it_is_initializable() 18 | { 19 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\Annotation\LdapObject'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/DependencyInjection/ConfigurationSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\DependencyInjection; 12 | 13 | use PhpSpec\ObjectBehavior; 14 | 15 | class ConfigurationSpec extends ObjectBehavior 16 | { 17 | function let() 18 | { 19 | $this->beConstructedWith(false); 20 | } 21 | 22 | function it_is_initializable() 23 | { 24 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\DependencyInjection\Configuration'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Doctrine/Type/LdapObjectType.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Doctrine\Type; 12 | 13 | use Doctrine\DBAL\Types\TextType; 14 | 15 | /** 16 | * More or less serves as an alias for a text type to store a single LdapObject. 17 | * 18 | * @author Chad Sikorra 19 | */ 20 | class LdapObjectType extends TextType 21 | { 22 | const TYPE = 'ldap_object'; 23 | 24 | /** 25 | * @inheritdoc 26 | */ 27 | public function getName() 28 | { 29 | return self::TYPE; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Doctrine/Type/LdapObjectCollectionType.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Doctrine\Type; 12 | 13 | use Doctrine\DBAL\Types\ArrayType; 14 | 15 | /** 16 | * More or less serves as an alias for an array type for storing a collection of LdapObject's. 17 | * 18 | * @author Chad Sikorra 19 | */ 20 | class LdapObjectCollectionType extends ArrayType 21 | { 22 | const TYPE = 'ldap_object_collection'; 23 | 24 | /** 25 | * @inheritdoc 26 | */ 27 | public function getName() 28 | { 29 | return self::TYPE; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Annotation/LdapObject.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Annotation; 12 | 13 | /** 14 | * @Annotation 15 | * @Target("PROPERTY") 16 | */ 17 | class LdapObject 18 | { 19 | /** 20 | * @Required 21 | * @var string 22 | */ 23 | public $type; 24 | 25 | /** 26 | * @var string 27 | */ 28 | public $domain; 29 | 30 | /** 31 | * @var string 32 | */ 33 | public $id = 'guid'; 34 | 35 | /** 36 | * @var bool 37 | */ 38 | public $collection = false; 39 | 40 | /** 41 | * @var array 42 | */ 43 | public $attributes = []; 44 | } 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.6 5 | - 7.0 6 | - 7.1 7 | - 7.2 8 | 9 | cache: 10 | directories: 11 | - $HOME/.composer/cache/files 12 | 13 | matrix: 14 | fast_finish: true 15 | include: 16 | - php: 5.6 17 | env: COMPOSER_FLAGS="--prefer-lowest" SYMFONY_DEPRECATIONS_HELPER=weak 18 | - php: 7.0 19 | env: SYMFONY_VERSION='2.8.*' 20 | - php: 7.0 21 | env: SYMFONY_VERSION='3.1.*' 22 | - php: 7.1 23 | env: SYMFONY_VERSION='3.2.*' 24 | 25 | before_install: 26 | - echo "extension=ldap.so" >> `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"` 27 | - composer self-update 28 | - if [ "$SYMFONY_VERSION" != "" ]; then composer require --dev --no-update symfony/symfony=$SYMFONY_VERSION; fi 29 | 30 | install: composer update $COMPOSER_FLAGS --prefer-dist 31 | 32 | script: 33 | - bin/phpspec run --format=pretty --no-interaction 34 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | build: off 2 | shallow_clone: false 3 | platform: 'x64' 4 | clone_folder: c:\projects\ldaptools-bundle 5 | 6 | environment: 7 | matrix: 8 | - PHP_VERSION: "5.6" 9 | - PHP_VERSION: "7.0" 10 | - PHP_VERSION: "7.1" 11 | - PHP_VERSION: "7.2" 12 | 13 | install: 14 | - ps: Invoke-WebRequest "https://raw.githubusercontent.com/ChadSikorra/ps-install-php/master/Install-PHP.ps1" -OutFile "Install-PHP.ps1" 15 | - ps: .\Install-PHP.ps1 -Version $Env:PHP_VERSION -Highest -Arch x64 -Extensions ldap,mbstring,intl,openssl 16 | - refreshenv 17 | - cd C:\projects\ldaptools-bundle 18 | - php -r "readfile('https://getcomposer.org/installer');" | php 19 | 20 | before_test: 21 | - cd C:\projects\ldaptools-bundle 22 | - php composer.phar install --no-interaction --no-progress --optimize-autoloader --prefer-source --no-ansi 23 | 24 | test_script: 25 | - cd C:\projects\ldaptools-bundle 26 | - bin\phpspec.bat run 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Chad Sikorra 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /Factory/LdapFactory.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Factory; 12 | 13 | use LdapTools\Connection\LdapConnection; 14 | use LdapTools\DomainConfiguration; 15 | 16 | /** 17 | * Used to assist in making components needing active connections more spec/test'able. 18 | * 19 | * @author Chad Sikorra 20 | */ 21 | class LdapFactory 22 | { 23 | /** 24 | * @param string $domain 25 | * @return DomainConfiguration 26 | */ 27 | public function getConfig($domain) 28 | { 29 | return new DomainConfiguration($domain); 30 | } 31 | 32 | /** 33 | * @param DomainConfiguration $config 34 | * @return LdapConnection 35 | */ 36 | public function getConnection(DomainConfiguration $config) 37 | { 38 | return new LdapConnection($config); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/Factory/LdapFactorySpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\Factory; 12 | 13 | use LdapTools\Bundle\LdapToolsBundle\Factory\LdapFactory; 14 | use LdapTools\DomainConfiguration; 15 | use PhpSpec\ObjectBehavior; 16 | use Prophecy\Argument; 17 | 18 | class LdapFactorySpec extends ObjectBehavior 19 | { 20 | function it_is_initializable() 21 | { 22 | $this->shouldHaveType(LdapFactory::class); 23 | } 24 | 25 | function it_should_get_a_domain_configuration() 26 | { 27 | $this->getConfig('foo')->shouldReturnAnInstanceOf('LdapTools\DomainConfiguration'); 28 | } 29 | 30 | function it_should_get_a_ldap_connection() 31 | { 32 | $this->getConnection((new DomainConfiguration('foo'))->setLazyBind(true)) 33 | ->shouldReturnAnInstanceOf('LdapTools\Connection\LdapConnectionInterface'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Security/User/LdapUserInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Security\User; 12 | 13 | /** 14 | * Any user loaded from the LdapUserProvider must implement this interface. 15 | * 16 | * @author Chad Sikorra 17 | */ 18 | interface LdapUserInterface 19 | { 20 | /** 21 | * Sets the username for the user. 22 | * 23 | * @param string $username 24 | */ 25 | public function setUsername($username); 26 | 27 | /** 28 | * Set the GUID used to uniquely identify the user in LDAP. 29 | * 30 | * @param string $guid 31 | */ 32 | public function setLdapGuid($guid); 33 | 34 | /** 35 | * Get the GUID used to uniquely identify the user in LDAP. 36 | * 37 | * @return string 38 | */ 39 | public function getLdapGuid(); 40 | 41 | /** 42 | * Sets the roles for the user. 43 | * 44 | * @param array $roles 45 | */ 46 | public function setRoles(array $roles); 47 | } 48 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/Event/LdapLoginEventSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\Event; 12 | 13 | use LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUser; 14 | use LdapTools\Object\LdapObject; 15 | use PhpSpec\ObjectBehavior; 16 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 17 | 18 | class LdapLoginEventSpec extends ObjectBehavior 19 | { 20 | function let(TokenInterface $token) 21 | { 22 | $this->beConstructedWith(new LdapUser(new LdapObject(['foo' =>'bar'])), $token); 23 | } 24 | 25 | function it_is_initializable() 26 | { 27 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\Event\LdapLoginEvent'); 28 | } 29 | 30 | function it_should_get_the_user() 31 | { 32 | $this->getUser()->shouldReturnAnInstanceOf('LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUser'); 33 | } 34 | 35 | function it_should_get_the_token($token) 36 | { 37 | $this->getToken()->shouldBeEqualTo($token); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ldaptools/ldaptools-bundle", 3 | "type": "symfony-bundle", 4 | "description": "Provides easy LDAP integration for Symfony via LdapTools.", 5 | "keywords": [ 6 | "LDAP", 7 | "Active Directory", 8 | "OpenLDAP", 9 | "Microsoft Exchange" 10 | ], 11 | "homepage": "http://github.com/ldaptools/LdapToolsBundle", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Chad Sikorra", 16 | "email": "Chad.Sikorra@gmail.com", 17 | "homepage": "http://www.chadsikorra.com" 18 | } 19 | ], 20 | "require": { 21 | "ldaptools/ldaptools": ">=0.25", 22 | "symfony/framework-bundle": "~2.7|~3.0|~4.0" 23 | }, 24 | "require-dev": { 25 | "friendsofphp/php-cs-fixer": "~1.0", 26 | "phpspec/phpspec": "~3.0", 27 | "akeneo/phpspec-skip-example-extension": "~2.0", 28 | "doctrine/orm": "~2.4", 29 | "symfony/form": "~2.7|~3.0|~4.0", 30 | "symfony/security-bundle": "~2.7|~3.0|~4.0" 31 | }, 32 | "suggest": { 33 | "doctrine/doctrine-bundle": "For LDAP object type integration in Doctrine." 34 | }, 35 | "config": { 36 | "bin-dir": "bin" 37 | }, 38 | "autoload": { 39 | "psr-4": {"LdapTools\\Bundle\\LdapToolsBundle\\": ""} 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/Command/SslCertificateCommandSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\Command; 12 | 13 | use LdapTools\Bundle\LdapToolsBundle\Command\SslCertificateCommand; 14 | use PhpSpec\ObjectBehavior; 15 | use Symfony\Component\Console\Helper\HelperSet; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | class SslCertificateCommandSpec extends ObjectBehavior 20 | { 21 | function let(InputInterface $input, OutputInterface $output, HelperSet $helperSet) 22 | { 23 | $this->setHelperSet($helperSet); 24 | $input->getOption('server')->willReturn(null); 25 | $input->getOption('port')->willReturn(389); 26 | } 27 | 28 | function it_is_initializable() 29 | { 30 | $this->shouldHaveType(SslCertificateCommand::class); 31 | } 32 | 33 | function it_should_require_a_server_name($input, $output) 34 | { 35 | $this->shouldThrow(new \Exception("You must enter a server name."))->duringExecute($input, $output); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/Doctrine/Type/LdapObjectTypeSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\Doctrine\Type; 12 | 13 | use Doctrine\DBAL\Types\Type; 14 | use LdapTools\Bundle\LdapToolsBundle\Doctrine\Type\LdapObjectType; 15 | use PhpSpec\ObjectBehavior; 16 | 17 | class LdapObjectTypeSpec extends ObjectBehavior 18 | { 19 | function let() 20 | { 21 | $type = LdapObjectType::TYPE; 22 | 23 | if (!Type::hasType($type)) { 24 | Type::addType($type, '\LdapTools\Bundle\LdapToolsBundle\Doctrine\Type\LdapObjectType'); 25 | } 26 | 27 | $this->beConstructedThrough('getType', [$type]); 28 | } 29 | 30 | function it_is_initializable() 31 | { 32 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\Doctrine\Type\LdapObjectType'); 33 | } 34 | 35 | function it_should_extend_the_DBAL_text_type() 36 | { 37 | $this->shouldBeAnInstanceOf('\Doctrine\DBAL\Types\TextType'); 38 | } 39 | 40 | function it_should_get_the_name() 41 | { 42 | $this->getName()->shouldBeEqualTo('ldap_object'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/Doctrine/Type/LdapObjectCollectionTypeSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\Doctrine\Type; 12 | 13 | use Doctrine\DBAL\Types\Type; 14 | use LdapTools\Bundle\LdapToolsBundle\Doctrine\Type\LdapObjectCollectionType; 15 | use PhpSpec\ObjectBehavior; 16 | 17 | class LdapObjectCollectionTypeSpec extends ObjectBehavior 18 | { 19 | function let() 20 | { 21 | $type = LdapObjectCollectionType::TYPE; 22 | 23 | if (!Type::hasType($type)) { 24 | Type::addType($type, '\LdapTools\Bundle\LdapToolsBundle\Doctrine\Type\LdapObjectCollectionType'); 25 | } 26 | 27 | $this->beConstructedThrough('getType', [$type]); 28 | } 29 | 30 | function it_is_initializable() 31 | { 32 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\Doctrine\Type\LdapObjectCollectionType'); 33 | } 34 | 35 | function it_should_extend_the_DBAL_array_type() 36 | { 37 | $this->shouldBeAnInstanceOf('\Doctrine\DBAL\Types\ArrayType'); 38 | } 39 | 40 | function it_should_get_the_name() 41 | { 42 | $this->getName()->shouldBeEqualTo('ldap_object_collection'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Resources/doc/Bundle-Event-Reference.md: -------------------------------------------------------------------------------- 1 | Bundle Event Reference 2 | ================ 3 | 4 | These events are specific to the LdapToolsBundle. You can tag a service with `kernel.event_listener` to hook into them: 5 | 6 | ```yaml 7 | # app/config/services.yml 8 | app.event.login_listener: 9 | class: AppBundle\Event\LoadUserListener 10 | tags: 11 | - { name: kernel.event_listener, event: ldap_tools_bundle.load_user.before, method: beforeLoadUser } 12 | 13 | ``` 14 | 15 | | Event Name | Event Class Used | Description | 16 | | --------------- | -------------- | ---------- | 17 | | ldap_tools_bundle.load_user.before | `LoadUserEvent` | Triggered before a LDAP user is loaded from the LdapUserProvider. | 18 | | ldap_tools_bundle.load_user.after | `LoadUserEvent` | Triggered after a LDAP user is loaded from the LdapUserProvider. | 19 | | ldap_tools_bundle.login.success | `LdapLoginEvent` | Triggered directly after a successful LDAP login/bind in the Guard or Auth provider. | 20 | | ldap_tools_bundle.guard.login.start | `AuthenticationHandlerEvent` | Triggered in the Guard when the entry point is called. Can set the response object here. | 21 | | ldap_tools_bundle.guard.login.success | `AuthenticationHandlerEvent` | Triggered in the Guard on successful authentication. Can set the response object here. | 22 | | ldap_tools_bundle.guard.login.failure | `AuthenticationHandlerEvent` | Triggered in the Guard on failed authentication. Can set the response object here. | 23 | -------------------------------------------------------------------------------- /Form/ChoiceLoader/LegacyLdapChoiceLoader.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Form\ChoiceLoader; 12 | 13 | use LdapTools\LdapManager; 14 | use LdapTools\Object\LdapObject; 15 | 16 | /** 17 | * Gets an array of LDAP objects to be passed to the ObjectChoiceList for pre-Symfony 2.7 compatibility. 18 | * 19 | * @author Chad Sikorra 20 | */ 21 | class LegacyLdapChoiceLoader 22 | { 23 | use LdapObjectChoiceTrait; 24 | 25 | /** 26 | * @param LdapManager $ldap 27 | * @param string $type The LDAP object type. 28 | * @param string $labelAttribute The LDAP attribute to use for the label. 29 | * @param string $id The attribute to use for the ID. 30 | * @param LdapQueryBuilder|\Closure|null 31 | */ 32 | public function __construct(LdapManager $ldap, $type, $labelAttribute = 'name', $id = 'guid', $query = null) 33 | { 34 | $this->ldap = $ldap; 35 | $this->type = $type; 36 | $this->id = $id; 37 | $this->labelAttribute = $labelAttribute; 38 | $this->setClosureOrQuery($query); 39 | } 40 | 41 | /** 42 | * @return LdapObject[] 43 | */ 44 | public function load() 45 | { 46 | return $this->getLdapObjectsByQuery()->toArray(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/LdapToolsBundleSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle; 12 | 13 | use Doctrine\DBAL\Types\Type; 14 | use LdapTools\Bundle\LdapToolsBundle\DependencyInjection\Compiler\EventRegisterPass; 15 | use LdapTools\Bundle\LdapToolsBundle\DependencyInjection\Compiler\LdifUrlLoaderPass; 16 | use LdapTools\Bundle\LdapToolsBundle\DependencyInjection\Security\Factory\LdapFormLoginFactory; 17 | use PhpSpec\ObjectBehavior; 18 | use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension; 19 | use Symfony\Component\DependencyInjection\ContainerBuilder; 20 | 21 | class LdapToolsBundleSpec extends ObjectBehavior 22 | { 23 | function it_is_initializable() 24 | { 25 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\LdapToolsBundle'); 26 | } 27 | 28 | function it_should_add_the_security_listener_factory_and_compiler_pass_when_calling_build(ContainerBuilder $container, SecurityExtension $extension) 29 | { 30 | $extension->addSecurityListenerFactory(new LdapFormLoginFactory())->shouldBeCalled(); 31 | $container->getExtension('security')->willReturn($extension); 32 | $container->addCompilerPass(new EventRegisterPass())->shouldBeCalled(); 33 | $container->addCompilerPass(new LdifUrlLoaderPass())->shouldBeCalled(); 34 | 35 | $this->build($container); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Event/LdapLoginEvent.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Event; 12 | 13 | use LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUser; 14 | use Symfony\Component\EventDispatcher\Event; 15 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 16 | use Symfony\Component\Security\Core\User\UserInterface; 17 | 18 | /** 19 | * Represents a LDAP login event. 20 | * 21 | * @author Chad Sikorra 22 | */ 23 | class LdapLoginEvent extends Event 24 | { 25 | /** 26 | * The event name when the login was successful 27 | */ 28 | const SUCCESS = 'ldap_tools_bundle.login.success'; 29 | 30 | /** 31 | * @var UserInterface|LdapUser 32 | */ 33 | protected $user; 34 | 35 | /** 36 | * @var TokenInterface 37 | */ 38 | protected $token; 39 | 40 | /** 41 | * @param UserInterface|LdapUser $user 42 | * @param TokenInterface $token 43 | */ 44 | public function __construct(UserInterface $user, TokenInterface $token) 45 | { 46 | $this->user = $user; 47 | $this->token = $token; 48 | } 49 | 50 | /** 51 | * @return UserInterface|LdapUser 52 | */ 53 | public function getUser() 54 | { 55 | return $this->user; 56 | } 57 | 58 | /** 59 | * @return TokenInterface 60 | */ 61 | public function getToken() 62 | { 63 | return $this->token; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/Event/LoadUserEventSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\Event; 12 | 13 | use LdapTools\Bundle\LdapToolsBundle\Event\LoadUserEvent; 14 | use LdapTools\Object\LdapObject; 15 | use PhpSpec\ObjectBehavior; 16 | use Symfony\Component\Security\Core\User\UserInterface; 17 | 18 | class LoadUserEventSpec extends ObjectBehavior 19 | { 20 | function let() 21 | { 22 | $this->beConstructedWith('foo', 'example.local'); 23 | } 24 | 25 | function it_is_initializable() 26 | { 27 | $this->shouldHaveType(LoadUserEvent::class); 28 | } 29 | 30 | function the_user_should_be_null_by_default() 31 | { 32 | $this->getUser()->shouldBeNull(); 33 | } 34 | 35 | function it_should_get_the_username() 36 | { 37 | $this->getUsername()->shouldEqual('foo'); 38 | } 39 | 40 | function it_should_get_the_domain() 41 | { 42 | $this->getDomain()->shouldEqual('example.local'); 43 | } 44 | 45 | function it_should_allow_being_constructed_with_a_user(UserInterface $user) 46 | { 47 | $this->beConstructedWith('foo', 'example.local', $user); 48 | 49 | $this->getUser()->shouldBeEqualTo($user); 50 | } 51 | 52 | function it_should_allow_being_constructed_with_a_ldap_object(UserInterface $user, LdapObject $ldapObject) 53 | { 54 | $this->beConstructedWith('foo', 'example.local', $user, $ldapObject); 55 | 56 | $this->getLdapObject()->shouldBeEqualTo($ldapObject); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/LdifUrlLoaderPass.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\DependencyInjection\Compiler; 12 | 13 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 14 | use Symfony\Component\DependencyInjection\ContainerBuilder; 15 | use Symfony\Component\DependencyInjection\Reference; 16 | 17 | /** 18 | * Add any services tagged as a LDIF URL loader to the LDIF parser service. 19 | * 20 | * @author Chad Sikorra 21 | */ 22 | class LdifUrlLoaderPass implements CompilerPassInterface 23 | { 24 | /** 25 | * The LDIF URL loader tag name. 26 | */ 27 | const LDIF_URL_LOADER_TAG = 'ldap_tools.ldif_url_loader'; 28 | 29 | /** 30 | * The LDIF parser service name. 31 | */ 32 | const LDIF_PARSER = 'ldap_tools.ldif_parser'; 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function process(ContainerBuilder $container) 38 | { 39 | $urlLoaders = $container->findTaggedServiceIds(self::LDIF_URL_LOADER_TAG); 40 | if (empty($urlLoaders)) { 41 | return; 42 | } 43 | $parser = $container->findDefinition(self::LDIF_PARSER); 44 | 45 | foreach ($urlLoaders as $id => $loader) { 46 | if (!isset($loader[0]['type'])) { 47 | throw new \InvalidArgumentException(sprintf('Service "%s" must define the "type" attribute on "%s" tags.', $id, self::LDIF_URL_LOADER_TAG)); 48 | } 49 | $parser->addMethodCall('setUrlLoader', [$loader[0]['type'], new Reference($id)]); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/Event/AuthenticationHandlerEventSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\Event; 12 | 13 | use LdapTools\Bundle\LdapToolsBundle\Event\AuthenticationHandlerEvent; 14 | use PhpSpec\ObjectBehavior; 15 | use Symfony\Component\HttpFoundation\RedirectResponse; 16 | use Symfony\Component\HttpFoundation\Request; 17 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 18 | use Symfony\Component\Security\Core\Exception\AuthenticationException; 19 | 20 | class AuthenticationHandlerEventSpec extends ObjectBehavior 21 | { 22 | function let(RedirectResponse $redirectResponse, Request $request) 23 | { 24 | $this->beConstructedWith($redirectResponse, $request); 25 | } 26 | 27 | function it_is_initializable() 28 | { 29 | $this->shouldHaveType(AuthenticationHandlerEvent::class); 30 | } 31 | 32 | function it_should_set_and_get_the_response($redirectResponse) 33 | { 34 | $newResponse = new RedirectResponse('/foo'); 35 | 36 | $this->getResponse()->shouldBeEqualTo($redirectResponse); 37 | $this->setResponse($newResponse)->getResponse()->shouldBeEqualTo($newResponse); 38 | } 39 | 40 | function it_should_be_constructed_with_an_exception_provider_key_and_token($redirectResponse, $request, TokenInterface $token) 41 | { 42 | $exception = new AuthenticationException('foo'); 43 | $this->beConstructedWith($redirectResponse, $request, $exception, $token, 'foo'); 44 | 45 | $this->getToken()->shouldBeEqualTo($token); 46 | $this->getRequest()->shouldBeEqualTo($request); 47 | $this->getProviderKey()->shouldBeEqualTo('foo'); 48 | $this->getException()->shouldBeEqualTo($exception); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /DependencyInjection/Security/Factory/LdapFormLoginFactory.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\DependencyInjection\Security\Factory; 12 | 13 | use Symfony\Component\DependencyInjection\ContainerBuilder; 14 | use Symfony\Component\DependencyInjection\Reference; 15 | use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory; 16 | 17 | /** 18 | * The LDAP form login factory. 19 | * 20 | * @author Chad Sikorra 21 | */ 22 | class LdapFormLoginFactory extends FormLoginFactory 23 | { 24 | public function __construct() 25 | { 26 | parent::__construct(); 27 | $this->addOption('domain_parameter', '_ldap_domain'); 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function getKey() 34 | { 35 | return 'ldap-tools-form'; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function getListenerId() 42 | { 43 | return 'ldap_tools.security.firewall.ldap_form_login_listener'; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId) 50 | { 51 | $provider = 'ldap_tools.security.user.ldap_user_provider.'.$id; 52 | $decorator = class_exists('Symfony\Component\DependencyInjection\ChildDefinition') ? 53 | 'Symfony\Component\DependencyInjection\ChildDefinition' : 54 | 'Symfony\Component\DependencyInjection\DefinitionDecorator'; 55 | 56 | $container->setDefinition($provider, new $decorator('ldap_tools.security.authentication.ldap_authentication_provider')) 57 | ->replaceArgument(0, $id) 58 | ->replaceArgument(2, new Reference($userProviderId)); 59 | 60 | return $provider; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/Security/Firewall/LdapFormLoginListenerSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\Security\Firewall; 12 | 13 | use PhpSpec\ObjectBehavior; 14 | use Psr\Log\LoggerInterface; 15 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 16 | use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; 17 | use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; 18 | use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; 19 | use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; 20 | use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; 21 | use Symfony\Component\Security\Http\HttpUtils; 22 | use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; 23 | 24 | class LdapFormLoginListenerSpec extends ObjectBehavior 25 | { 26 | function let(AuthenticationManagerInterface $authManager, SessionAuthenticationStrategyInterface $authStrategy, HttpUtils $httpUtils, AuthenticationSuccessHandlerInterface $authSuccess, AuthenticationFailureHandlerInterface $authFailure, LoggerInterface $logger, EventDispatcherInterface $eventDispatcher, CsrfTokenManagerInterface $csrfTokenManager) 27 | { 28 | 29 | $this->beConstructedWith( 30 | new TokenStorage(), 31 | $authManager, 32 | $authStrategy, 33 | $httpUtils, 34 | 'restricted', 35 | $authSuccess, 36 | $authFailure, 37 | [], 38 | $logger, 39 | $eventDispatcher, 40 | $csrfTokenManager 41 | ); 42 | } 43 | 44 | function it_is_initializable() 45 | { 46 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\Security\Firewall\LdapFormLoginListener'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Command/SslCertificateCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Command; 12 | 13 | use Symfony\Component\Console\Command\Command; 14 | use Symfony\Component\Console\Input\InputInterface; 15 | use Symfony\Component\Console\Input\InputOption; 16 | use Symfony\Component\Console\Output\OutputInterface; 17 | use LdapTools\Utilities\LdapUtilities; 18 | 19 | /** 20 | * Retrieves the LDAP SSL certificate from a given server and generates the certificate bundle for it. 21 | * 22 | * @author Chad Sikorra 23 | */ 24 | class SslCertificateCommand extends Command 25 | { 26 | /** 27 | * {@inheritDoc} 28 | */ 29 | protected function configure() 30 | { 31 | $this 32 | ->setName('ldaptools:generate:sslcert') 33 | ->addOption('server', null, InputOption::VALUE_REQUIRED, 'The LDAP server name.') 34 | ->addOption('port', null, InputOption::VALUE_OPTIONAL, 'The LDAP port number.', 389) 35 | ->setDescription('Retrieves the LDAP SSL/TLS certificate from a server and generates the certificate bundle.'); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function execute(InputInterface $input, OutputInterface $output) 42 | { 43 | $server = trim($input->getOption('server')); 44 | $port = (int) $input->getOption('port'); 45 | if (!$server) { 46 | throw new \Exception('You must enter a server name.'); 47 | } 48 | 49 | $certs = LdapUtilities::getLdapSslCertificates($server, $port); 50 | $bundle = $certs['peer_certificate'].implode('', $certs['peer_certificate_chain']); 51 | if (empty($bundle)) { 52 | $output->writeln(sprintf('Unable to retrieve SSL certificate from %s on port %s.', $server, $port)); 53 | 54 | return 1; 55 | } 56 | $output->writeln($bundle); 57 | 58 | return 0; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Security/User/LdapUserChecker.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Security\User; 12 | 13 | use LdapTools\Enums\AD\ResponseCode; 14 | use LdapTools\Connection\LdapConnection; 15 | use Symfony\Component\Security\Core\Exception\CredentialsExpiredException; 16 | use Symfony\Component\Security\Core\Exception\DisabledException; 17 | use Symfony\Component\Security\Core\Exception\LockedException; 18 | use Symfony\Component\Security\Core\User\UserChecker; 19 | use Symfony\Component\Security\Core\User\UserInterface; 20 | 21 | /** 22 | * Interpret extended LDAP codes from authentication to determine the state of the LDAP account. 23 | * 24 | * @author Chad Sikorra 25 | */ 26 | class LdapUserChecker extends UserChecker 27 | { 28 | /** 29 | * Based on the LDAP error code and the LDAP type, throw any specific exceptions detected. 30 | * 31 | * @param UserInterface $user The user object. 32 | * @param int $code The extended LDAP error code. 33 | * @param string $ldapType The LDAP type used for authentication. 34 | */ 35 | public function checkLdapErrorCode(UserInterface $user, $code, $ldapType) 36 | { 37 | if ($ldapType == LdapConnection::TYPE_AD && $code == ResponseCode::AccountLocked) { 38 | $ex = new LockedException('User account is locked.'); 39 | $ex->setUser($user); 40 | throw $ex; 41 | } 42 | 43 | if ($ldapType == LdapConnection::TYPE_AD && $code == ResponseCode::AccountPasswordMustChange) { 44 | $ex = new CredentialsExpiredException('User credentials have expired.'); 45 | $ex->setUser($user); 46 | throw $ex; 47 | } 48 | 49 | if ($ldapType == LdapConnection::TYPE_AD && $code == ResponseCode::AccountDisabled) { 50 | $ex = new DisabledException('User account is disabled.'); 51 | $ex->setUser($user); 52 | throw $ex; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Log/LdapProfilerLogger.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Log; 12 | 13 | use LdapTools\Log\LdapLoggerInterface; 14 | use LdapTools\Log\LogOperation; 15 | 16 | /** 17 | * Handles LDAP operation logging for use within the profiler. 18 | * 19 | * @author Chad Sikorra 20 | */ 21 | class LdapProfilerLogger implements LdapLoggerInterface 22 | { 23 | /** 24 | * @var LogOperation[] 25 | */ 26 | protected $opsByDomain = []; 27 | 28 | /** 29 | * @var LogOperation[] 30 | */ 31 | protected $allOperations = []; 32 | 33 | /** 34 | * @var LogOperation[] 35 | */ 36 | protected $errors = []; 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function start(LogOperation $operation) 42 | { 43 | if (!isset($this->opsByDomain[$operation->getDomain()])) { 44 | $this->opsByDomain[$operation->getDomain()] = []; 45 | } 46 | $this->opsByDomain[$operation->getDomain()][] = $operation; 47 | $this->allOperations[] = $operation; 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function end(LogOperation $operation) 54 | { 55 | if (!is_null($operation->getError())) { 56 | $this->errors[] = $operation; 57 | } 58 | } 59 | 60 | /** 61 | * Get all of the operations recorded by the profiler. Or get the operations for a specific domain. 62 | * 63 | * @param null|string $domain 64 | * @return LogOperation[] 65 | */ 66 | public function getOperations($domain = null) 67 | { 68 | if (!is_null($domain) && !isset($this->opsByDomain[$domain])) { 69 | return []; 70 | } elseif (!is_null($domain)) { 71 | return $this->opsByDomain[$domain]; 72 | } 73 | 74 | return $this->allOperations; 75 | } 76 | 77 | /** 78 | * Get all the operations that had errors. 79 | * 80 | * @return LogOperation[] 81 | */ 82 | public function getErrors() 83 | { 84 | return $this->errors; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /LdapToolsBundle.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle; 12 | 13 | use LdapTools\Bundle\LdapToolsBundle\DependencyInjection\Compiler\EventRegisterPass; 14 | use LdapTools\Bundle\LdapToolsBundle\DependencyInjection\Compiler\LdifUrlLoaderPass; 15 | use LdapTools\Bundle\LdapToolsBundle\DependencyInjection\Security\Factory\LdapFormLoginFactory; 16 | use LdapTools\Bundle\LdapToolsBundle\Doctrine\Type\LdapObjectCollectionType; 17 | use LdapTools\Bundle\LdapToolsBundle\Doctrine\Type\LdapObjectType; 18 | use Symfony\Component\DependencyInjection\ContainerBuilder; 19 | use Symfony\Component\HttpKernel\Bundle\Bundle; 20 | use Doctrine\DBAL\Types\Type; 21 | 22 | /** 23 | * The Bundle class. 24 | * 25 | * @author Chad Sikorra 26 | */ 27 | class LdapToolsBundle extends Bundle 28 | { 29 | public function __construct() 30 | { 31 | // It's useful to auto-register the types, but we should not assume they are using doctrine... 32 | if (!class_exists('\Doctrine\DBAL\Types\Type')) { 33 | return; 34 | } 35 | 36 | if (!Type::hasType(LdapObjectType::TYPE)) { 37 | Type::addType( 38 | LdapObjectType::TYPE, 39 | '\LdapTools\Bundle\LdapToolsBundle\Doctrine\Type\LdapObjectType' 40 | ); 41 | } 42 | if (!Type::hasType(LdapObjectCollectionType::TYPE)) { 43 | Type::addType( 44 | LdapObjectCollectionType::TYPE, 45 | '\LdapTools\Bundle\LdapToolsBundle\Doctrine\Type\LdapObjectCollectionType' 46 | ); 47 | } 48 | } 49 | 50 | /** 51 | * Make Symfony aware of the LDAP listener factory and add the compiler pass. 52 | * 53 | * @param ContainerBuilder $container 54 | */ 55 | public function build(ContainerBuilder $container) 56 | { 57 | parent::build($container); 58 | $extension = $container->getExtension('security'); 59 | $extension->addSecurityListenerFactory(new LdapFormLoginFactory()); 60 | $container->addCompilerPass(new EventRegisterPass()); 61 | $container->addCompilerPass(new LdifUrlLoaderPass()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/CacheWarmer/LdapToolsCacheWarmerSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\CacheWarmer; 12 | 13 | use LdapTools\Configuration; 14 | use LdapTools\DomainConfiguration; 15 | use LdapTools\Factory\LdapObjectSchemaFactory; 16 | use LdapTools\LdapManager; 17 | use LdapTools\Schema\LdapObjectSchema; 18 | use LdapTools\Schema\Parser\SchemaParserInterface; 19 | use PhpSpec\ObjectBehavior; 20 | 21 | class LdapToolsCacheWarmerSpec extends ObjectBehavior 22 | { 23 | function let(LdapManager $ldap, Configuration $configuration) 24 | { 25 | $this->beConstructedWith($ldap, $configuration); 26 | } 27 | 28 | function it_is_initializable() 29 | { 30 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\CacheWarmer\LdapToolsCacheWarmer'); 31 | } 32 | 33 | function it_should_be_optional() 34 | { 35 | $this->isOptional()->shouldBeEqualTo(true); 36 | } 37 | 38 | function it_should_warm_up_the_cache($ldap, $configuration, SchemaParserInterface $parser, LdapObjectSchemaFactory $schemaFactory) 39 | { 40 | $ldap->getSchemaParser()->willReturn($parser); 41 | $ldap->getDomainContext()->willReturn('foo'); 42 | $ldap->getSchemaFactory()->willReturn($schemaFactory); 43 | 44 | // It will switch to each domain as it loops. But it should call the original context twice... 45 | $ldap->switchDomain('foo')->shouldBeCalledTimes(2); 46 | $ldap->switchDomain('bar')->shouldBeCalledTimes(1); 47 | 48 | // When the schema factory get is called it will take care of the caching... 49 | $schemaFactory->get('ad', 'foo')->shouldBeCalled()->willReturn(null); 50 | $schemaFactory->get('openldap', 'bar')->shouldBeCalled()->willReturn(null); 51 | 52 | $domainOne = new DomainConfiguration('foo'); 53 | $domainTwo = (new DomainConfiguration('bar'))->setLdapType('openldap'); 54 | $configuration->getDomainConfiguration()->willReturn([$domainOne, $domainTwo]); 55 | 56 | $parser->parseAll('ad')->shouldBeCalled()->willReturn([new LdapObjectSchema('ad', 'foo')]); 57 | $parser->parseAll('openldap')->shouldBeCalled()->willReturn([new LdapObjectSchema('openldap', 'bar')]); 58 | 59 | $this->warmUp('/foo'); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/DependencyInjection/Compiler/LdifUrlLoaderPassSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\DependencyInjection\Compiler; 12 | 13 | use LdapTools\Bundle\LdapToolsBundle\DependencyInjection\Compiler\LdifUrlLoaderPass; 14 | use PhpSpec\ObjectBehavior; 15 | use Prophecy\Argument; 16 | use Symfony\Component\DependencyInjection\ContainerBuilder; 17 | use Symfony\Component\DependencyInjection\Definition; 18 | use Symfony\Component\DependencyInjection\Reference; 19 | 20 | class LdifUrlLoaderPassSpec extends ObjectBehavior 21 | { 22 | function it_is_initializable() 23 | { 24 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\DependencyInjection\Compiler\LdifUrlLoaderPass'); 25 | } 26 | 27 | function it_should_process_tagged_services_when_there_are_none_found(ContainerBuilder $container) 28 | { 29 | $container->findTaggedServiceIds(LdifUrlLoaderPass::LDIF_URL_LOADER_TAG)->willReturn([]); 30 | $container->findDefinition(Argument::any())->shouldNotBeCalled(); 31 | 32 | $this->process($container); 33 | } 34 | 35 | function it_should_process_tagged_services_when_they_exist(ContainerBuilder $container, Definition $definition) 36 | { 37 | $id = 'foo.ldif_url_loader'; 38 | $container->findTaggedServiceIds(LdifUrlLoaderPass::LDIF_URL_LOADER_TAG)->willReturn( 39 | [$id => [['type' => 'foo']]] 40 | ); 41 | $container->findDefinition(LdifUrlLoaderPass::LDIF_PARSER)->shouldBeCalled()->willReturn($definition); 42 | $definition->addMethodCall('setUrlLoader', ['foo', new Reference($id)])->shouldBeCalled(); 43 | 44 | $this->process($container); 45 | } 46 | 47 | function it_should_require_the_type_property_for_the_tag(ContainerBuilder $container, Definition $definition) 48 | { 49 | $id = 'foo.ldif_url_loader'; 50 | $container->findTaggedServiceIds(LdifUrlLoaderPass::LDIF_URL_LOADER_TAG)->willReturn([$id => [[]]]); 51 | $container->findDefinition(LdifUrlLoaderPass::LDIF_PARSER)->shouldBeCalled()->willReturn($definition); 52 | 53 | $this->shouldThrow(new \InvalidArgumentException('Service "foo.ldif_url_loader" must define the "type" attribute on "ldap_tools.ldif_url_loader" tags.'))->duringProcess($container); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/Form/Type/LdapObjectTypeSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\Form\Type; 12 | 13 | use LdapTools\LdapManager; 14 | use LdapTools\Object\LdapObjectCollection; 15 | use LdapTools\Query\LdapQuery; 16 | use LdapTools\Query\LdapQueryBuilder; 17 | use PhpSpec\ObjectBehavior; 18 | use Prophecy\Argument; 19 | use Symfony\Component\Form\Extension\Core\Type\ChoiceType; 20 | use Symfony\Component\HttpKernel\Kernel; 21 | use Symfony\Component\OptionsResolver\OptionsResolver; 22 | 23 | class LdapObjectTypeSpec extends ObjectBehavior 24 | { 25 | /** 26 | * @var OptionsResolver 27 | */ 28 | protected $resolver; 29 | 30 | public function let(LdapManager $ldap, LdapQueryBuilder $qb, LdapQuery $query, LdapObjectCollection $collection) 31 | { 32 | $ldap->getDomainContext()->willReturn('foo.bar'); 33 | $ldap->buildLdapQuery()->willReturn($qb); 34 | 35 | $qb->select(Argument::any())->willReturn($qb); 36 | $qb->from(Argument::any())->willReturn($qb); 37 | $qb->getLdapQuery()->willReturn($query); 38 | $query->getResult()->WillReturn($collection); 39 | $collection->toArray()->willReturn([]); 40 | 41 | $this->resolver = new OptionsResolver(); 42 | 43 | if (Kernel::VERSION >= 2.6) { 44 | $this->resolver->setDefault('ldap_type', 'user'); 45 | } else { 46 | $this->resolver->setDefaults(['ldap_type' => 'user']); 47 | } 48 | 49 | $this->beConstructedWith($ldap); 50 | } 51 | 52 | function it_is_initializable() 53 | { 54 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\Form\Type\LdapObjectType'); 55 | } 56 | 57 | function it_should_have_a_parent_of_choice() 58 | { 59 | $interface = new \ReflectionClass('\Symfony\Component\Form\FormTypeInterface'); 60 | if ($interface->hasMethod('getName')) { 61 | $expected = 'choice'; 62 | } else { 63 | $expected = ChoiceType::class; 64 | } 65 | 66 | $this->getParent()->shouldBeEqualTo($expected); 67 | } 68 | 69 | function it_should_get_the_name() 70 | { 71 | $this->getName()->shouldBeEqualTo('ldap_object'); 72 | } 73 | 74 | function it_should_be_an_instance_of_the_abstract_form_type() 75 | { 76 | $this->shouldBeAnInstanceOf('\Symfony\Component\Form\AbstractType'); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /CacheWarmer/LdapToolsCacheWarmer.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\CacheWarmer; 12 | 13 | use LdapTools\Configuration; 14 | use LdapTools\Factory\LdapObjectSchemaFactory; 15 | use LdapTools\LdapManager; 16 | use LdapTools\Schema\LdapObjectSchema; 17 | use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; 18 | 19 | /** 20 | * A cache warmer for the LDAP schema types. 21 | * 22 | * @author Chad Sikorra 23 | */ 24 | class LdapToolsCacheWarmer implements CacheWarmerInterface 25 | { 26 | /** 27 | * @var LdapManager 28 | */ 29 | protected $ldap; 30 | 31 | /** 32 | * @var Configuration 33 | */ 34 | protected $config; 35 | 36 | /** 37 | * @param LdapManager $ldap 38 | * @param Configuration $config 39 | */ 40 | public function __construct(LdapManager $ldap, Configuration $config) 41 | { 42 | $this->ldap = $ldap; 43 | $this->config = $config; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function warmUp($cacheDir) 50 | { 51 | $domain = $this->ldap->getDomainContext(); 52 | foreach ($this->config->getDomainConfiguration() as $domainConfig) { 53 | $this->ldap->switchDomain($domainConfig->getDomainName()); 54 | $schemaFactory = $this->ldap->getSchemaFactory(); 55 | $parser = $this->ldap->getSchemaParser(); 56 | $schema = empty($domainConfig->getSchemaName()) ? $domainConfig->getLdapType() : $domainConfig->getSchemaName(); 57 | $ldapObjects = $parser->parseAll($schema); 58 | 59 | $this->cacheAllLdapSchemaObjects($schemaFactory, ...$ldapObjects); 60 | } 61 | $this->ldap->switchDomain($domain); 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function isOptional() 68 | { 69 | return true; 70 | } 71 | 72 | /** 73 | * @param LdapObjectSchemaFactory $schemaFactory 74 | * @param LdapObjectSchema ...$schemaObjects 75 | */ 76 | protected function cacheAllLdapSchemaObjects(LdapObjectSchemaFactory $schemaFactory, LdapObjectSchema ...$schemaObjects) 77 | { 78 | /** @var LdapObjectSchema $ldapSchemaObject */ 79 | foreach ($schemaObjects as $ldapSchemaObject) { 80 | $schemaFactory->get($ldapSchemaObject->getSchemaName(), $ldapSchemaObject->getObjectType()); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Event/LoadUserEvent.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Event; 12 | 13 | use LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUser; 14 | use LdapTools\Object\LdapObject; 15 | use Symfony\Component\EventDispatcher\Event; 16 | use Symfony\Component\Security\Core\User\UserInterface; 17 | 18 | /** 19 | * Represents a User load event. 20 | * 21 | * @author Chad Sikorra 22 | */ 23 | class LoadUserEvent extends Event 24 | { 25 | /** 26 | * The event name that happens before a user is loaded from the user provider. 27 | */ 28 | const BEFORE = 'ldap_tools_bundle.load_user.before'; 29 | 30 | /** 31 | * The event name that happens after a user is loaded from the user provider. 32 | */ 33 | const AFTER = 'ldap_tools_bundle.load_user.after'; 34 | 35 | /** 36 | * @var string 37 | */ 38 | protected $username; 39 | 40 | /** 41 | * @var string 42 | */ 43 | protected $domain; 44 | 45 | /** 46 | * @var UserInterface|LdapUser|null 47 | */ 48 | protected $user; 49 | 50 | /** 51 | * @var LdapObject|null 52 | */ 53 | protected $ldapObject; 54 | 55 | /** 56 | * @param $username 57 | * @param $domain 58 | * @param UserInterface|null $user 59 | * @param LdapObject|null $ldapObject 60 | */ 61 | public function __construct($username, $domain, UserInterface $user = null, LdapObject $ldapObject = null) 62 | { 63 | $this->username = $username; 64 | $this->domain = $domain; 65 | $this->user = $user; 66 | $this->ldapObject = $ldapObject; 67 | } 68 | 69 | /** 70 | * @return string 71 | */ 72 | public function getUsername() 73 | { 74 | return $this->username; 75 | } 76 | 77 | /** 78 | * @return string 79 | */ 80 | public function getDomain() 81 | { 82 | return $this->domain; 83 | } 84 | 85 | /** 86 | * This is only available on an AFTER load event. Otherwise it will be null. 87 | * 88 | * @return LdapUser|null|UserInterface 89 | */ 90 | public function getUser() 91 | { 92 | return $this->user; 93 | } 94 | 95 | /** 96 | * Get the LDAP object the user was created from. This is only available on an AFTER load event. 97 | * 98 | * @return LdapObject|null 99 | */ 100 | public function getLdapObject() 101 | { 102 | return $this->ldapObject; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Resources/doc/LDAP-Authentication-With-The-FOSUserBundle.md: -------------------------------------------------------------------------------- 1 | LDAP Authentication with the FOSUserBundle 2 | ========== 3 | 4 | There may be times where you want to have a custom User class that gets loaded from a database (such as FOSUserBundle) 5 | but then authenticate that user against LDAP on login. This can be accomplished easily when using this bundle's LDAP 6 | guard component. 7 | 8 | #### 1. Follow the [FOSUserBundle Getting Started Guide](https://symfony.com/doc/master/bundles/FOSUserBundle/index.html). 9 | 10 | Follow the above guide like you normally would to get your database user ready/created. 11 | 12 | #### 2. Follow the [LDAP Authentication Provider](./LDAP-Authentication-Provider.md) steps to setup the Guard. 13 | 14 | **Note:** This is for Symfony 2.8+ only, as we are using the Guard component. 15 | 16 | #### 3. Setup your user provider to use the FOSUserBundle user provider. 17 | 18 | Your end security config using the FOSUserBundle provider, but the LDAP authentication Guard, would look like: 19 | 20 | ```yml 21 | security: 22 | 23 | hide_user_not_found: false 24 | 25 | encoders: 26 | FOS\UserBundle\Model\UserInterface: bcrypt 27 | 28 | providers: 29 | fos_userbundle: 30 | id: fos_user.user_provider.username 31 | 32 | firewalls: 33 | dev: 34 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 35 | security: false 36 | 37 | main: 38 | anonymous: ~ 39 | # Using the FOSUserBundle provider to find the users... 40 | provider: fos_userbundle 41 | form_login: 42 | login_path: login 43 | check_path: login_check 44 | use_forward: true 45 | pattern: ^/ 46 | logout: ~ 47 | # Using the LDAP Guard to authenticate the users... 48 | guard: 49 | authenticators: 50 | - ldap_tools.security.ldap_guard_authenticator 51 | login: 52 | pattern: ^/login$ 53 | anonymous: ~ 54 | 55 | access_control: 56 | - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } 57 | - { path: ^/, roles: ROLE_USER } 58 | ``` 59 | 60 | #### 4. Create FOSUserBundle Users and Roles 61 | 62 | Note that with the above you're still responsible for creating users within the FOSUserBundle so they exist in the database. 63 | However, the user's can have any password (or none at all) as the authentication is done against LDAP and not against their 64 | password stored in the database. 65 | 66 | You should also note that LDAP role mapping for the LdapTools config will not take effect. That's a function of the LDAP 67 | user provider in this bundle. However, you can hook into the `ldap_tools_bundle.login.success` event and assign any roles 68 | based on an LDAP query there. For an example see the [LDAP authentication provider doc](./LDAP-Authentication-Provider.md#Successful-Login-Event). 69 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/Log/LdapProfilerLoggerSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\Log; 12 | 13 | use LdapTools\Log\LogOperation; 14 | use LdapTools\Operation\AddOperation; 15 | use LdapTools\Operation\BatchModifyOperation; 16 | use LdapTools\Operation\DeleteOperation; 17 | use PhpSpec\ObjectBehavior; 18 | 19 | class LdapProfilerLoggerSpec extends ObjectBehavior 20 | { 21 | function it_is_initializable() 22 | { 23 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\Log\LdapProfilerLogger'); 24 | } 25 | 26 | function it_should_implement_the_ldap_logger_interface() 27 | { 28 | $this->shouldImplement('\LdapTools\Log\LdapLoggerInterface'); 29 | } 30 | 31 | function it_should_add_the_log_operation_when_calling_start() 32 | { 33 | $ops = [ 34 | (new LogOperation(new AddOperation()))->setDomain('foo.bar'), 35 | (new LogOperation(new DeleteOperation('foo')))->setDomain('foo.bar'), 36 | (new LogOperation(new BatchModifyOperation('foo')))->setDomain('example.local'), 37 | ]; 38 | /** @var LogOperation $op */ 39 | foreach ($ops as $op) { 40 | $this->start($op->start()); 41 | } 42 | $this->getOperations()->shouldHaveCount(3); 43 | 44 | /** @var LogOperation $op */ 45 | foreach ($ops as $op) { 46 | $this->end($op->stop()); 47 | } 48 | $this->getOperations()->shouldHaveCount(3); 49 | 50 | $this->getOperations()->shouldBeEqualTo($ops); 51 | } 52 | 53 | function it_should_be_able_to_get_operations_by_domain() 54 | { 55 | $ops = [ 56 | (new LogOperation(new AddOperation()))->setDomain('foo.bar'), 57 | (new LogOperation(new BatchModifyOperation('foo')))->setDomain('example.local'), 58 | ]; 59 | /** @var LogOperation $op */ 60 | foreach ($ops as $op) { 61 | $this->start($op->start()); 62 | } 63 | 64 | $this->getOperations('foo.bar')->shouldHaveCount(1); 65 | $this->getOperations('example.local')->shouldHaveCount(1); 66 | } 67 | 68 | function it_should_be_able_to_get_operations_with_errors() 69 | { 70 | $op1 = (new LogOperation(new AddOperation()))->setDomain('foo.bar')->setError('foo'); 71 | $op2 = (new LogOperation(new BatchModifyOperation('foo')))->setDomain('example.local'); 72 | $ops = [ 73 | $op1, 74 | $op2, 75 | ]; 76 | 77 | /** @var LogOperation $op */ 78 | foreach ($ops as $op) { 79 | $this->start($op->start()); 80 | $this->end($op->stop()); 81 | } 82 | 83 | $this->getErrors()->shouldBeEqualTo([$op1]); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Resources/doc/LDAP-Object-Form-Type.md: -------------------------------------------------------------------------------- 1 | LDAP Object Form Type 2 | ================ 3 | 4 | The LDAP object form type provides a simple method of displaying different types of LDAP objects in a choice selection 5 | format for a form. This can be combined with the LDAP object Doctrine type to easily store/retrieve specific LDAP objects 6 | when working with a form that relates to a Doctrine entity. 7 | 8 | The form type can be used as follows: 9 | 10 | ```php 11 | use LdapTools\Bundle\LdapToolsBundle\Form\Type\LdapObjectType; 12 | 13 | # ... 14 | 15 | $form = $this->createFormBuilder() 16 | // For Symfony 2.7, replace LdapObjectType::class with "ldap_object". 17 | ->add('ldap', LdapObjectType::class, [ 18 | // The ldap_type can be any valid LdapTools type. Such as: user, computer, contact, etc. 19 | 'ldap_type' => 'group', 20 | // The ldap_query_builder allows a closure to further limit/filter what LDAP objects show up. 21 | // Omit this and you will get all of the specified 'ldap_type'. 22 | 'ldap_query_builder' => function (LdapQueryBuilder $query) { 23 | $query->andWhere($query->filter()->startsWith('name', 'App-')); 24 | } 25 | ]) 26 | ->add('send', SubmitType::class) 27 | ->getForm(); 28 | ``` 29 | 30 | ## LDAP Object Form Types and Doctrine Entities 31 | 32 | Often times you will probably be working with a form that relates to a Doctrine entity. You can relate the the LDAP 33 | object form type to a column with a special annotation that will easily load/save the LDAP object to the entity to 34 | eliminate some extra work: 35 | 36 | ```php 37 | # On the entity where your form is saved... 38 | 39 | use LdapTools\Bundle\LdapToolsBundle\Annotation as LdapTools; 40 | 41 | /** 42 | * User 43 | * 44 | * @ORM\Table() 45 | * @ORM\Entity 46 | */ 47 | class User 48 | { 49 | #... 50 | 51 | /** 52 | * @var LdapObject 53 | * 54 | * @ORM\Column(name="ldapGroup", type="ldap_object") 55 | * @LdapTools\LdapObject(type="group") 56 | */ 57 | private $ldapGroup; 58 | 59 | # ... 60 | } 61 | ``` 62 | 63 | Then you just follow the initial instructions at the start of this document to setup the LdapObjectType on your form and 64 | tie it to the `$ldapGroup` column in the example above. 65 | 66 | The above is an example of saving a specific LDAP object to a column. If your form type allows for multiple values to be 67 | selected, then you need to modify the annotation on your entity a bit so it knows that it is expecting a collection of 68 | LDAP objects: 69 | 70 | ```php 71 | # On the entity where your form is saved... 72 | 73 | use LdapTools\Bundle\LdapToolsBundle\Annotation as LdapTools; 74 | 75 | /** 76 | * User 77 | * 78 | * @ORM\Table() 79 | * @ORM\Entity 80 | */ 81 | class User 82 | { 83 | #... 84 | 85 | /** 86 | * @var LdapObjectCollection 87 | * 88 | * @ORM\Column(name="ldapGroups", type="ldap_object_collection") 89 | * @LdapTools\LdapObject(type="group", collection=true) 90 | */ 91 | private $ldapGroups; 92 | 93 | # ... 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /Form/ChoiceLoader/LdapObjectChoiceTrait.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Form\ChoiceLoader; 12 | 13 | use LdapTools\LdapManager; 14 | use LdapTools\Object\LdapObject; 15 | use LdapTools\Query\LdapQueryBuilder; 16 | 17 | /** 18 | * Removes duplication between the Choice List and the Choice Loader. 19 | * 20 | * @author Chad Sikorra 21 | */ 22 | trait LdapObjectChoiceTrait 23 | { 24 | /** 25 | * @var \Closure|null 26 | */ 27 | protected $queryCallback; 28 | 29 | /** 30 | * @var LdapQueryBuilder|null 31 | */ 32 | protected $ldapQueryBuilder; 33 | 34 | /** 35 | * @var LdapManager 36 | */ 37 | protected $ldap; 38 | 39 | /** 40 | * @var string 41 | */ 42 | protected $id; 43 | 44 | /** 45 | * @var string 46 | */ 47 | protected $type; 48 | 49 | /** 50 | * @var string 51 | */ 52 | protected $labelAttribute; 53 | 54 | /** 55 | * @param LdapQueryBuilder|\Closure|null $query 56 | */ 57 | protected function setClosureOrQuery($query) 58 | { 59 | if ($query instanceof \Closure) { 60 | $this->queryCallback = $query; 61 | } elseif ($query instanceof LdapQueryBuilder) { 62 | $this->ldapQueryBuilder = $query; 63 | } 64 | } 65 | 66 | /** 67 | * Get the values of a set of choices. 68 | * 69 | * @param LdapObject[] $choices 70 | * @return array 71 | */ 72 | protected function getLdapValuesForChoices(LdapObject ...$choices) 73 | { 74 | $values = []; 75 | 76 | foreach ($choices as $i => $ldapObject) { 77 | $values[$i] = (string) $ldapObject->get($this->id); 78 | } 79 | 80 | return $values; 81 | } 82 | 83 | /** 84 | * Get the LDAP objects from LDAP. Optionally only get those specified by the passed values. 85 | * 86 | * @param array $values The values used to narrow the LDAP query. 87 | * @return \LdapTools\Object\LdapObjectCollection 88 | */ 89 | protected function getLdapObjectsByQuery($values = []) 90 | { 91 | if (!$this->ldapQueryBuilder) { 92 | $query = $this->ldap->buildLdapQuery() 93 | ->select([$this->id, $this->labelAttribute]) 94 | ->from($this->type); 95 | } else { 96 | $query = clone $this->ldapQueryBuilder; 97 | } 98 | 99 | if (!empty($values)) { 100 | foreach ($values as $value) { 101 | $query->orWhere([$this->id => $value]); 102 | } 103 | } 104 | if ($this->queryCallback) { 105 | $closure = $this->queryCallback; 106 | $closure($query); 107 | } 108 | 109 | return $query->getLdapQuery()->getResult(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/Log/LdapLoggerSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\Log; 12 | 13 | use LdapTools\Log\LogOperation; 14 | use LdapTools\Operation\DeleteOperation; 15 | use PhpSpec\ObjectBehavior; 16 | use Prophecy\Argument; 17 | use Psr\Log\LoggerInterface; 18 | use Symfony\Component\Stopwatch\Stopwatch; 19 | 20 | class LdapLoggerSpec extends ObjectBehavior 21 | { 22 | function let(LoggerInterface $logger, Stopwatch $stopwatch) 23 | { 24 | $this->beConstructedWith($logger, $stopwatch); 25 | } 26 | 27 | function it_is_initializable() 28 | { 29 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\Log\LdapLogger'); 30 | } 31 | 32 | function it_should_call_the_stopwatch_and_logger_on_start($stopwatch, $logger) 33 | { 34 | $log = new LogOperation((new DeleteOperation('foo'))->setServer('foo')); 35 | $log->setDomain('foo'); 36 | $stopwatch->start('ldaptools', strtolower($log->getOperation()->getName()))->shouldBeCalled(); 37 | $logger->debug( 38 | "(foo on foo) Start Delete Operation - DN: foo, Controls: array (\n)" 39 | )->shouldBeCalled(); 40 | 41 | $this->start($log); 42 | } 43 | 44 | function it_should_call_the_stopwatch_and_logger_on_stop($stopwatch, $logger) 45 | { 46 | $log = new LogOperation((new DeleteOperation('foo'))->setServer('bar')); 47 | $log->setDomain('example.local'); 48 | $log->start(); 49 | $log->stop(); 50 | 51 | $stopwatch->stop('ldaptools')->shouldBeCalled(); 52 | $logger->debug( 53 | '(example.local on bar) End Delete Operation - Completed in '.(round(($log->getStopTime() - $log->getStartTime()) * 1000))." ms." 54 | )->shouldBeCalled(); 55 | 56 | $this->end($log); 57 | } 58 | 59 | function it_should_not_call_the_stopwatch_or_logger_when_they_are_not_used($stopwatch, $logger) 60 | { 61 | $log = new LogOperation(new DeleteOperation('foo')); 62 | $this->beConstructedWith(null, null); 63 | 64 | $logger->debug(Argument::any(), Argument::any())->shouldNotBeCalled(); 65 | $stopwatch->start(Argument::any(), Argument::any())->shouldNotBeCalled(); 66 | $stopwatch->stop(Argument::any(), Argument::any())->shouldNotBeCalled(); 67 | 68 | $this->start($log->start()); 69 | $this->end($log->stop()); 70 | } 71 | 72 | function it_should_call_the_logger_error_method_when_the_log_contains_an_error($logger) 73 | { 74 | $log = new LogOperation((new DeleteOperation('foo'))->setServer('bar')); 75 | $log->setError('foo'); 76 | $log->setDomain('foo.bar'); 77 | $log->start()->stop(); 78 | $logger->error( 79 | '(foo.bar on bar) End Delete Operation - Error: foo, Completed in '.(round(($log->getStopTime() - $log->getStartTime()) * 1000))." ms." 80 | )->shouldBeCalled(); 81 | 82 | $this->start($log); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/DependencyInjection/Security/Factory/LdapFormLoginFactorySpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\DependencyInjection\Security\Factory; 12 | 13 | use PhpSpec\ObjectBehavior; 14 | use Prophecy\Argument; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | use Symfony\Component\DependencyInjection\Definition; 17 | use Symfony\Component\DependencyInjection\Reference; 18 | 19 | class LdapFormLoginFactorySpec extends ObjectBehavior 20 | { 21 | function it_is_initializable() 22 | { 23 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\DependencyInjection\Security\Factory\LdapFormLoginFactory'); 24 | } 25 | 26 | function it_should_get_the_key() 27 | { 28 | $this->getKey()->shouldBeEqualTo('ldap-tools-form'); 29 | } 30 | 31 | function it_should_create_the_listener_and_provider_ids(ContainerBuilder $container, Definition $upDefinition, Definition $definition) 32 | { 33 | $id = 'restricted'; 34 | $entryPoint = 'foo'; 35 | $userProviderId = 'ldap_tools.security.user.ldap_user_provider'; 36 | $listenerId = 'ldap_tools.security.firewall.ldap_form_login_listener'; 37 | $options = ['remember_me' => false, 'login_path' => '/login', 'use_forward' => false]; 38 | 39 | $ignoredDefs = [ 40 | "security.authentication.success_handler.restricted.ldap_tools_form", 41 | "security.authentication.failure_handler.restricted.ldap_tools_form", 42 | "security.authentication.form_entry_point.".$id, 43 | ]; 44 | 45 | // Container expectations... 46 | $container->setDefinition("$userProviderId.".$id, Argument::any())->shouldBeCalled()->willReturn($upDefinition); 47 | $container->setDefinition("$listenerId.$id", Argument::any())->willReturn($definition); 48 | $container->getDefinition("$listenerId.".$id, Argument::any())->willReturn($definition); 49 | 50 | // Not concerned with these really, but need to add them... 51 | foreach ($ignoredDefs as $def) { 52 | $container->setDefinition($def, Argument::any())->willReturn($definition); 53 | } 54 | $definition->addMethodCall(Argument::any(), Argument::any())->willReturn($definition); 55 | $definition->replaceArgument(Argument::any(), Argument::any())->willReturn($definition); 56 | $definition->addArgument(Argument::any(), Argument::any())->willReturn($definition); 57 | 58 | // UserProvider expectations... 59 | $upDefinition->replaceArgument(0, $id)->shouldBeCalled()->willReturn($upDefinition); 60 | $upDefinition->replaceArgument(2, new Reference($userProviderId))->shouldBeCalled()->willReturn($upDefinition); 61 | 62 | $this->create($container, $id, $options, $userProviderId, $entryPoint)->shouldBeEqualTo([ 63 | "$userProviderId.$id", 64 | "$listenerId.".$id, 65 | "security.authentication.form_entry_point.restricted", 66 | ]); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Resources/doc/LDIF-Parser-URL-Loaders.md: -------------------------------------------------------------------------------- 1 | LDIF Parser URL Loaders 2 | ================ 3 | 4 | The LdapTools library includes a [LDIF Parser](https://github.com/ldaptools/ldaptools/blob/master/docs/en/tutorials/LDIF-Files.md) capable of loading LDIF values from a URL (such as `http://` or `file://`). 5 | By default it only includes support for a limited number of URL types. If you have your own URL type you want to use in 6 | a LDIF file to load data then you need to create a [LDIF URL Loader](https://github.com/ldaptools/ldaptools/blob/master/docs/en/tutorials/LDIF-Files.md#ldif-url-loaders) which is a class that implements `LdapTools\Ldif\UrlLoader\UrlLoaderInterface`. 7 | Then you need to add an instance of that URL Loader to the LDIF Parser class via `setUrlLoader($type, $loader)`. 8 | 9 | The above process is made easy in this bundle by first creating the LDIF URL Loader, registering it as a service, then simply 10 | tagging the service with the `ldap_tools.ldif_url_loader` tag and a `type` property on the tag that defines the type of 11 | URL it is (ie. `https`, `file`, etc). 12 | 13 | ## Create the LDIF URL Loader 14 | 15 | ```php 16 | namespace AppBundle\LdifUrlLoader; 17 | 18 | use LdapTools\Ldif\LdifUrlLoader\UrlLoaderInterface; 19 | 20 | class LdifHttpsLoader implements UrlLoaderInterface 21 | { 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function load($url) 26 | { 27 | // Custom logic for your URL loading process here... 28 | 29 | // Return the resulting data... 30 | return $data; 31 | } 32 | } 33 | ``` 34 | 35 | ## Define the LDIF URL Loader as a Service 36 | 37 | ```yaml 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | ``` 51 | 52 | Now in your LDIF files your URL loader will be used when parsing files that consume data from the URL. 53 | 54 | Assume a LDIF file with an entry like: 55 | 56 | ``` 57 | dn: cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com 58 | objectclass: top 59 | objectclass: person 60 | objectclass: organizationalPerson 61 | cn: Barbara Jensen 62 | sn: Jensen 63 | uid: bjensen 64 | telephonenumber: +1 408 555 1212 65 | description:< https://example.local/users/bjensen.html 66 | ``` 67 | 68 | The `description` at the bottom will be loaded from the URL Loader, such as in a controller: 69 | 70 | ```php 71 | 72 | class DefaultController extends Controller 73 | { 74 | public function indexAction() 75 | { 76 | try { 77 | # Use the LDIF parser from the service which will have the URL loader set... 78 | $ldif = $this->get('ldap_tools.ldif_parser')->parse(file_get_contents('/path/to/ldif.txt')); 79 | } catch (LdifParserException $e) { 80 | echo "Error Parsing LDIF: ".$e->getMessage(); 81 | } 82 | 83 | // Iterate through the operations parsed from the LDIF file... 84 | foreach ($ldif->toOperations() as $operation) { 85 | // Load them into LDAP if you want... 86 | $this->get('ldap_tools.ldap_manager')->getConnection()->execute($operation); 87 | } 88 | 89 | # ... 90 | } 91 | } 92 | ``` 93 | -------------------------------------------------------------------------------- /Log/LdapLogger.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Log; 12 | 13 | use LdapTools\Log\LdapLoggerInterface; 14 | use LdapTools\Log\LogOperation; 15 | use Psr\Log\LoggerInterface; 16 | use Symfony\Component\Stopwatch\Stopwatch; 17 | 18 | /** 19 | * LDAP logger. 20 | * 21 | * @author Chad Sikorra 22 | */ 23 | class LdapLogger implements LdapLoggerInterface 24 | { 25 | /** 26 | * @var null|LoggerInterface 27 | */ 28 | protected $logger; 29 | 30 | /** 31 | * @var null|Stopwatch 32 | */ 33 | protected $stopwatch; 34 | 35 | /** 36 | * @param LoggerInterface|null $logger 37 | * @param Stopwatch|null $stopwatch 38 | */ 39 | public function __construct(LoggerInterface $logger = null, Stopwatch $stopwatch = null) 40 | { 41 | $this->logger = $logger; 42 | $this->stopwatch = $stopwatch; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function start(LogOperation $log) 49 | { 50 | if (!is_null($this->stopwatch)) { 51 | $this->stopwatch->start('ldaptools', strtolower($log->getOperation()->getName())); 52 | } 53 | if (!is_null($this->logger)) { 54 | $this->log($log); 55 | } 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function end(LogOperation $log) 62 | { 63 | if (!is_null($this->stopwatch)) { 64 | $this->stopwatch->stop('ldaptools'); 65 | } 66 | if (!is_null($this->logger)) { 67 | $this->log($log); 68 | } 69 | } 70 | 71 | /** 72 | * Logs a message. 73 | * 74 | * @param LogOperation $log 75 | */ 76 | protected function log(LogOperation $log) 77 | { 78 | $message = $this->getLogMessage($log); 79 | 80 | if (!is_null($log->getError())) { 81 | $this->logger->error($message); 82 | } else { 83 | $this->logger->debug($message); 84 | } 85 | } 86 | 87 | /** 88 | * @param LogOperation $log 89 | * @return string 90 | */ 91 | protected function getLogMessage(LogOperation $log) 92 | { 93 | $startOrStop = is_null($log->getStopTime()) ? 'Start' : 'End'; 94 | $message = "(".$log->getDomain()." on ".$log->getOperation()->getServer().") $startOrStop ".$log->getOperation()->getName()." Operation - "; 95 | 96 | $params = []; 97 | if (is_null($log->getStopTime())) { 98 | foreach ($log->getOperation()->getLogArray() as $key => $value) { 99 | if ($key != "Server") { 100 | $params[] = "$key: $value"; 101 | } 102 | } 103 | } else { 104 | if (!is_null($log->getError())) { 105 | $params[] = "Error: ".$log->getError(); 106 | } 107 | $params[] = "Completed in ".(round(($log->getStopTime() - $log->getStartTime()) * 1000))." ms."; 108 | } 109 | $message .= implode(', ', $params); 110 | 111 | return $message; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 0.9.2 (2018-06-07) 5 | ------------------ 6 | * Rename the option use_referrer to use_referer to match Symfony. 7 | 8 | 0.9.1 (2018-03-23) 9 | ------------------ 10 | * Do not query LDAP for mapped roles if none are defined. 11 | 12 | 0.9.0 (2017-12-10) 13 | ------------------ 14 | * Correct deprecations in Symfony 3.4, 4.0. 15 | * Add HTTP basic auth support. 16 | * Allow the LDAP Guard to be more easily extended. 17 | * Bump composer requirements for Symfony 4.0. 18 | 19 | 0.8.1 (2017-08-13) 20 | ------------------ 21 | * Correct a deprecation in the Active Directory Response Code checking. 22 | 23 | 0.8.0 (2017-08-13) 24 | ------------------ 25 | * Bumped the minimum Symfony version requirement to 2.7. 26 | * Improved the default LDAP authenticator bind logic for OpenLDAP/AD. 27 | * The login username can now be configured to query LDAP for a DN to bind with for a given LDAP attribute. 28 | * LDAP Group-to-Role mapping is now usable as a service (ldap_tools.security.user.ldap_role_mapper). 29 | * The username/password is no longer required for the domain configurations. 30 | * By default domain configurations now lazy bind, and only connect/bind when absolutely needed. 31 | * Refactored configuration of the LDAP User Provider. 32 | * Bumped the minimum LdapTools version requirement. 33 | 34 | 0.7.0 (2017-04-14) 35 | ------------------ 36 | * Add Guard redirection settings to the security config section of the bundle. 37 | * Add Guard settings for: post_only, remember_me, username/password/domain parameters 38 | * Add Guard events to set a response object on: start, auth success, auth failure 39 | * Add a ldaptools:generate:config command to assist in generating your LDAP configuration. 40 | * Only search groups recursively when the LDAP type is Active Directory. 41 | * Do not use cache in debug/dev mode. 42 | 43 | 0.6.0 (2017-01-29) 44 | ------------------ 45 | * Add better Doctrine integration configuration (specify Doctrine entity manager names, or disable them). 46 | * The LdapUser must implement the LdapUserInterface. Removes the dependency on the LdapObject class. 47 | * Add a command (ldaptools:generate:sslcert) to assist in retrieving the LDAP SSL certificate. 48 | 49 | 0.5.0 (2016-10-23) 50 | ------------------ 51 | * By default user attributes/roles are no longer refreshed on each request. It is now configurable. 52 | * Add a before and after event when loading a user from the user provider. 53 | * Correct several specs, add the bump the PhpSpec version. 54 | * Add more docs and examples. 55 | 56 | 0.4.0 (2016-09-05) 57 | ------------------ 58 | * Add a LDIF parser service. Allow tagging LDIF URL loaders to it. 59 | * Add token information to the LDAP login success event. 60 | * Correct several specs, add the build status to the readme. 61 | * Add the bundle to Scrutinizer and add the score to the readme. 62 | 63 | 0.3.0 (2016-08-22) 64 | ------------------ 65 | * Allow group role mappings to be set by SID, GUID, DN or name. 66 | * Add the connect_timeout domain config option. 67 | * Add a LDAP login success event (ldap_tools_bundle.login.success). 68 | 69 | 0.2.0 (2016-06-26) 70 | ------------------ 71 | * Add a LDAP Guard authenticator for newer Symfony versions (replaces the ldap_tools_form) 72 | * Add a schema_name domain config option (@rouet) 73 | * Add an idle_reconnect domain config option. 74 | * Fix several specs. 75 | * Bump the required version of LdapTools and CS-Fixer in composer. 76 | 77 | 0.1.0 (2015-12-25) 78 | ------------------ 79 | * Initial release. 80 | -------------------------------------------------------------------------------- /Form/ChoiceLoader/LdapObjectChoiceLoader.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Form\ChoiceLoader; 12 | 13 | use LdapTools\LdapManager; 14 | use LdapTools\Object\LdapObject; 15 | use Symfony\Component\Form\ChoiceList\ChoiceListInterface; 16 | use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; 17 | use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; 18 | use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; 19 | use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; 20 | 21 | /** 22 | * Provides a choice list loader for LDAP objects. 23 | * 24 | * @author Chad Sikorra 25 | */ 26 | class LdapObjectChoiceLoader implements ChoiceLoaderInterface 27 | { 28 | use LdapObjectChoiceTrait; 29 | 30 | /** 31 | * @var ChoiceListFactoryInterface 32 | */ 33 | private $factory; 34 | 35 | /** 36 | * @var ChoiceListInterface|null 37 | */ 38 | protected $choiceList; 39 | 40 | /** 41 | * @param LdapManager $ldap 42 | * @param string $type The LDAP object type. 43 | * @param string $labelAttribute The LDAP attribute to use for the label. 44 | * @param string $id The attribute to use for the ID. 45 | * @param LdapQueryBuilder|\Closure|null 46 | */ 47 | public function __construct(LdapManager $ldap, $type, $labelAttribute = 'name', $id = 'guid', $query = null) 48 | { 49 | $this->factory = new PropertyAccessDecorator(new DefaultChoiceListFactory()); 50 | $this->ldap = $ldap; 51 | $this->type = $type; 52 | $this->id = $id; 53 | $this->labelAttribute = $labelAttribute; 54 | $this->setClosureOrQuery($query); 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function loadChoiceList($value = null) 61 | { 62 | if ($this->choiceList) { 63 | return $this->choiceList; 64 | } 65 | $ldapObjects = $this->getLdapObjectsByQuery(); 66 | 67 | $choices = []; 68 | /** @var LdapObject $object */ 69 | foreach ($ldapObjects as $object) { 70 | $choices[$object->get($this->labelAttribute)] = $object; 71 | } 72 | $this->choiceList = $this->factory->createListFromChoices($choices, $this->id); 73 | 74 | return $this->choiceList; 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function loadValuesForChoices(array $choices, $value = null) 81 | { 82 | if (empty(array_filter($choices))) { 83 | return []; 84 | } 85 | if (!$this->choiceList) { 86 | return $this->getLdapValuesForChoices(...$choices); 87 | } 88 | 89 | return $this->loadChoiceList($value)->getValuesForChoices($choices); 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | public function loadChoicesForValues(array $values, $value = null) 96 | { 97 | if (empty(array_filter($values))) { 98 | return []; 99 | } 100 | if (!$this->choiceList) { 101 | $this->loadChoiceList($value); 102 | } 103 | 104 | return $this->choiceList->getChoicesForValues($values); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/Security/User/LdapUserCheckerSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\Security\User; 12 | 13 | use LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUser; 14 | use LdapTools\Enums\AD\ResponseCode; 15 | use LdapTools\Connection\LdapConnection; 16 | use PhpSpec\ObjectBehavior; 17 | 18 | class LdapUserCheckerSpec extends ObjectBehavior 19 | { 20 | function it_is_initializable() 21 | { 22 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUserChecker'); 23 | } 24 | 25 | function it_should_implement_the_UserCheckerInterface() 26 | { 27 | $this->shouldImplement('\Symfony\Component\Security\Core\User\UserCheckerInterface'); 28 | } 29 | 30 | function it_should_extend_the_UserChecker() 31 | { 32 | $this->shouldBeAnInstanceOf('\Symfony\Component\Security\Core\User\UserChecker'); 33 | } 34 | 35 | function it_should_error_when_the_response_code_for_the_user_is_for_a_disabled_account(LdapUser $user) 36 | { 37 | $this->shouldThrow('\Symfony\Component\Security\Core\Exception\DisabledException')->duringCheckLdapErrorCode( 38 | $user, 39 | ResponseCode::AccountDisabled, 40 | LdapConnection::TYPE_AD 41 | ); 42 | } 43 | 44 | function it_should_not_error_when_the_response_code_is_the_same_value_as_disabled_for_something_other_than_AD(LdapUser $user) 45 | { 46 | $this->shouldNotThrow('\Symfony\Component\Security\Core\Exception\DisabledException')->duringCheckLdapErrorCode( 47 | $user, 48 | ResponseCode::AccountDisabled, 49 | LdapConnection::TYPE_OPENLDAP 50 | ); 51 | } 52 | 53 | function it_should_error_when_the_response_code_for_the_user_is_for_a_locked_account(LdapUser $user) 54 | { 55 | $this->shouldThrow('\Symfony\Component\Security\Core\Exception\LockedException')->duringCheckLdapErrorCode( 56 | $user, 57 | ResponseCode::AccountLocked, 58 | LdapConnection::TYPE_AD 59 | ); 60 | } 61 | 62 | function it_should_not_error_when_the_response_code_is_the_same_value_as_locked_for_something_other_than_AD(LdapUser $user) 63 | { 64 | $this->shouldNotThrow('\Symfony\Component\Security\Core\Exception\LockedException')->duringCheckLdapErrorCode( 65 | $user, 66 | ResponseCode::AccountLocked, 67 | LdapConnection::TYPE_OPENLDAP 68 | ); 69 | } 70 | 71 | function it_should_throw_CredentialsExpired_when_the_response_code_for_the_user_is_for_an_account_whose_password_must_change(LdapUser $user) 72 | { 73 | $this->shouldThrow('\Symfony\Component\Security\Core\Exception\CredentialsExpiredException')->duringCheckLdapErrorCode( 74 | $user, 75 | ResponseCode::AccountPasswordMustChange, 76 | LdapConnection::TYPE_AD 77 | ); 78 | } 79 | 80 | function it_should_not_throw_CredentialsExpired_when_the_response_code_is_the_same_value_as_pass_must_change_for_something_other_than_AD(LdapUser $user) 81 | { 82 | $this->shouldNotThrow('\Symfony\Component\Security\Core\Exception\CredentialsExpiredException')->duringCheckLdapErrorCode( 83 | $user, 84 | ResponseCode::AccountPasswordMustChange, 85 | LdapConnection::TYPE_OPENLDAP 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/Form/ChoiceLoader/LegacyLdapChoiceLoaderSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\Form\ChoiceLoader; 12 | 13 | use LdapTools\LdapManager; 14 | use LdapTools\Object\LdapObject; 15 | use LdapTools\Object\LdapObjectCollection; 16 | use LdapTools\Object\LdapObjectType; 17 | use LdapTools\Query\LdapQuery; 18 | use LdapTools\Query\LdapQueryBuilder; 19 | use PhpSpec\ObjectBehavior; 20 | 21 | class LegacyLdapChoiceLoaderSpec extends ObjectBehavior 22 | { 23 | public function let(LdapManager $ldap, LdapQueryBuilder $qb, LdapQuery $query) 24 | { 25 | $ldap->buildLdapQuery()->willReturn($qb); 26 | $qb->getLdapQuery()->willReturn($query); 27 | $this->beConstructedWith($ldap, LdapObjectType::USER); 28 | } 29 | 30 | function it_is_initializable() 31 | { 32 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\Form\ChoiceLoader\LegacyLdapChoiceLoader'); 33 | } 34 | 35 | function it_should_load_a_set_of_choices_as_ldap_objects($qb, $query) 36 | { 37 | // These are the default attributes it should select (name/value for the choice) 38 | $qb->select(['guid', 'name'])->shouldBeCalled()->willReturn($qb); 39 | $qb->from("user")->shouldBeCalled()->willReturn($qb); 40 | 41 | $collection = new LdapObjectCollection( 42 | new LdapObject(['name' => 'foo', 'guid' => '123'], ['user'], 'user', 'user'), 43 | new LdapObject(['name' => 'bar', 'guid' => '456'], ['user'], 'user', 'user') 44 | ); 45 | $query->getResult()->shouldBeCalled()->willReturn($collection); 46 | 47 | $this->load()->shouldBeEqualTo($collection->toArray()); 48 | } 49 | 50 | function it_should_support_calling_a_closure_against_the_query_builder_when_loading_the_choices($qb, $query, $ldap) 51 | { 52 | $foo = function($qb) { 53 | $qb->where(['foo' => 'bar']); 54 | }; 55 | $this->beConstructedWith($ldap, LdapObjectType::GROUP, 'upn', 'sid', $foo); 56 | 57 | // These are the default attributes it should select (name/value for the choice) 58 | $qb->select(['sid', 'upn'])->shouldBeCalled()->willReturn($qb); 59 | $qb->from("group")->shouldBeCalled()->willReturn($qb); 60 | 61 | $collection = new LdapObjectCollection( 62 | new LdapObject(['upn' => 'foo', 'sid' => '123'], ['group'], 'group', 'group'), 63 | new LdapObject(['upn' => 'bar', 'sid' => '456'], ['group'], 'group', 'group') 64 | ); 65 | $query->getResult()->shouldBeCalled()->willReturn($collection); 66 | 67 | // As the result of the closure... 68 | $qb->where(['foo' => 'bar'])->shouldBeCalled(); 69 | 70 | $this->load()->shouldBeEqualTo($collection->toArray()); 71 | } 72 | 73 | function it_should_support_setting_a_specific_ldap_query_builder_to_load_the_choicelist($qb, $ldap, $query) 74 | { 75 | $this->beConstructedWith($ldap, LdapObjectType::GROUP, 'upn', 'sid', $qb); 76 | $qb->getLdapQuery()->shouldBeCalled()->willReturn($query); 77 | 78 | $collection = new LdapObjectCollection( 79 | new LdapObject(['upn' => 'foo', 'sid' => '123'], ['group'], 'group', 'group'), 80 | new LdapObject(['upn' => 'bar', 'sid' => '456'], ['group'], 'group', 'group') 81 | ); 82 | $query->getResult()->shouldBeCalled()->willReturn($collection); 83 | 84 | $this->load()->shouldBeEqualTo($collection->toArray()); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Event/AuthenticationHandlerEvent.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Event; 12 | 13 | use Symfony\Component\EventDispatcher\Event; 14 | use Symfony\Component\HttpFoundation\Request; 15 | use Symfony\Component\HttpFoundation\Response; 16 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 17 | use Symfony\Component\Security\Core\Exception\AuthenticationException; 18 | 19 | /** 20 | * Represents an authentication handler event, such as success or failure, where the response can be set. 21 | * 22 | * @author Chad Sikorra 23 | */ 24 | class AuthenticationHandlerEvent extends Event 25 | { 26 | /** 27 | * The event name that happens after the default authentication success handler is called. 28 | */ 29 | const SUCCESS = 'ldap_tools_bundle.guard.login.success'; 30 | 31 | /** 32 | * The event name that happens after the default authentication failure handler is called. 33 | */ 34 | const FAILURE = 'ldap_tools_bundle.guard.login.failure'; 35 | 36 | /** 37 | * The event name that happens when the entry point is called for the guard and returns a redirect/response. 38 | */ 39 | const START = 'ldap_tools_bundle.guard.login.start'; 40 | 41 | /** 42 | * @var Response 43 | */ 44 | protected $response; 45 | 46 | /** 47 | * @var AuthenticationException|null 48 | */ 49 | protected $exception; 50 | 51 | /** 52 | * @var Request 53 | */ 54 | protected $request; 55 | 56 | /** 57 | * @var TokenInterface|null 58 | */ 59 | protected $token; 60 | 61 | /** 62 | * @var string|null 63 | */ 64 | protected $providerKey; 65 | 66 | /** 67 | * @param Response $response 68 | * @param Request $request 69 | * @param null|AuthenticationException $exception 70 | * @param TokenInterface|null $token 71 | * @param string|null $providerKey 72 | */ 73 | public function __construct(Response $response, Request $request, AuthenticationException $exception = null, TokenInterface $token = null, $providerKey = null) 74 | { 75 | $this->request = $request; 76 | $this->response = $response; 77 | $this->exception = $exception; 78 | $this->token = $token; 79 | $this->providerKey = $providerKey; 80 | } 81 | 82 | /** 83 | * @return Response 84 | */ 85 | public function getResponse() 86 | { 87 | return $this->response; 88 | } 89 | 90 | /** 91 | * @return Request 92 | */ 93 | public function getRequest() 94 | { 95 | return $this->request; 96 | } 97 | 98 | /** 99 | * @param Response $response 100 | * @return $this 101 | */ 102 | public function setResponse(Response $response) 103 | { 104 | $this->response = $response; 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * @return AuthenticationException|null 111 | */ 112 | public function getException() 113 | { 114 | return $this->exception; 115 | } 116 | 117 | /** 118 | * @return null|TokenInterface 119 | */ 120 | public function getToken() 121 | { 122 | return $this->token; 123 | } 124 | 125 | /** 126 | * @return null|string 127 | */ 128 | public function getProviderKey() 129 | { 130 | return $this->providerKey; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/Security/User/LdapUserSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\Security\User; 12 | 13 | use LdapTools\Object\LdapObject; 14 | use PhpSpec\ObjectBehavior; 15 | 16 | class LdapUserSpec extends ObjectBehavior 17 | { 18 | function let() 19 | { 20 | $attributes = [ 21 | 'dn' => 'cn=chad,dc=foo,dc=bar', 22 | 'username' => 'chad', 23 | 'disabled' => false, 24 | 'passwordMustChange' => false, 25 | 'accountExpirationDate' => new \DateTime('2233-3-22'), 26 | 'groups' => [ 27 | 'foo', 28 | 'bar', 29 | ], 30 | 'guid' => '39ff94c0-d84f-4b5d-9d63-94439e533949', 31 | 'locked' => false 32 | ]; 33 | $this->beConstructedWith(); 34 | $this->refresh($attributes); 35 | } 36 | 37 | function it_is_initializable() 38 | { 39 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUser'); 40 | } 41 | 42 | function it_should_implement_AdvancedUserInterface() 43 | { 44 | $this->shouldImplement('\Symfony\Component\Security\Core\User\AdvancedUserInterface'); 45 | } 46 | 47 | function it_should_implement_Serializable() 48 | { 49 | $this->shouldImplement('\Serializable'); 50 | } 51 | 52 | function it_should_get_the_username() 53 | { 54 | $this->getUsername()->shouldBeEqualTo('chad'); 55 | } 56 | 57 | function it_should_get_the_enabled_status() 58 | { 59 | $this->isEnabled()->shouldBeEqualTo(true); 60 | } 61 | 62 | function it_should_get_the_locked_status() 63 | { 64 | $this->isAccountNonLocked()->shouldBeEqualTo(true); 65 | } 66 | 67 | function it_should_get_the_account_expiration_status() 68 | { 69 | $this->isAccountNonExpired()->shouldBeEqualTo(true); 70 | } 71 | 72 | function it_should_get_the_guid() 73 | { 74 | $this->getGuid()->shouldBeEqualTo('39ff94c0-d84f-4b5d-9d63-94439e533949'); 75 | } 76 | 77 | function it_should_get_the_groups() 78 | { 79 | $this->getGroups()->shouldBeEqualTo(['foo','bar']); 80 | } 81 | 82 | function it_should_set_the_username() 83 | { 84 | $this->setUsername('foo'); 85 | $this->getUsername()->shouldBeEqualTo('foo'); 86 | } 87 | 88 | function it_should_return_null_for_salt() 89 | { 90 | $this->getSalt()->shouldBeNull(); 91 | } 92 | 93 | function it_should_return_null_for_password() 94 | { 95 | $this->getPassword()->shouldBeNull(); 96 | } 97 | 98 | function it_should_return_null_when_erasing_credentials() 99 | { 100 | $this->eraseCredentials()->shouldBeNull(); 101 | } 102 | 103 | function it_should_have_no_roles_by_default() 104 | { 105 | $this->getRoles()->shouldHaveCount(0); 106 | } 107 | 108 | function it_should_add_roles_properly() 109 | { 110 | $this->addRole('foo'); 111 | $this->getRoles()->shouldBeEqualTo(['FOO']); 112 | $this->addRole('BAR'); 113 | $this->getRoles()->shouldBeEqualTo(['FOO', 'BAR']); 114 | } 115 | 116 | function it_should_set_roles_properly() 117 | { 118 | $this->addRole('meh'); 119 | $this->setRoles(['foo','BAR']); 120 | $this->getRoles()->shouldBeEqualTo(['FOO', 'BAR']); 121 | } 122 | 123 | function it_should_have_a_string_representation_of_a_dn_by_default() 124 | { 125 | $this->__toString()->shouldBeEqualTo('chad'); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Form/ChoiceList/LdapObjectChoiceList.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Form\ChoiceList; 12 | 13 | use LdapTools\Object\LdapObject; 14 | use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; 15 | 16 | /** 17 | * Overrides certain ChoiceList methods so the old extended ObjectChoiceList works properly 18 | * 19 | * @author Chad Sikorra 20 | */ 21 | class LdapObjectChoiceList extends ObjectChoiceList 22 | { 23 | /** 24 | * @var LdapObject[] 25 | */ 26 | protected $ldapObjects; 27 | 28 | /** 29 | * @var string 30 | */ 31 | protected $id; 32 | 33 | /** 34 | * LdapObjectChoiceList constructor. 35 | * @param array|\Traversable $choices 36 | * @param null $labelPath 37 | * @param array $preferredChoices 38 | * @param null $groupPath 39 | * @param null $valuePath 40 | * @param PropertyAccessorInterface|null $propertyAccessor 41 | */ 42 | public function __construct($choices, $labelPath = null, array $preferredChoices = array(), $groupPath = null, $valuePath = null, PropertyAccessorInterface $propertyAccessor = null) 43 | { 44 | $this->id = $valuePath; 45 | $this->ldapObjects = $choices; 46 | parent::__construct($choices, $labelPath, $preferredChoices, $groupPath, $valuePath); 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function getChoicesForValues(array $values) 53 | { 54 | if (empty(array_filter($values))) { 55 | return []; 56 | } 57 | 58 | $choices = []; 59 | foreach ($values as $i => $value) { 60 | foreach ($this->ldapObjects as $ldapObject) { 61 | if ($ldapObject->has($this->id, $value)) { 62 | $choices[$i] = $ldapObject; 63 | break; 64 | } 65 | } 66 | } 67 | 68 | return $choices; 69 | } 70 | 71 | /** 72 | * {@inheritdoc} 73 | */ 74 | public function getValuesForChoices(array $choices) 75 | { 76 | if (empty(array_filter($choices))) { 77 | return []; 78 | } 79 | 80 | $values = []; 81 | foreach ($choices as $i => $choice) { 82 | $values[$i] = $choice->get($this->id); 83 | } 84 | 85 | return $values; 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function getIndicesForChoices(array $choices) 92 | { 93 | if (empty(array_filter($choices))) { 94 | return []; 95 | } 96 | 97 | $indices = []; 98 | foreach ($choices as $k => $choice) { 99 | foreach ($this->ldapObjects as $i => $ldapObject) { 100 | if ($ldapObject->has($this->id, $choice->get($this->id))) { 101 | $indices[$k] = $i; 102 | break; 103 | } 104 | } 105 | } 106 | 107 | return $indices; 108 | } 109 | 110 | /** 111 | * {@inheritdoc} 112 | */ 113 | public function getIndicesForValues(array $values) 114 | { 115 | if (empty(array_filter($values))) { 116 | return []; 117 | } 118 | 119 | $indices = []; 120 | foreach ($values as $k => $value) { 121 | foreach ($this->ldapObjects as $i => $ldapObject) { 122 | if ($ldapObject->has($this->id, $value)) { 123 | $indices[$k] = $i; 124 | break; 125 | } 126 | } 127 | } 128 | 129 | return $indices; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/DependencyInjection/Compiler/EventRegisterPassSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\DependencyInjection\Compiler; 12 | 13 | use LdapTools\Bundle\LdapToolsBundle\DependencyInjection\Compiler\EventRegisterPass; 14 | use PhpSpec\ObjectBehavior; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | use Symfony\Component\DependencyInjection\Definition; 17 | 18 | class EventRegisterPassSpec extends ObjectBehavior 19 | { 20 | function it_is_initializable() 21 | { 22 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\DependencyInjection\Compiler\EventRegisterPass'); 23 | } 24 | 25 | function it_should_process_tagged_services_when_there_are_none_found(ContainerBuilder $container) 26 | { 27 | $container->findTaggedServiceIds(EventRegisterPass::SUBSCRIBER_TAG)->willReturn([]); 28 | $container->findTaggedServiceIds(EventRegisterPass::LISTENER_TAG)->willReturn([]); 29 | 30 | $this->process($container); 31 | } 32 | 33 | function it_should_process_tagged_services_when_they_exist(ContainerBuilder $container, Definition $definition) 34 | { 35 | $container->findTaggedServiceIds(EventRegisterPass::SUBSCRIBER_TAG)->willReturn( 36 | ['foo.subscriber' => []] 37 | ); 38 | $container->findTaggedServiceIds(EventRegisterPass::LISTENER_TAG)->willReturn( 39 | ['foo.listener' => [['method' => 'foo', 'event' => 'ldap.object.before_modify']]] 40 | ); 41 | $container->findDefinition(EventRegisterPass::DISPATCHER)->willReturn($definition); 42 | $container->getDefinition('foo.listener')->willReturn($definition); 43 | $container->getDefinition('foo.subscriber')->willReturn($definition); 44 | 45 | $this->process($container); 46 | } 47 | 48 | function it_should_require_the_method_and_event_property_for_the_listener_services(ContainerBuilder $container, Definition $definition) 49 | { 50 | $container->findTaggedServiceIds(EventRegisterPass::SUBSCRIBER_TAG)->willReturn([]); 51 | $container->findTaggedServiceIds(EventRegisterPass::LISTENER_TAG)->willReturn( 52 | ['foo.listener' => [['event' => 'ldap.object.before_modify']]] 53 | ); 54 | $container->findDefinition(EventRegisterPass::DISPATCHER)->willReturn($definition); 55 | $container->getDefinition('foo.listener')->willReturn($definition); 56 | 57 | $this->shouldThrow('\InvalidArgumentException')->duringProcess($container); 58 | 59 | $container->findTaggedServiceIds(EventRegisterPass::LISTENER_TAG)->willReturn( 60 | ['foo.listener' => [['method' => 'foo']]] 61 | ); 62 | 63 | $this->shouldThrow('\InvalidArgumentException')->duringProcess($container); 64 | 65 | $container->findTaggedServiceIds(EventRegisterPass::LISTENER_TAG)->willReturn( 66 | ['foo.listener' => []] 67 | ); 68 | 69 | $this->shouldThrow('\InvalidArgumentException')->duringProcess($container); 70 | } 71 | 72 | function it_should_not_allow_an_abstract_service_as_a_listener(ContainerBuilder $container, Definition $definition) 73 | { 74 | $container->findTaggedServiceIds(EventRegisterPass::SUBSCRIBER_TAG)->willReturn([]); 75 | $container->findTaggedServiceIds(EventRegisterPass::LISTENER_TAG)->willReturn( 76 | ['foo.listener' => [['method' => 'foo', 'event' => 'ldap.object.before_modify']]] 77 | ); 78 | $container->findDefinition(EventRegisterPass::DISPATCHER)->willReturn($definition); 79 | $container->getDefinition('foo.listener')->willReturn($definition); 80 | $definition->isAbstract()->willReturn(true); 81 | 82 | $this->shouldThrow('\InvalidArgumentException')->duringProcess($container); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/EventRegisterPass.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\DependencyInjection\Compiler; 12 | 13 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 14 | use Symfony\Component\DependencyInjection\ContainerBuilder; 15 | use Symfony\Component\DependencyInjection\Reference; 16 | use Symfony\Component\DependencyInjection\Definition; 17 | 18 | /** 19 | * Add any tagged listeners/subscribers to the LdapTools event dispatcher. 20 | * 21 | * @author Chad Sikorra 22 | */ 23 | class EventRegisterPass implements CompilerPassInterface 24 | { 25 | /** 26 | * The event subscriber tag name. 27 | */ 28 | const SUBSCRIBER_TAG = 'ldap_tools.event_subscriber'; 29 | 30 | /** 31 | * The event listener tag name. 32 | */ 33 | const LISTENER_TAG = 'ldap_tools.event_listener'; 34 | 35 | /** 36 | * The event dispatcher service name. 37 | */ 38 | const DISPATCHER = 'ldap_tools.event_dispatcher'; 39 | 40 | /** 41 | * @inheritdoc 42 | */ 43 | public function process(ContainerBuilder $container) 44 | { 45 | $subscribers = $container->findTaggedServiceIds(self::SUBSCRIBER_TAG); 46 | $listeners = $container->findTaggedServiceIds(self::LISTENER_TAG); 47 | 48 | if (empty($subscribers) && empty($listeners)) { 49 | return; 50 | } 51 | $dispatcher = $container->findDefinition(self::DISPATCHER); 52 | 53 | if (!empty($subscribers)) { 54 | $this->addSubscribersToEventDispatcher($container, $subscribers, $dispatcher); 55 | } 56 | if (!empty($listeners)) { 57 | $this->addListenersToEventDispatcher($container, $listeners, $dispatcher); 58 | } 59 | } 60 | 61 | /** 62 | * @param ContainerBuilder $container 63 | * @param array $events 64 | * @param Definition $dispatcher 65 | */ 66 | protected function addSubscribersToEventDispatcher(ContainerBuilder $container, array $events, Definition $dispatcher) 67 | { 68 | foreach ($events as $id => $event) { 69 | $this->validateTaggedService($container, $id); 70 | $dispatcher->addMethodCall('addSubscriber', [new Reference($id)]); 71 | } 72 | } 73 | 74 | /** 75 | * @param ContainerBuilder $container 76 | * @param array $events 77 | * @param Definition $dispatcher 78 | */ 79 | protected function addListenersToEventDispatcher(ContainerBuilder $container, array $events, Definition $dispatcher) 80 | { 81 | foreach ($events as $id => $event) { 82 | $this->validateTaggedService($container, $id); 83 | if (!isset($event[0]['event'])) { 84 | throw new \InvalidArgumentException(sprintf('Service "%s" must define the "event" attribute on "%s" tags.', $id, self::LISTENER_TAG)); 85 | } 86 | if (!isset($event[0]['method'])) { 87 | throw new \InvalidArgumentException(sprintf('Service "%s" must define the "method" attribute on "%s" tags.', $id, self::LISTENER_TAG)); 88 | } 89 | $dispatcher->addMethodCall('addListener', [$event[0]['event'], [new Reference($id), $event[0]['method']]]); 90 | } 91 | } 92 | 93 | /** 94 | * @param ContainerBuilder $container 95 | * @param string $id 96 | */ 97 | protected function validateTaggedService(ContainerBuilder $container, $id) 98 | { 99 | if ($container->getDefinition($id)->isAbstract()) { 100 | throw new \InvalidArgumentException(sprintf( 101 | 'The abstract service "%s" cannot be tagged as a LdapTools subscriber/listener.', 102 | $id 103 | )); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /DataCollector/LdapToolsDataCollector.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\DataCollector; 12 | 13 | use LdapTools\Bundle\LdapToolsBundle\Log\LdapProfilerLogger; 14 | use LdapTools\LdapManager; 15 | use LdapTools\Log\LogOperation; 16 | use Symfony\Component\HttpFoundation\Request; 17 | use Symfony\Component\HttpFoundation\Response; 18 | use Symfony\Component\HttpKernel\DataCollector\DataCollector; 19 | 20 | /** 21 | * Data Collector for LdapTools. 22 | * 23 | * @author Chad Sikorra 24 | */ 25 | class LdapToolsDataCollector extends DataCollector 26 | { 27 | /** 28 | * @var LdapManager 29 | */ 30 | protected $ldap; 31 | 32 | /** 33 | * @var LdapProfilerLogger 34 | */ 35 | protected $logger; 36 | 37 | /** 38 | * LdapToolsDataCollector constructor. 39 | * @param LdapProfilerLogger $logger 40 | * @param LdapManager|null $ldap 41 | */ 42 | public function __construct(LdapProfilerLogger $logger, LdapManager $ldap = null) 43 | { 44 | $this->ldap = $ldap; 45 | $this->logger = $logger; 46 | $this->reset(); 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function getName() 53 | { 54 | return 'ldaptools'; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function collect(Request $request, Response $response, \Exception $exception = null) 61 | { 62 | if (!$this->ldap) { 63 | return; 64 | } 65 | $this->data['domains'] = $this->ldap->getDomains(); 66 | 67 | $this->data['operations_by_domain'] = []; 68 | foreach ($this->data['domains'] as $domain) { 69 | $this->data['operations_by_domain'][$domain] = $this->addOperationToData(...$this->logger->getOperations($domain)); 70 | } 71 | $this->data['operations'] = $this->addOperationToData(...$this->logger->getOperations()); 72 | $this->data['errors'] = $this->addOperationToData(...$this->logger->getErrors()); 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | public function reset() 79 | { 80 | $this->data['domains'] = []; 81 | $this->data['errors'] = []; 82 | $this->data['operations'] = []; 83 | $this->data['operations_by_domain'] = []; 84 | } 85 | 86 | /** 87 | * @return array 88 | */ 89 | public function getOperationsByDomain() 90 | { 91 | return $this->data['operations_by_domain']; 92 | } 93 | 94 | /** 95 | * @return int 96 | */ 97 | public function getTime() 98 | { 99 | $time = 0; 100 | 101 | foreach ($this->data['operations'] as $operation) { 102 | $time += $operation['duration']; 103 | } 104 | 105 | return $time; 106 | } 107 | 108 | /** 109 | * @return array 110 | */ 111 | public function getOperations() 112 | { 113 | return $this->data['operations']; 114 | } 115 | 116 | /** 117 | * @return array 118 | */ 119 | public function getErrors() 120 | { 121 | return $this->data['errors']; 122 | } 123 | 124 | /** 125 | * @return string[] 126 | */ 127 | public function getDomains() 128 | { 129 | return $this->data['domains']; 130 | } 131 | 132 | /** 133 | * @param \LdapTools\Log\LogOperation[] ...$logs 134 | * @return array 135 | */ 136 | protected function addOperationToData(LogOperation ...$logs) 137 | { 138 | $logData = []; 139 | 140 | foreach ($logs as $log) { 141 | $data = []; 142 | 143 | $data['data'] = $log->getOperation()->getLogArray(); 144 | $data['startTime'] = $log->getStartTime(); 145 | $data['stopTime'] = $log->getStopTime(); 146 | $data['domain'] = $log->getDomain(); 147 | $data['error'] = $log->getError(); 148 | $data['name'] = $log->getOperation()->getName(); 149 | $data['duration'] = ($data['stopTime'] - $data['startTime']) * 1000; 150 | 151 | $logData[] = $data; 152 | } 153 | 154 | return $logData; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/DataCollector/LdapToolsDataCollectorSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\DataCollector; 12 | 13 | use LdapTools\Bundle\LdapToolsBundle\Log\LdapProfilerLogger; 14 | use LdapTools\LdapManager; 15 | use LdapTools\Log\LogOperation; 16 | use LdapTools\Operation\AddOperation; 17 | use LdapTools\Operation\DeleteOperation; 18 | use PhpSpec\ObjectBehavior; 19 | use Symfony\Component\HttpFoundation\Request; 20 | use Symfony\Component\HttpFoundation\Response; 21 | 22 | class LdapToolsDataCollectorSpec extends ObjectBehavior 23 | { 24 | function let(LdapManager $ldap) 25 | { 26 | $logger = new LdapProfilerLogger(); 27 | 28 | // Add some log data... 29 | $addOperation = (new AddOperation())->setAttributes(['username' => 'foo', 'unicodePwd' => 'bar']); 30 | $deleteOperation = new DeleteOperation('foo'); 31 | 32 | $addLog = new LogOperation($addOperation); 33 | $addLog->setDomain('foo.bar'); 34 | $addLog->setError('fail'); 35 | $deleteLog = new LogOperation($deleteOperation); 36 | $deleteLog->setDomain('example.local'); 37 | 38 | /** @var LogOperation $log */ 39 | foreach ([$addLog, $deleteLog] as $log) { 40 | $logger->start($log->start()); 41 | $logger->end($log->stop()); 42 | } 43 | 44 | $ldap->getDomains()->willReturn(['foo.bar', 'example.local']); 45 | 46 | $this->beConstructedWith($logger, $ldap); 47 | } 48 | 49 | function it_is_initializable() 50 | { 51 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\DataCollector\LdapToolsDataCollector'); 52 | } 53 | 54 | function it_should_extend_the_abstract_data_collector() 55 | { 56 | $this->shouldBeAnInstanceOf('Symfony\Component\HttpKernel\DataCollector\DataCollector'); 57 | } 58 | 59 | function it_should_get_the_name() 60 | { 61 | $this->getName()->shouldBeEqualTo('ldaptools'); 62 | } 63 | 64 | function it_should_collect_data(Request $request, Response $response) 65 | { 66 | $this->collect($request, $response); 67 | } 68 | 69 | function it_should_get_errors(Request $request, Response $response) 70 | { 71 | $this->collect($request, $response); 72 | $this->getErrors()->shouldBeArray(); 73 | $this->getErrors()->shouldHaveCount(1); 74 | $this->getErrors()[0]->shouldHaveKeyWithValue('error', 'fail'); 75 | } 76 | 77 | function it_should_get_all_operations(Request $request, Response $response) 78 | { 79 | $this->collect($request, $response); 80 | $this->getOperations()->shouldBeArray(); 81 | $this->getOperations()->shouldHaveCount(2); 82 | } 83 | 84 | function it_should_get_operations_by_domain(Request $request, Response $response) 85 | { 86 | $this->collect($request, $response); 87 | $this->getOperationsByDomain()->shouldBeArray(); 88 | $this->getOperationsByDomain()->shouldHaveCount(2); 89 | } 90 | 91 | function it_should_get_the_domains(Request $request, Response $response) 92 | { 93 | $this->collect($request, $response); 94 | $this->getDomains()->shouldBeEqualTo(['foo.bar','example.local']); 95 | } 96 | 97 | function it_should_get_the_time_of_all_operations_combined(Request $request, Response $response) 98 | { 99 | $this->collect($request, $response); 100 | $this->getTime()->shouldBeDouble(); 101 | $this->getTime()->shouldNotBeEqualTo(0); 102 | } 103 | 104 | function it_should_remove_password_information_from_the_operation(Request $request, Response $response) 105 | { 106 | $this->collect($request, $response); 107 | $this->getOperations()[0]->shouldHaveKeyWithValue('data', [ 108 | 'DN' => null, 109 | 'Attributes' => print_r([ 110 | 'username' => 'foo', 111 | 'unicodePwd' => '******', 112 | ], true), 113 | 'Server' => null, 114 | "Controls" => "array (\n)", 115 | ]); 116 | } 117 | 118 | function it_should_reset_data(Request $request, Response $response) 119 | { 120 | $this->collect($request, $response); 121 | $this->reset(); 122 | 123 | $this->getOperations()->shouldBeEqualTo([]); 124 | $this->getOperationsByDomain()->shouldBeEqualTo([]); 125 | $this->getDomains()->shouldBeEqualTo([]); 126 | $this->getTime()->shouldBeEqualTo(0); 127 | $this->getErrors()->shouldBeEqualTo([]); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Security/LdapAuthenticationTrait.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Security; 12 | 13 | use LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUserProvider; 14 | use LdapTools\Exception\Exception; 15 | use LdapTools\LdapManager; 16 | use LdapTools\Object\LdapObject; 17 | use Symfony\Component\Security\Core\Exception\BadCredentialsException; 18 | use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; 19 | use Symfony\Component\Security\Core\User\UserInterface; 20 | use Symfony\Component\Security\Core\User\UserProviderInterface; 21 | 22 | /** 23 | * Common methods between the Guard and Authentication Provider. 24 | * 25 | * @author Chad Sikorra 26 | */ 27 | trait LdapAuthenticationTrait 28 | { 29 | /** 30 | * @var LdapManager 31 | */ 32 | protected $ldap; 33 | 34 | /** 35 | * @var LdapUserProvider 36 | */ 37 | protected $ldapUserProvider; 38 | 39 | /** 40 | * The logic for determining the username/DN to bind with is as follows: 41 | * 42 | * 1. Always prefer a DN from a default user from the LDAP user provider, or LdapObject instance from LdapTools 43 | * 2. If it wasn't a LdapObject and no attribute was explicitly set to query LDAP for, use the UserInterface username 44 | * 3. Query LDAP using a specific attribute for a user with the specified username, return the DN. 45 | * 46 | * @param UserInterface $user 47 | * @param string|null $queryAttribute 48 | * @return string 49 | */ 50 | protected function getBindUsername(UserInterface $user, $queryAttribute) 51 | { 52 | if ($user instanceof LdapObject && $user->has('dn')) { 53 | return $user->get('dn'); 54 | } 55 | if ($queryAttribute === null) { 56 | return $user->getUsername(); 57 | } 58 | 59 | return $this->ldapUserProvider 60 | ->getLdapUser($queryAttribute, $user->getUsername()) 61 | ->get('dn'); 62 | } 63 | 64 | /** 65 | * If no LDAP credentials are in the config then attempt to use the user supplied credentials from the login. But 66 | * only if we are using the LdapUserProvider. 67 | * 68 | * @param string $username 69 | * @param string $password 70 | * @param UserProviderInterface $userProvider 71 | */ 72 | protected function setLdapCredentialsIfNeeded($username, $password, UserProviderInterface $userProvider) 73 | { 74 | // Only care about this in the context of the LDAP user provider... 75 | if (!$userProvider instanceof LdapUserProvider) { 76 | return; 77 | } 78 | 79 | // Only if the username/password are not defined in the config.... 80 | $config = $this->ldap->getConnection()->getConfig(); 81 | if (!(empty($config->getUsername()) && (empty($config->getPassword() && $config->getPassword() !== '0')))) { 82 | return; 83 | } 84 | 85 | $config->setUsername($username); 86 | $config->setPassword($password); 87 | } 88 | 89 | /** 90 | * If the domain needs to a different context for the request, then switch it. 91 | * 92 | * @param string|null $domain 93 | */ 94 | protected function switchDomainIfNeeded($domain) 95 | { 96 | if (!empty($domain) && $this->ldap->getDomainContext() !== $domain) { 97 | $this->ldap->switchDomain($domain); 98 | } 99 | } 100 | 101 | /** 102 | * If the passed domain is not the current context, then switch back to it. 103 | * 104 | * @param string $domain 105 | */ 106 | protected function switchDomainBackIfNeeded($domain) 107 | { 108 | if ($domain !== $this->ldap->getDomainContext()) { 109 | $this->ldap->switchDomain($domain); 110 | } 111 | } 112 | 113 | /** 114 | * Determine whether or not the exception should be masked with a BadCredentials or not. 115 | * 116 | * @param \Exception $e 117 | * @param bool $hideUserNotFoundExceptions 118 | * @throws \Exception 119 | */ 120 | protected function hideOrThrow(\Exception $e, $hideUserNotFoundExceptions) 121 | { 122 | if ($hideUserNotFoundExceptions) { 123 | throw new BadCredentialsException('Bad credentials.', 0, $e); 124 | } 125 | 126 | // Specifically show LdapTools related exceptions, ignore others. 127 | // Custom auth exception messages don't exist until Symfony 2.8, 2.7 is still under support... 128 | if (!$hideUserNotFoundExceptions && $e instanceof Exception && class_exists('Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException')) { 129 | throw new CustomUserMessageAuthenticationException($e->getMessage(), [], $e->getCode()); 130 | } 131 | 132 | throw $e; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Security/User/LdapUser.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Security\User; 12 | 13 | use LdapTools\Object\LdapObject; 14 | use Symfony\Component\Security\Core\User\AdvancedUserInterface; 15 | 16 | /** 17 | * Represents a user from LDAP. 18 | * 19 | * @author Chad Sikorra 20 | */ 21 | class LdapUser extends LdapObject implements LdapUserInterface, AdvancedUserInterface, \Serializable 22 | { 23 | /** 24 | * @var array The Symfony roles for this user. 25 | */ 26 | protected $roles = []; 27 | 28 | public function __construct() 29 | { 30 | parent::__construct([]); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function getSalt() 37 | { 38 | return null; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function getPassword() 45 | { 46 | return null; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function eraseCredentials() 53 | { 54 | return null; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function getRoles() 61 | { 62 | return $this->roles; 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public function setRoles(array $roles) 69 | { 70 | $this->roles = []; 71 | foreach ($roles as $role) { 72 | $this->addRole($role); 73 | } 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * {@inheritdoc} 80 | */ 81 | public function addRole($role) 82 | { 83 | $role = strtoupper($role); 84 | 85 | if (!in_array($role, $this->roles)) { 86 | $this->roles[] = $role; 87 | } 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | public function removeRole($role) 96 | { 97 | $role = strtoupper($role); 98 | 99 | if (in_array($role, $this->roles)) { 100 | $this->roles = array_diff($this->roles, [$role]); 101 | } 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * {@inheritdoc} 108 | */ 109 | public function getUsername() 110 | { 111 | return $this->get('username'); 112 | } 113 | 114 | /** 115 | * {@inheritdoc} 116 | */ 117 | public function setUsername($username) 118 | { 119 | return $this->set('username', $username); 120 | } 121 | 122 | /** 123 | * {@inheritdoc} 124 | */ 125 | public function isAccountNonExpired() 126 | { 127 | if (!$this->has('accountExpirationDate') || $this->get('accountExpirationDate') === false) { 128 | $result = true; 129 | } elseif ($this->get('accountExpirationDate') instanceof \DateTime) { 130 | $result = ($this->get('accountExpirationDate') > new \DateTime()); 131 | } else { 132 | $result = (bool) $this->get('accountExpirationDate'); 133 | } 134 | 135 | return $result; 136 | } 137 | 138 | /** 139 | * {@inheritdoc} 140 | */ 141 | public function isAccountNonLocked() 142 | { 143 | return $this->has('locked') ? !$this->get('locked') : true; 144 | } 145 | 146 | /** 147 | * {@inheritdoc} 148 | */ 149 | public function isCredentialsNonExpired() 150 | { 151 | return $this->has('passwordMustChange') ? !$this->get('passwordMustChange') : true; 152 | } 153 | 154 | /** 155 | * {@inheritdoc} 156 | */ 157 | public function isEnabled() 158 | { 159 | return $this->has('enabled') ? $this->get('enabled') : true; 160 | } 161 | 162 | /** 163 | * {@inheritdoc} 164 | */ 165 | public function getLdapGuid() 166 | { 167 | return $this->get('guid'); 168 | } 169 | 170 | /** 171 | * {@inheritdoc} 172 | */ 173 | public function setLdapGuid($guid) 174 | { 175 | return $this->set('guid', $guid); 176 | } 177 | 178 | /** 179 | * {@inheritdoc} 180 | */ 181 | public function getGroups() 182 | { 183 | return $this->has('groups') ? $this->get('groups') : []; 184 | } 185 | 186 | /** 187 | * {@inheritdoc} 188 | */ 189 | public function serialize() 190 | { 191 | return serialize([ 192 | $this->attributes, 193 | $this->type, 194 | $this->roles 195 | ]); 196 | } 197 | 198 | /** 199 | * {@inheritdoc} 200 | */ 201 | public function unserialize($serialized) 202 | { 203 | list($this->attributes, $this->type, $this->roles) = unserialize($serialized); 204 | } 205 | 206 | /** 207 | * @return string 208 | */ 209 | public function __toString() 210 | { 211 | return $this->getUsername(); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /Form/Type/LdapObjectType.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Form\Type; 12 | 13 | use LdapTools\Bundle\LdapToolsBundle\Form\ChoiceList\LdapObjectChoiceList; 14 | use LdapTools\Bundle\LdapToolsBundle\Form\ChoiceLoader\LdapObjectChoiceLoader; 15 | use LdapTools\Bundle\LdapToolsBundle\Form\ChoiceLoader\LegacyLdapChoiceLoader; 16 | use LdapTools\LdapManager; 17 | use Symfony\Component\Form\AbstractType; 18 | use Symfony\Component\Form\Extension\Core\Type\ChoiceType; 19 | use Symfony\Component\OptionsResolver\OptionsResolver; 20 | use Symfony\Component\OptionsResolver\OptionsResolverInterface; 21 | use Symfony\Component\OptionsResolver\Options; 22 | 23 | /** 24 | * Provides a form type that represents LDAP object. 25 | * 26 | * @author Chad Sikorra 27 | */ 28 | class LdapObjectType extends AbstractType 29 | { 30 | /** 31 | * @var LdapManager 32 | */ 33 | protected $ldap; 34 | 35 | /** 36 | * @param LdapManager $ldap 37 | */ 38 | public function __construct(LdapManager $ldap) 39 | { 40 | $this->ldap = $ldap; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function configureOptions(OptionsResolver $resolver) 47 | { 48 | $ldap = $this->ldap; 49 | 50 | $resolver->setDefaults([ 51 | 'ldap_domain' => $this->ldap->getDomainContext(), // The LDAP domain context 52 | 'ldap_attributes' => null, // The attributes to select 53 | 'ldap_query_builder' => null, // A closure or a LdapQueryBuilder instance 54 | 'choice_name' => 'name', 55 | 'choice_value' => 'guid', 56 | 'choices' => [], // A user supplied array of LDAP objects. 57 | 'choice_loader' => function (Options $options) use ($ldap) { 58 | if (!interface_exists('\Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface')) { 59 | return null; 60 | } 61 | 62 | return new LdapObjectChoiceLoader($ldap, 63 | $options['ldap_type'], 64 | $options['choice_name'], 65 | $options['choice_value'], 66 | $options['ldap_query_builder'] 67 | ); 68 | }, 69 | 'choice_list' => function (Options $options) use ($ldap) { 70 | // Always prefer the ChoiceLoader if it exists. Fall back to the ObjectChoiceList... 71 | if (interface_exists('\Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface')) { 72 | return null; 73 | } 74 | $legacyChoiceLoader = new LegacyLdapChoiceLoader($ldap, $options['ldap_type'], $options['choice_name'], $options['choice_value'], $options['ldap_query_builder']); 75 | $preferred = isset($options['preferred_choices']) ? $options['preferred_choices'] : []; 76 | 77 | return new LdapObjectChoiceList( 78 | $legacyChoiceLoader->load(), 79 | $options['choice_name'], 80 | $preferred, 81 | null, 82 | $options['choice_value'] 83 | ); 84 | }, 85 | ]); 86 | $resolver->setRequired(['ldap_type']); 87 | $this->setAllowedTypes($resolver); 88 | } 89 | 90 | /** 91 | * {@inheritdoc} 92 | */ 93 | public function setDefaultOptions(OptionsResolverInterface $resolver) 94 | { 95 | $this->configureOptions($resolver); 96 | } 97 | 98 | /** 99 | * @return string 100 | */ 101 | public function getParent() 102 | { 103 | // FQCN is needed instead of a simple name in Symfony 3+ ... 104 | $interface = new \ReflectionClass('\Symfony\Component\Form\FormTypeInterface'); 105 | 106 | if ($interface->hasMethod('getName')) { 107 | $parent = 'choice'; 108 | } else { 109 | $parent = ChoiceType::class; 110 | } 111 | 112 | return $parent; 113 | } 114 | 115 | /** 116 | * @return string 117 | */ 118 | public function getName() 119 | { 120 | return 'ldap_object'; 121 | } 122 | 123 | /** 124 | * A rather ugly way of allowing compatibility with the resolver component for pre 2.6 versions. 125 | * 126 | * @param OptionsResolver $resolver 127 | */ 128 | protected function setAllowedTypes(OptionsResolver $resolver) 129 | { 130 | $allowed = ['ldap_query_builder', ['\Closure', 'LdapTools\Query\LdapQueryBuilder', 'null']]; 131 | 132 | $reflection = new \ReflectionClass(get_class($resolver)); 133 | $parameters = $reflection->getMethod('addAllowedTypes')->getParameters(); 134 | 135 | if ($parameters[0]->isArray()) { 136 | $resolver->setAllowedTypes([$allowed[0] => $allowed[1]]); 137 | } else { 138 | $resolver->setAllowedTypes(...$allowed); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LdapToolsBundle [![Build Status](https://travis-ci.org/ldaptools/ldaptools-bundle.svg)](https://travis-ci.org/ldaptools/ldaptools-bundle) [![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/github/ldaptools/ldaptools-bundle?branch=master&svg=true)](https://ci.appveyor.com/project/ChadSikorra/ldaptools-bundle-td18j) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/ldaptools/ldaptools-bundle/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/ldaptools/ldaptools-bundle/?branch=master) [![Latest Stable Version](https://poser.pugx.org/ldaptools/ldaptools-bundle/v/stable.svg)](https://packagist.org/packages/ldaptools/ldaptools-bundle) 2 | ----------- 3 | 4 | The LdapToolsBundle provides easy integration of LDAP for Symfony via [LdapTools](https://github.com/ldaptools/ldaptools). 5 | 6 | * An [LDAP authentication provider](/Resources/doc/LDAP-Authentication-Provider.md), including AdvancedUserInterface support. 7 | * An [LDAP form type](/Resources/doc/LDAP-Object-Form-Type.md) to easily use LDAP objects in forms. 8 | * An LDAP type for Doctrine to easily store and retrieve LDAP results in a Doctrine entity. 9 | * Logging capabilities for LDAP operations. 10 | * Web Debug Toolbar integration for LDAP operations. 11 | * Integration of [LdapTools events](/Resources/doc/LDAP-Events.md) for LDAP operations (authentication, creation, modification, etc) using service tags. 12 | 13 | **Note**: The LdapTools library requires PHP 5.6+. 14 | 15 | ### Installation 16 | 17 | The recommended way to install the LdapToolsBundle is using [Composer](http://getcomposer.org/download/): 18 | 19 | ```bash 20 | composer require ldaptools/ldaptools-bundle 21 | ``` 22 | 23 | Then enable the bundle in the kernel: 24 | 25 | ```php 26 | // app/AppKernel.php 27 | class AppKernel extends Kernel 28 | { 29 | public function registerBundles() 30 | { 31 | $bundles = array( 32 | // ... 33 | new LdapTools\Bundle\LdapToolsBundle\LdapToolsBundle(), 34 | ); 35 | 36 | // ... 37 | } 38 | } 39 | ``` 40 | 41 | ### Getting Started 42 | 43 | After installing the bundle you can run the following command to assist in generating/testing your LDAP config: 44 | 45 | ```php 46 | # It will prompt for some basic questions (LDAP server, username/password to use, etc) 47 | php bin/console ldaptools:generate:config 48 | ``` 49 | 50 | Adding your domain(s) to the `config.yml` file is as easy as the following example: 51 | 52 | ```yaml 53 | # app/config/config.yml 54 | ldap_tools: 55 | domains: 56 | # The below "example" key can be anything you want. It just has to be a unique name for the YML config. 57 | example: 58 | # The LDAP FQDN is required 59 | domain_name: example.local 60 | # The username to use for the LDAP connection 61 | username: foo 62 | # The password to use for the username 63 | password: secret 64 | # The base DN for LDAP searches (queried from the RootDSE if not provided) 65 | base_dn: "dc=example,dc=local" 66 | # The LDAP servers to use for the connection (Queried from DNS if not provided) 67 | servers: ["dc1", "dc2", "dc3"] 68 | # Define another domain if you want 69 | foo: 70 | domain_name: foo.bar 71 | username: foo 72 | password: bar 73 | servers: ['dc1.foo.bar', 'dc2.foo.bar'] 74 | base_dn: 'dc=foo,dc=bar' 75 | ``` 76 | 77 | Domain configuration options are also documented in the [LdapTools documentation](https://github.com/ldaptools/ldaptools/blob/master/docs/en/reference/Main-Configuration.md#domain-section). 78 | 79 | Then in your controller you can use the `ldap_tools.ldap_manager` service to query/modify/create LDAP objects... 80 | 81 | ```php 82 | 83 | class DefaultController extends Controller 84 | { 85 | public function indexAction() 86 | { 87 | $ldap = $this->get('ldap_tools.ldap_manager'); 88 | 89 | $users = $ldap->buildLdapQuery()->fromUsers()->getLdapQuery()->getResult(); 90 | 91 | $users->count(); 92 | foreach ($users as $user) { 93 | $user->getFirstName(); 94 | $user->getLastName(); 95 | $user->getUsername(); 96 | } 97 | 98 | # ... 99 | } 100 | } 101 | ``` 102 | 103 | ### Generate/Retrieve Your LDAP SSL Certificate 104 | 105 | If you want a quick way to retrieve your server's LDAP SSL certificate you can run a simple command to output it: 106 | 107 | ```php 108 | # Just supply your LDAP server name... 109 | php bin/console ldaptools:generate:sslcert --server "servername" 110 | ``` 111 | 112 | ### Documentation 113 | 114 | * [Configuration Reference](/Resources/doc/Configuration-Reference.md) 115 | * [LDAP Authentication Provider](/Resources/doc/LDAP-Authentication-Provider.md) 116 | * [LDAP Authentication with the FOSUserBundle](/Resources/doc/LDAP-Authentication-With-The-FOSUserBundle.md) 117 | * [Save LDAP Users to the Database After Login](/Resources/doc/Save-LDAP-Users-to-the-Database-After-Login.md) 118 | * [LDAP Object Form Type](/Resources/doc/LDAP-Object-Form-Type.md) 119 | * [LdapTools LDAP Events](/Resources/doc/LDAP-Events.md) 120 | * [Bundle Events Reference](/Resources/doc/Bundle-Event-Reference.md) 121 | * [LDIF Parser URL Loaders](/Resources/doc/LDIF-Parser-URL-Loaders.md) 122 | -------------------------------------------------------------------------------- /Security/User/LdapRoleMapper.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Security\User; 12 | 13 | use LdapTools\Connection\LdapConnection; 14 | use LdapTools\LdapManager; 15 | use LdapTools\Object\LdapObjectCollection; 16 | use LdapTools\Utilities\LdapUtilities; 17 | 18 | /** 19 | * Maps LDAP groups to Symfony Roles for a LdapUserInterface user. 20 | * 21 | * @author Chad Sikorra 22 | */ 23 | class LdapRoleMapper 24 | { 25 | /** 26 | * @var LdapManager 27 | */ 28 | protected $ldap; 29 | 30 | /** 31 | * @var array 32 | */ 33 | protected $options = [ 34 | 'check_groups_recursively' => true, 35 | 'default_role' => 'ROLE_USER', 36 | 'roles' => [], 37 | 'role_ldap_type' => 'group', 38 | 'role_attributes' => [ 39 | 'guid' => 'guid', 40 | 'sid' => 'sid', 41 | 'name' => 'name', 42 | 'members' => 'members', 43 | ], 44 | ]; 45 | 46 | /** 47 | * @param LdapManager $ldap 48 | * @param array $options 49 | */ 50 | public function __construct(LdapManager $ldap, array $options) 51 | { 52 | $this->ldap = $ldap; 53 | $this->options = array_merge($this->options, $options); 54 | } 55 | 56 | /** 57 | * Set the roles for the user based on LDAP group membership. 58 | * 59 | * @param LdapUserInterface $user 60 | * @return LdapUserInterface 61 | */ 62 | public function setRoles(LdapUserInterface $user) 63 | { 64 | $roles = []; 65 | 66 | if ($this->options['default_role']) { 67 | $roles[] = $this->options['default_role']; 68 | } 69 | 70 | if (!empty($this->options['roles'])) { 71 | $groups = $this->getGroupsForUser($user); 72 | 73 | foreach ($this->options['roles'] as $role => $roleGroups) { 74 | if ($this->hasGroupForRoles($roleGroups, $groups)) { 75 | $roles[] = $role; 76 | } 77 | } 78 | } 79 | 80 | $user->setRoles($roles); 81 | 82 | return $user; 83 | } 84 | 85 | /** 86 | * Check all of the groups that are valid for a specific role against all of the LDAP groups that the user belongs 87 | * to. 88 | * 89 | * @param array $roleGroups 90 | * @param LdapObjectCollection $ldapGroups 91 | * @return bool 92 | */ 93 | protected function hasGroupForRoles(array $roleGroups, LdapObjectCollection $ldapGroups) 94 | { 95 | foreach ($roleGroups as $roleGroup) { 96 | if (LdapUtilities::isValidLdapObjectDn($roleGroup)) { 97 | $attribute = 'dn'; 98 | } elseif (preg_match(LdapUtilities::MATCH_GUID, $roleGroup)) { 99 | $attribute = $this->options['role_attributes']['guid']; 100 | } elseif (preg_match(LdapUtilities::MATCH_SID, $roleGroup)) { 101 | $attribute = $this->options['role_attributes']['sid']; 102 | } else { 103 | $attribute = $this->options['role_attributes']['name']; 104 | } 105 | 106 | if ($this->hasGroupWithAttributeValue($ldapGroups, $attribute, $roleGroup)) { 107 | return true; 108 | } 109 | } 110 | 111 | return false; 112 | } 113 | 114 | /** 115 | * Check each LDAP group to see if any of them have an attribute with a specific value. 116 | * 117 | * @param LdapObjectCollection $groups 118 | * @param string $attribute 119 | * @param string $value 120 | * @return bool 121 | */ 122 | protected function hasGroupWithAttributeValue(LdapObjectCollection $groups, $attribute, $value) 123 | { 124 | $value = strtolower($value); 125 | 126 | /** @var \LdapTools\Object\LdapObject $group */ 127 | foreach ($groups as $group) { 128 | if ($group->has($attribute) && strtolower($group->get($attribute)) === $value) { 129 | return true; 130 | } 131 | } 132 | 133 | return false; 134 | } 135 | 136 | /** 137 | * @param LdapUserInterface $user 138 | * @return LdapObjectCollection 139 | */ 140 | protected function getGroupsForUser(LdapUserInterface $user) 141 | { 142 | $select = $this->options['role_attributes']; 143 | unset($select['members']); 144 | 145 | $query = $this->ldap->buildLdapQuery() 146 | ->from($this->options['role_ldap_type']) 147 | ->select(array_values($select)); 148 | 149 | /** 150 | * @todo How to support recursive group checks for all LDAP types? Need a recursive method check of sorts... 151 | */ 152 | if ($this->ldap->getConnection()->getConfig()->getLdapType() === LdapConnection::TYPE_AD && $this->options['check_groups_recursively']) { 153 | $query->where($query->filter()->hasMemberRecursively($user->getLdapGuid(), $this->options['role_attributes']['members'])); 154 | } else { 155 | $query->where([$this->options['role_attributes']['members'] => $user->getLdapGuid()]); 156 | } 157 | 158 | return $query->getLdapQuery()->getResult(); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/Security/User/LdapRoleMapperSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\Security\User; 12 | 13 | use LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUser; 14 | use LdapTools\Connection\LdapConnection; 15 | use LdapTools\DomainConfiguration; 16 | use LdapTools\LdapManager; 17 | use LdapTools\Object\LdapObject; 18 | use LdapTools\Object\LdapObjectCollection; 19 | use LdapTools\Query\Builder\ADFilterBuilder; 20 | use LdapTools\Query\LdapQuery; 21 | use LdapTools\Query\LdapQueryBuilder; 22 | use PhpSpec\ObjectBehavior; 23 | use Prophecy\Argument; 24 | 25 | class LdapRoleMapperSpec extends ObjectBehavior 26 | { 27 | protected $user; 28 | 29 | protected $options = [ 30 | 'roles' => [ 31 | 'ROLE_AWESOME' => ['foo'], 32 | 'ROLE_ADMIN' => ['291d8444-9d5b-4b0a-a6d7-853408f704d5'], 33 | 'ROLE_DN' => ['cn=Stuff,dc=example,dc=local'], 34 | 'ROLE_SID' => ['S-1-5-18'], 35 | ], 36 | ]; 37 | 38 | function let(LdapManager $ldap, LdapQueryBuilder $qb, LdapQuery $query, LdapConnection $connection) 39 | { 40 | $this->user = new LdapUser(); 41 | $this->user->refresh([ 42 | 'name' => 'Stuff', 43 | 'username' => 'foo', 44 | 'guid' => '291d8444-9d5b-4b0a-a6d7-853408f704d5', 45 | 'dn' => 'cn=Stuff,dc=example,dc=local', 46 | ]); 47 | 48 | $groups = new LdapObjectCollection(); 49 | $groups->add(new LdapObject(['name' => 'Foo', 'dn' => 'cn=Foo,dc=example,dc=local'])); 50 | $groups->add(new LdapObject(['guid' => '291d8444-9d5b-4b0a-a6d7-853408f704d5', 'dn' => 'cn=Bar,dc=example,dc=local'])); 51 | $groups->add(new LdapObject(['sid' => 'S-1-5-18', 'dn' => 'cn=LocalSys,dc=example,dc=local'])); 52 | $groups->add(new LdapObject(['name' => 'Just a DN', 'dn' => 'cn=Stuff,dc=example,dc=local'])); 53 | 54 | $filter = new ADFilterBuilder(); 55 | $qb->filter()->willReturn($filter); 56 | $qb->where(Argument::type('LdapTools\Query\Operator\MatchingRule'))->willReturn($qb); 57 | $qb->from('group')->willReturn($qb); 58 | $qb->select(["guid", "sid", "name"])->willReturn($qb); 59 | 60 | $ldap->buildLdapQuery()->willReturn($qb); 61 | $qb->getLdapQuery()->willReturn($query); 62 | $query->getResult()->willReturn($groups); 63 | 64 | $config = new DomainConfiguration('foo.bar'); 65 | 66 | $ldap->getDomainContext()->willReturn('foo.bar'); 67 | $ldap->getConnection()->willReturn($connection); 68 | $connection->getConfig()->willReturn($config); 69 | 70 | $this->beConstructedWith($ldap, $this->options); 71 | } 72 | 73 | function it_is_initializable() 74 | { 75 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\Security\User\LdapRoleMapper'); 76 | } 77 | 78 | function it_should_set_the_roles_properly_for_the_returned_groups($query) 79 | { 80 | $this->setRoles($this->user)->getRoles()->shouldBeEqualTo(['ROLE_USER', 'ROLE_AWESOME', 'ROLE_ADMIN', 'ROLE_DN', 'ROLE_SID']); 81 | 82 | $query->getResult()->willReturn(new LdapObjectCollection(new LdapObject(['name' => 'foo']))); 83 | $this->setRoles($this->user)->getRoles()->shouldBeEqualTo(['ROLE_USER', 'ROLE_AWESOME']); 84 | 85 | $query->getResult()->willReturn(new LdapObjectCollection(new LdapObject(['name' => 'foo.bar']))); 86 | $this->setRoles($this->user)->getRoles()->shouldBeEqualTo(['ROLE_USER']); 87 | } 88 | 89 | function it_should_not_search_recursively_when_the_LDAP_type_is_openldap($qb, $connection) 90 | { 91 | $connection->getConfig()->willReturn((new DomainConfiguration('foo.bar'))->setLdapType('openldap')); 92 | 93 | $qb->where(Argument::type('LdapTools\Query\Operator\MatchingRule'))->shouldNotBeCalled(); 94 | $qb->where(Argument::withKey('members'))->shouldBeCalled(); 95 | 96 | $this->setRoles($this->user); 97 | } 98 | 99 | function it_should_search_recursively_when_the_LDAP_type_is_active_directory($qb) 100 | { 101 | $qb->where(Argument::type('LdapTools\Query\Operator\MatchingRule'))->shouldBeCalled(); 102 | 103 | $this->setRoles($this->user); 104 | } 105 | 106 | function it_should_set_the_ldap_type_for_the_role_query($ldap, $qb) 107 | { 108 | $this->beConstructedWith($ldap, array_merge($this->options, ['role_ldap_type' => 'foo'])); 109 | 110 | $qb->from('foo')->shouldBeCalled()->willReturn($qb); 111 | 112 | $this->setRoles($this->user); 113 | } 114 | 115 | function it_should_set_the_attribute_map_for_the_role_query($ldap, $qb) 116 | { 117 | $this->beConstructedWith($ldap, array_merge($this->options, ['role_attributes' => [ 118 | 'members' => 'members', 119 | 'name' => 'cn', 120 | 'guid' => 'foo', 121 | 'sid' => 'bar' 122 | ]])); 123 | 124 | $qb->select(['cn', 'foo', 'bar'])->shouldBeCalled()->willReturn($qb); 125 | 126 | $this->setRoles($this->user); 127 | } 128 | 129 | function it_should_not_set_a_default_role_if_it_is_set_to_null($ldap, $query) 130 | { 131 | $this->beConstructedWith($ldap, array_merge($this->options, ['default_role' => null])); 132 | 133 | $query->getResult()->willReturn(new LdapObjectCollection(new LdapObject(['name' => 'Test']))); 134 | 135 | $this->setRoles($this->user)->getRoles()->shouldBeEqualTo([]); 136 | } 137 | 138 | function it_should_set_the_default_role($ldap) 139 | { 140 | $this->beConstructedWith($ldap, array_merge($this->options, ['default_role' => 'foobar'])); 141 | 142 | $this->setRoles($this->user)->getRoles()->shouldContain('FOOBAR'); 143 | } 144 | 145 | function it_should_not_query_ldap_if_no_roles_are_defined($ldap, $query) 146 | { 147 | $this->beConstructedWith($ldap, ['roles' => []]); 148 | 149 | $query->getResult()->shouldNotBeCalled(); 150 | $this->setRoles($this->user); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Security/Authentication/Provider/LdapAuthenticationProvider.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Security\Authentication\Provider; 12 | 13 | use LdapTools\Bundle\LdapToolsBundle\Event\LdapLoginEvent; 14 | use LdapTools\Bundle\LdapToolsBundle\Security\LdapAuthenticationTrait; 15 | use LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUserChecker; 16 | use LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUserProvider; 17 | use LdapTools\Exception\LdapConnectionException; 18 | use LdapTools\LdapManager; 19 | use LdapTools\Operation\AuthenticationOperation; 20 | use LdapTools\Operation\AuthenticationResponse; 21 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 22 | use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface; 23 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 24 | use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; 25 | use Symfony\Component\Security\Core\Exception\BadCredentialsException; 26 | use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; 27 | use Symfony\Component\Security\Core\User\UserInterface; 28 | use Symfony\Component\Security\Core\User\UserProviderInterface; 29 | 30 | /** 31 | * Authenticate a user against LDAP. 32 | * 33 | * @author Chad Sikorra 34 | */ 35 | class LdapAuthenticationProvider implements AuthenticationProviderInterface 36 | { 37 | use LdapAuthenticationTrait; 38 | 39 | /** 40 | * @var UserProviderInterface 41 | */ 42 | protected $userProvider; 43 | 44 | /** 45 | * @var bool 46 | */ 47 | protected $hideUserNotFoundExceptions; 48 | 49 | /** 50 | * @var string 51 | */ 52 | protected $providerKey; 53 | 54 | /** 55 | * @var LdapUserChecker 56 | */ 57 | protected $userChecker; 58 | 59 | /** 60 | * @var EventDispatcherInterface 61 | */ 62 | protected $dispatcher; 63 | 64 | /** 65 | * @var array 66 | */ 67 | protected $options = [ 68 | 'login_query_attribute' => null, 69 | ]; 70 | 71 | /** 72 | * @param string $providerKey 73 | * @param bool $hideUserNotFoundExceptions 74 | * @param UserProviderInterface $userProvider 75 | * @param LdapUserChecker $userChecker 76 | * @param LdapManager $ldap 77 | * @param EventDispatcherInterface $dispatcher 78 | * @param LdapUserProvider $ldapUserProvider 79 | * @param array $options 80 | */ 81 | public function __construct( 82 | $providerKey, 83 | $hideUserNotFoundExceptions = true, 84 | UserProviderInterface $userProvider, 85 | LdapUserChecker $userChecker, 86 | LdapManager $ldap, 87 | EventDispatcherInterface $dispatcher, 88 | LdapUserProvider $ldapUserProvider, 89 | array $options 90 | ) { 91 | $this->userProvider = $userProvider; 92 | $this->ldap = $ldap; 93 | $this->hideUserNotFoundExceptions = $hideUserNotFoundExceptions; 94 | $this->providerKey = $providerKey; 95 | $this->userChecker = $userChecker; 96 | $this->dispatcher = $dispatcher; 97 | $this->ldapUserProvider = $ldapUserProvider; 98 | $this->options = array_merge($this->options, $options); 99 | } 100 | 101 | /** 102 | * {@inheritdoc} 103 | */ 104 | public function authenticate(TokenInterface $token) 105 | { 106 | $domain = $this->ldap->getDomainContext(); 107 | 108 | try { 109 | $this->switchDomainIfNeeded($this->getDomainFromToken($token)); 110 | $this->setLdapCredentialsIfNeeded($token->getUsername(), $token->getCredentials(), $this->userProvider); 111 | $user = $this->userProvider->loadUserByUsername($token->getUsername()); 112 | $this->userChecker->checkPreAuth($user); 113 | $token = $this->doAuthentication($user, $token); 114 | $this->userChecker->checkPostAuth($user); 115 | } catch (UsernameNotFoundException $e) { 116 | $this->hideOrThrow($e, $this->hideUserNotFoundExceptions); 117 | } catch (BadCredentialsException $e) { 118 | $this->hideOrThrow($e, $this->hideUserNotFoundExceptions); 119 | } catch (LdapConnectionException $e) { 120 | $this->hideOrThrow($e, $this->hideUserNotFoundExceptions); 121 | } finally { 122 | $this->switchDomainBackIfNeeded($domain); 123 | } 124 | 125 | return $token; 126 | } 127 | 128 | /** 129 | * {@inheritdoc} 130 | */ 131 | public function supports(TokenInterface $token) 132 | { 133 | return ($token instanceof UsernamePasswordToken); 134 | } 135 | 136 | /** 137 | * @param UserInterface $user 138 | * @param TokenInterface $token 139 | * @return UsernamePasswordToken 140 | */ 141 | protected function doAuthentication(UserInterface $user, TokenInterface $token) 142 | { 143 | $auth = new AuthenticationOperation( 144 | $this->getBindUsername($user, $this->options['login_query_attribute']), 145 | $token->getCredentials() 146 | ); 147 | /** @var AuthenticationResponse $response */ 148 | $response = $this->ldap->getConnection()->execute($auth); 149 | 150 | if (!$response->isAuthenticated()) { 151 | $this->userChecker->checkLdapErrorCode( 152 | $user, 153 | $response->getErrorCode(), 154 | $this->ldap->getConnection()->getConfig()->getLdapType() 155 | ); 156 | 157 | throw new BadCredentialsException($response->getErrorMessage(), $response->getErrorCode()); 158 | } 159 | $this->dispatcher->dispatch(LdapLoginEvent::SUCCESS, new LdapLoginEvent($user, $token)); 160 | 161 | $newToken = new UsernamePasswordToken($user, null, $this->providerKey, $user->getRoles()); 162 | $newToken->setAttributes($token->getAttributes()); 163 | 164 | return $newToken; 165 | } 166 | 167 | /** 168 | * @param TokenInterface $token 169 | * @return string 170 | */ 171 | protected function getDomainFromToken(TokenInterface $token) 172 | { 173 | return $token->hasAttribute('ldap_domain') ? $token->getAttribute('ldap_domain') : ''; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /Resources/doc/LDAP-Events.md: -------------------------------------------------------------------------------- 1 | LDAP Events 2 | ================ 3 | 4 | There are several LDAP related events to hook into. The events themselves are integrated into [LdapTools](https://github.com/ldaptools/ldaptools). 5 | You can easily access them with this bundle by tagging your services to act on specific LDAP events. The recommended way 6 | of doing this is creating a subscriber class, then creating a tagged service using that class. But overall there are 7 | three main methods to using events: Service Event Subscribers, Service Event Listeners, or defining them using the event 8 | dispatcher associated with the LdapManager. 9 | 10 | * [Service Event Subscribers](#the-service-event-subscriber-method) 11 | * [Service Event Listeners](#the-service-event-listener-method) 12 | * [Event Dispatcher Method](#the-ldapmanager-event-dispatcher-method) 13 | * [Injecting the LdapManager](#injecting-the-ldapmanager) 14 | 15 | For additional information of events please see the [LdapTools documentation](https://github.com/ldaptools/ldaptools/blob/master/docs/en/reference/Events.md). 16 | 17 | ## The Service Event Subscriber Method 18 | 19 | Using this method you can define all your event actions in a single class using different methods. Then define a service 20 | that uses that class along with a special tag. 21 | 22 | 1. Create the subscriber class: 23 | 24 | ```php 25 | namespace AppBundle\Subscriber; 26 | 27 | use LdapTools\Event\EventSubscriberInterface; 28 | use LdapTools\Event\Event; 29 | use LdapTools\Event\LdapObjectEvent; 30 | use LdapTools\Event\LdapObjectCreationEvent; 31 | 32 | class LdapToolsSubscriber implements EventSubscriberInterface 33 | { 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public static function getSubscribedEvents() 38 | { 39 | /** 40 | * This should return an array mapping where the key is a LdapTools event name and the value is the method name 41 | * in this class that should be called for the event. All event names are defined as constants within the 42 | * '\LdapTools\Event\Event' class. 43 | */ 44 | return [ 45 | Event::LDAP_OBJECT_BEFORE_MODIFY => 'beforeModify', 46 | Event::LDAP_OBJECT_AFTER_CREATE > 'afterCreate', 47 | ]; 48 | } 49 | 50 | /** 51 | * Will be called before a LDAP object modification is saved to LDAP. 52 | */ 53 | public function beforeModify(LdapObjectEvent $event) 54 | { 55 | $ldapObject = $event->getLdapObject(); 56 | 57 | // Perform some custom logic against the LDAP object that's about to be modified... 58 | } 59 | 60 | /** 61 | * Will be called after an LDAP object is created. 62 | */ 63 | public function afterCreate(LdapObjectCreationEvent $event) 64 | { 65 | $attributes = $event->getData(); 66 | $dn = $event->getDn(); 67 | 68 | // Perform some custom logic regarding the LDAP object that was created... 69 | } 70 | } 71 | ``` 72 | 73 | 2. Define the class as a service in your service config file and tag it: 74 | 75 | ** 76 | ```xml 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | ``` 89 | 90 | ## The Service Event Listener Method 91 | 92 | Using this method you can define your event action in a class, then you create a service that uses that class and define 93 | the event name and method it should call within the service definition. 94 | 95 | 1. Add the method to a class: 96 | 97 | ```php 98 | namespace AppBundle\Misc; 99 | 100 | use LdapTools\Event\LdapObjectEvent; 101 | 102 | class AppUtility 103 | { 104 | #... 105 | 106 | /** 107 | * Will be called after a LDAP objects modifications are saved to LDAP. 108 | */ 109 | public function afterModify(LdapObjectEvent $event) 110 | { 111 | $ldapObject = $event->getLdapObject(); 112 | 113 | // Perform some custom logic against the LDAP object that was modified... 114 | } 115 | 116 | # ... 117 | } 118 | ``` 119 | 120 | 2. Define the class as a service in your service config file and tag it: 121 | 122 | *In your bundles services config file* 123 | ```xml 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | ``` 136 | 137 | The difference above in this method is that you directly define the event name and class method name directly on the 138 | service definition. Additionally, you tag it with `ldap_tools.event_listener`. 139 | 140 | ## The LdapManager Event Dispatcher Method 141 | 142 | It's also possible to define an event action by directly using the event dispatcher of the LdapManager: 143 | 144 | ```php 145 | use LdapTools\Event\Event; 146 | use LdapTools\Event\LdapObjectEvent; 147 | 148 | class DefaultController extends Controller 149 | { 150 | public function indexAction() 151 | { 152 | $dispatcher = $this->get('ldap_tools.ldap_manager')->getEventDispatcher(); 153 | 154 | $dispatcher->addListener(Event::LDAP_OBJECT_BEFORE_MODIFY, function(LdapObjectEvent $event) { 155 | if ($event->getLdapObject()->hasFirstName('foo')) { 156 | $event->getLdapObject()->setFirstName('bar'); 157 | } 158 | }); 159 | 160 | # ... 161 | } 162 | } 163 | ``` 164 | 165 | ## Injecting the LdapManager 166 | 167 | Often times you might want to inject the `ldap_tools.ldap_manager` service into one of your event definitions you have 168 | tagged as a service. However, by doing this you will create a circular reference error in the Dependency Injection Container. 169 | To get around this you can [mark the service as lazy](http://symfony.com/doc/current/service_container/lazy_services.html) in your event service definition: 170 | 171 | 1. Install the needed dependency: 172 | 173 | ```bash 174 | composer require ocramius/proxy-manager 175 | ``` 176 | 177 | 2. Update your service definition: 178 | 179 | ```yaml 180 | services: 181 | app_bundle.event.ldap_listener: 182 | class: AppBundle\Event\LdapEventListener 183 | # This needs to be set to true...which will delay resolution/instantiation of the service... 184 | lazy: true 185 | # Inject the LdapManager in your constructor... 186 | arguments: ['@ldap_tools.ldap_manager'] 187 | # Tag the listener so it gets loaded... 188 | tags: 189 | - { name: 'ldap_tools.event_listener', event: 'ldap.operation.execute.before', method: beforeOperation } 190 | ``` 191 | 192 | 3. Clear and Warm-up the cache: 193 | 194 | ```bash 195 | php bin/console cache:clear --env=dev 196 | php bin/console cache:warmup --env=dev 197 | ``` 198 | 199 | Now when you use the service definition with the LdapManager it should no longer result in a circular reference error in 200 | the DiC. -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/Form/ChoiceLoader/LdapObjectChoiceLoaderSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\Form\ChoiceLoader; 12 | 13 | use LdapTools\LdapManager; 14 | use LdapTools\Object\LdapObject; 15 | use LdapTools\Object\LdapObjectCollection; 16 | use LdapTools\Object\LdapObjectType; 17 | use LdapTools\Query\LdapQuery; 18 | use LdapTools\Query\LdapQueryBuilder; 19 | use PhpSpec\ObjectBehavior; 20 | 21 | /** 22 | * @require Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface 23 | */ 24 | class LdapObjectChoiceLoaderSpec extends ObjectBehavior 25 | { 26 | public function let(LdapManager $ldap, LdapQueryBuilder $qb, LdapQuery $query) 27 | { 28 | $ldap->buildLdapQuery()->willReturn($qb); 29 | $qb->getLdapQuery()->willReturn($query); 30 | 31 | $this->beConstructedWith($ldap, LdapObjectType::USER); 32 | } 33 | 34 | function it_is_initializable() 35 | { 36 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\Form\ChoiceLoader\LdapObjectChoiceLoader'); 37 | } 38 | 39 | function it_should_load_a_choice_list($qb, $query) 40 | { 41 | // These are the default attributes it should select (name/value for the choice) 42 | $qb->select(['guid', 'name'])->shouldBeCalled()->willReturn($qb); 43 | $qb->from("user")->shouldBeCalled()->willReturn($qb); 44 | 45 | $collection = new LdapObjectCollection( 46 | new LdapObject(['name' => 'foo', 'guid' => '123'], 'user'), 47 | new LdapObject(['name' => 'bar', 'guid' => '456'], 'user') 48 | ); 49 | $query->getResult()->shouldBeCalled()->willReturn($collection); 50 | 51 | $this->loadChoiceList()->shouldBeAnInstanceOf('\Symfony\Component\Form\ChoiceList\ArrayChoiceList'); 52 | } 53 | 54 | function it_should_not_query_ldap_if_it_already_loaded_the_choicelist($qb, $query) 55 | { 56 | // These are the default attributes it should select (name/value for the choice) 57 | $qb->select(['guid', 'name'])->shouldBeCalled()->willReturn($qb); 58 | $qb->from("user")->shouldBeCalled()->willReturn($qb); 59 | 60 | $collection = new LdapObjectCollection( 61 | new LdapObject(['name' => 'foo', 'guid' => '123'], 'user'), 62 | new LdapObject(['name' => 'bar', 'guid' => '456'], 'user') 63 | ); 64 | $query->getResult()->shouldBeCalledTimes(1)->willReturn($collection); 65 | 66 | $this->loadChoiceList()->shouldBeAnInstanceOf('\Symfony\Component\Form\ChoiceList\ArrayChoiceList'); 67 | // The second call should just load the already returned choice list.. 68 | $this->loadChoiceList()->shouldBeAnInstanceOf('\Symfony\Component\Form\ChoiceList\ArrayChoiceList'); 69 | } 70 | 71 | function it_should_support_setting_the_ldap_type_and_choice_label_and_values($ldap, $qb, $query) 72 | { 73 | $this->beConstructedWith($ldap, LdapObjectType::GROUP, 'upn', 'sid'); 74 | 75 | // These are the default attributes it should select (name/value for the choice) 76 | $qb->select(['sid', 'upn'])->shouldBeCalled()->willReturn($qb); 77 | $qb->from("group")->shouldBeCalled()->willReturn($qb); 78 | 79 | $collection = new LdapObjectCollection( 80 | new LdapObject(['upn' => 'foo', 'sid' => '123'], 'group'), 81 | new LdapObject(['upn' => 'bar', 'sid' => '456'], 'group') 82 | ); 83 | $query->getResult()->shouldBeCalled()->willReturn($collection); 84 | 85 | $this->loadChoiceList()->shouldBeAnInstanceOf('\Symfony\Component\Form\ChoiceList\ArrayChoiceList'); 86 | } 87 | 88 | function it_should_support_setting_a_specific_ldap_query_builder_to_load_the_choicelist($ldap, $qb, $query) 89 | { 90 | $this->beConstructedWith($ldap, LdapObjectType::GROUP, 'upn', 'sid', $qb); 91 | $qb->getLdapQuery()->shouldBeCalled()->willReturn($query); 92 | 93 | $collection = new LdapObjectCollection( 94 | new LdapObject(['upn' => 'foo', 'sid' => '123'], 'group'), 95 | new LdapObject(['upn' => 'bar', 'sid' => '456'], 'group') 96 | ); 97 | $query->getResult()->shouldBeCalled()->willReturn($collection); 98 | 99 | $this->loadChoiceList()->shouldBeAnInstanceOf('\Symfony\Component\Form\ChoiceList\ArrayChoiceList'); 100 | } 101 | 102 | function it_should_support_calling_a_closure_against_the_query_builder_when_loading_the_choicelist($qb, $query, $ldap) 103 | { 104 | $foo = function($qb) { 105 | $qb->where(['foo' => 'bar']); 106 | }; 107 | $this->beConstructedWith($ldap, LdapObjectType::GROUP, 'upn', 'sid', $foo); 108 | 109 | // These are the default attributes it should select (name/value for the choice) 110 | $qb->select(['sid', 'upn'])->shouldBeCalled()->willReturn($qb); 111 | $qb->from("group")->shouldBeCalled()->willReturn($qb); 112 | 113 | $collection = new LdapObjectCollection( 114 | new LdapObject(['upn' => 'foo', 'sid' => '123'], 'group'), 115 | new LdapObject(['upn' => 'bar', 'sid' => '456'], 'group') 116 | ); 117 | $query->getResult()->shouldBeCalled()->willReturn($collection); 118 | 119 | // As the result of the closure... 120 | $qb->where(['foo' => 'bar'])->shouldBeCalled(); 121 | 122 | $this->loadChoiceList()->shouldBeAnInstanceOf('\Symfony\Component\Form\ChoiceList\ArrayChoiceList'); 123 | } 124 | 125 | function it_should_load_values_for_choices($qb, $query) 126 | { 127 | $collection = new LdapObjectCollection( 128 | new LdapObject(['name' => 'foo', 'guid' => '123'], 'user'), 129 | new LdapObject(['name' => 'bar', 'guid' => '456'], 'user') 130 | ); 131 | 132 | $this->loadValuesForChoices($collection->toArray())->shouldBeEqualTo(['123', '456']); 133 | 134 | $qb->select(['guid', 'name'])->shouldBeCalled()->willReturn($qb); 135 | $qb->from("user")->shouldBeCalled()->willReturn($qb); 136 | 137 | $collection = new LdapObjectCollection( 138 | new LdapObject(['name' => 'foo', 'guid' => '123'], 'user'), 139 | new LdapObject(['name' => 'bar', 'guid' => '456'], 'user') 140 | ); 141 | $query->getResult()->shouldBeCalled()->willReturn($collection); 142 | 143 | $this->loadChoiceList(); 144 | $this->loadValuesForChoices($collection->toArray())->shouldBeEqualTo(['123', '456']); 145 | } 146 | 147 | function it_should_load_choices_for_values($qb, $query) 148 | { 149 | $qb->select(['guid', 'name'])->shouldBeCalled()->willReturn($qb); 150 | $qb->from("user")->shouldBeCalled()->willReturn($qb); 151 | 152 | $user1 = new LdapObject(['name' => 'foo', 'guid' => '123'], 'user'); 153 | $user2 = new LdapObject(['name' => 'bar', 'guid' => '456'], 'user'); 154 | $collection = new LdapObjectCollection( 155 | $user1, 156 | $user2 157 | ); 158 | $query->getResult()->shouldBeCalledTimes(1)->willReturn($collection); 159 | 160 | $this->loadChoicesForValues(['123'])->shouldBeEqualTo([$user1]); 161 | $this->loadChoicesForValues(['456'])->shouldBeEqualTo([$user2]); 162 | $this->loadChoicesForValues([])->shouldBeEqualTo([]); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /DependencyInjection/LdapToolsExtension.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\DependencyInjection; 12 | 13 | use Symfony\Component\DependencyInjection\ContainerBuilder; 14 | use Symfony\Component\Config\FileLocator; 15 | use Symfony\Component\DependencyInjection\Reference; 16 | use Symfony\Component\HttpKernel\DependencyInjection\Extension; 17 | use Symfony\Component\DependencyInjection\Loader; 18 | 19 | /** 20 | * Load and configure the needed services/parameters for the bundle. 21 | * 22 | * @author Chad Sikorra 23 | */ 24 | class LdapToolsExtension extends Extension 25 | { 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function load(array $configs, ContainerBuilder $container) 30 | { 31 | $configuration = new Configuration($container->getParameter('kernel.debug')); 32 | $config = $this->processConfiguration($configuration, $configs); 33 | 34 | $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 35 | $loader->load('services.xml'); 36 | 37 | $this->setLdapConfigDefinition($container, $config); 38 | $this->setDoctrineConfiguration($container, $config); 39 | $this->setSecurityConfiguration($container, $config['security']); 40 | $this->setGuardConfiguration($container, $config['security']['guard'], $config['security']); 41 | } 42 | 43 | /** 44 | * @param ContainerBuilder $container 45 | * @param array $config 46 | */ 47 | protected function setDoctrineConfiguration(ContainerBuilder $container, array $config) 48 | { 49 | // If they explicitly disabled doctrine integration, do nothing... 50 | if (!$config['doctrine']['integration_enabled']) { 51 | return; 52 | } 53 | // We only tag the doctrine event subscriber if there are domains listed in the config... 54 | if (!(isset($config['domains']) && !empty($config['domains']))) { 55 | return; 56 | } 57 | 58 | $connections = array_filter($config['doctrine']['connections']); 59 | if (empty($connections)) { 60 | $container->getDefinition('ldap_tools.doctrine.event_listener.ldap_object')->addTag( 61 | 'doctrine.event_subscriber' 62 | ); 63 | } else { 64 | foreach ($connections as $connection) { 65 | $container->getDefinition('ldap_tools.doctrine.event_listener.ldap_object')->addTag( 66 | 'doctrine.event_subscriber', 67 | ['connection' => $connection] 68 | ); 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * Pass the configuration to be loaded for the LdapTools Configuration class. 75 | * 76 | * @param ContainerBuilder $container 77 | * @param array $config 78 | */ 79 | protected function setLdapConfigDefinition(ContainerBuilder $container, array $config) 80 | { 81 | $ldapCfg = ['general' => $config['general']]; 82 | 83 | // Only tag the cache warmer if there are domains listed in the config... 84 | if (isset($config['domains']) && !empty($config['domains'])) { 85 | $ldapCfg['domains'] = $config['domains']; 86 | $container->getDefinition('ldap_tools.cache_warmer.ldap_tools_cache_warmer')->addTag('kernel.cache_warmer'); 87 | } else { 88 | $container->getDefinition('data_collector.ldap_tools')->replaceArgument(0, null); 89 | } 90 | 91 | $definition = $container->getDefinition('LdapTools\Configuration'); 92 | $definition->addMethodCall('loadFromArray', [$ldapCfg]); 93 | $definition->addMethodCall('setEventDispatcher', [new Reference('ldap_tools.event_dispatcher')]); 94 | 95 | $loggerChain = $container->getDefinition('ldap_tools.log.logger_chain'); 96 | if ($config['logging']) { 97 | $loggerChain->addMethodCall('addLogger', [new Reference('ldap_tools.log.logger')]); 98 | } 99 | if ($config['profiling']) { 100 | $loggerChain->addMethodCall('addLogger', [new Reference('ldap_tools.log.profiler')]); 101 | } 102 | if ($config['logging'] || $config['profiling']) { 103 | $definition->addMethodCall('setLogger', [new Reference('ldap_tools.log.logger_chain')]); 104 | } 105 | } 106 | 107 | /** 108 | * @param ContainerBuilder $container 109 | * @param array $config 110 | */ 111 | protected function setSecurityConfiguration(ContainerBuilder $container, array $config) 112 | { 113 | $container->setParameter('ldap_tools.security.role_mapper.options', [ 114 | 'check_groups_recursively' => $config['check_groups_recursively'], 115 | 'roles' => isset($config['roles']) ? $config['roles'] : [], 116 | 'role_attributes' => $config['role_attributes'], 117 | 'role_ldap_type' => $config['role_ldap_type'], 118 | 'default_role' => $config['default_role'], 119 | ]); 120 | $container->setParameter('ldap_tools.security.user.ldap_user_provider.options', [ 121 | 'additional_attributes' => isset($config['additional_attributes']) ? $config['additional_attributes'] : [], 122 | 'user' => $config['user'], 123 | 'ldap_object_type' => $config['ldap_object_type'], 124 | 'search_base' => $config['search_base'], 125 | 'refresh_user_attributes' => $config['refresh_user_attributes'], 126 | 'refresh_user_roles' => $config['refresh_user_roles'] 127 | ]); 128 | $container->setParameter('ldap_tools.security.authentication.ldap_authentication_provider.options', [ 129 | 'login_query_attribute' => $config['login_query_attribute'], 130 | ]); 131 | } 132 | 133 | protected function setGuardConfiguration(ContainerBuilder $container, array $config, array $security) 134 | { 135 | $container->setParameter('ldap_tools.security.guard.auth_success', [ 136 | 'default_target_path' => $config['default_target_path'], 137 | 'always_use_target_path' => $config['always_use_target_path'], 138 | 'target_path_parameter' => $config['target_path_parameter'], 139 | 'use_referer' => $config['use_referer'], 140 | 'login_path' => $config['login_path'], 141 | ]); 142 | $container->setParameter('ldap_tools.security.guard.auth_failure', [ 143 | 'failure_path' => $config['failure_path'], 144 | 'failure_forward' => $config['failure_forward'], 145 | 'failure_path_parameter' => $config['failure_path_parameter'], 146 | 'login_path' => $config['login_path'], 147 | ]); 148 | $container->setParameter('ldap_tools.security.guard.options', [ 149 | 'username_parameter' => $config['username_parameter'], 150 | 'password_parameter' => $config['password_parameter'], 151 | 'domain_parameter' => $config['domain_parameter'], 152 | 'post_only' => $config['post_only'], 153 | 'remember_me' => $config['remember_me'], 154 | 'login_query_attribute' => $security['login_query_attribute'], 155 | 'http_basic' => $config['http_basic'], 156 | 'http_basic_domain' => $config['http_basic_domain'], 157 | 'http_basic_realm' => $config['http_basic_realm'], 158 | ]); 159 | $container->getDefinition('ldap_tools.security.authentication.form_entry_point') 160 | ->addArgument(new Reference('security.http_utils')) 161 | ->addArgument($config['login_path']) 162 | ->addArgument($config['use_forward']); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Security/User/LdapUserProvider.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Security\User; 12 | 13 | use LdapTools\BatchModify\BatchCollection; 14 | use LdapTools\Bundle\LdapToolsBundle\Event\LoadUserEvent; 15 | use LdapTools\Exception\EmptyResultException; 16 | use LdapTools\Exception\MultiResultException; 17 | use LdapTools\LdapManager; 18 | use LdapTools\Object\LdapObject; 19 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 20 | use Symfony\Component\Security\Core\Exception\UnsupportedUserException; 21 | use Symfony\Component\Security\Core\User\UserInterface; 22 | use Symfony\Component\Security\Core\User\UserProviderInterface; 23 | use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; 24 | 25 | /** 26 | * Loads a user from LDAP. 27 | * 28 | * @author Chad Sikorra 29 | */ 30 | class LdapUserProvider implements UserProviderInterface 31 | { 32 | /** 33 | * @var LdapManager 34 | */ 35 | protected $ldap; 36 | 37 | /** 38 | * @var EventDispatcherInterface 39 | */ 40 | protected $dispatcher; 41 | 42 | /** 43 | * @var LdapRoleMapper 44 | */ 45 | protected $roleMapper; 46 | 47 | /** 48 | * @var array Default attributes selected for the Advanced User Interface. 49 | */ 50 | protected $defaultAttributes = [ 51 | 'username', 52 | 'guid', 53 | 'accountExpirationDate', 54 | 'enabled', 55 | 'groups', 56 | 'locked', 57 | 'passwordMustChange', 58 | ]; 59 | 60 | /** 61 | * @var array 62 | */ 63 | protected $options = [ 64 | 'refresh_user_roles' => false, 65 | 'refresh_user_attributes' => false, 66 | 'search_base' => null, 67 | 'ldap_object_type' => 'user', 68 | 'user' => LdapUser::class, 69 | 'additional_attributes' => [], 70 | ]; 71 | 72 | /** 73 | * @param LdapManager $ldap 74 | * @param EventDispatcherInterface $dispatcher 75 | * @param LdapRoleMapper $roleMapper 76 | * @param array $options 77 | */ 78 | public function __construct(LdapManager $ldap, EventDispatcherInterface $dispatcher, LdapRoleMapper $roleMapper, array $options) 79 | { 80 | $this->ldap = $ldap; 81 | $this->dispatcher = $dispatcher; 82 | $this->roleMapper = $roleMapper; 83 | $this->options = array_merge($this->options, $options); 84 | } 85 | 86 | /** 87 | * {@inheritdoc} 88 | */ 89 | public function loadUserByUsername($username) 90 | { 91 | $this->dispatcher->dispatch(LoadUserEvent::BEFORE, new LoadUserEvent($username, $this->ldap->getDomainContext())); 92 | $ldapUser = $this->getLdapUser('username', $username); 93 | $user = $this->constructUserClass($ldapUser); 94 | $this->roleMapper->setRoles($user); 95 | $this->dispatcher->dispatch(LoadUserEvent::AFTER, new LoadUserEvent($username, $this->ldap->getDomainContext(), $user, $ldapUser)); 96 | 97 | return $user; 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | public function refreshUser(UserInterface $user) 104 | { 105 | if (!$user instanceof LdapUserInterface) { 106 | throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user))); 107 | } 108 | $roles = $user->getRoles(); 109 | 110 | if ($this->options['refresh_user_attributes']) { 111 | $user = $this->constructUserClass($this->getLdapUser('guid', $user->getLdapGuid())); 112 | } 113 | if ($this->options['refresh_user_roles']) { 114 | $this->roleMapper->setRoles($user); 115 | } else { 116 | $user->setRoles($roles); 117 | } 118 | 119 | return $user; 120 | } 121 | 122 | /** 123 | * {@inheritdoc} 124 | */ 125 | public function supportsClass($class) 126 | { 127 | return is_subclass_of($class, LdapUserInterface::class); 128 | } 129 | 130 | /** 131 | * Search for, and return, the LDAP user by a specific attribute. 132 | * 133 | * @param string $attribute 134 | * @param string $value 135 | * @return LdapObject 136 | */ 137 | public function getLdapUser($attribute, $value) 138 | { 139 | try { 140 | $query = $this->ldap->buildLdapQuery() 141 | ->select($this->getAttributesToSelect()) 142 | ->from($this->options['ldap_object_type']) 143 | ->where([$attribute => $value]); 144 | if (!is_null($this->options['search_base'])) { 145 | $query->setBaseDn($this->options['search_base']); 146 | } 147 | return $query->getLdapQuery()->getSingleResult(); 148 | } catch (EmptyResultException $e) { 149 | throw new UsernameNotFoundException(sprintf('Username "%s" was not found.', $value)); 150 | } catch (MultiResultException $e) { 151 | throw new UsernameNotFoundException(sprintf('Multiple results for "%s" were found.', $value)); 152 | } 153 | } 154 | 155 | /** 156 | * Get all the attributes that should be selected for when querying LDAP. 157 | * 158 | * @return array 159 | */ 160 | protected function getAttributesToSelect() 161 | { 162 | return array_values(array_unique(array_filter(array_merge( 163 | $this->defaultAttributes, 164 | $this->options['additional_attributes'] 165 | )))); 166 | } 167 | 168 | /** 169 | * @param LdapObject $ldapObject 170 | * @return LdapUserInterface 171 | */ 172 | protected function constructUserClass(LdapObject $ldapObject) 173 | { 174 | if (!$this->supportsClass($this->options['user'])) { 175 | throw new UnsupportedUserException(sprintf( 176 | 'The LDAP user provider class "%s" must implement "%s".', 177 | $this->options['user'], 178 | LdapUserInterface::class 179 | )); 180 | } 181 | 182 | $errorMessage = 'Unable to instantiate user class "%s". Error was: %s'; 183 | try { 184 | /** @var LdapUserInterface $user */ 185 | $user = new $this->options['user'](); 186 | $user->setUsername($ldapObject->get('username')); 187 | $user->setLdapGuid($ldapObject->get('guid')); 188 | } catch (\Throwable $e) { 189 | throw new UnsupportedUserException(sprintf($errorMessage, $this->options['user'], $e->getMessage())); 190 | // Unlikely to help much in PHP 5.6, but oh well... 191 | } catch (\Exception $e) { 192 | throw new UnsupportedUserException(sprintf($errorMessage, $this->options['user'], $e->getMessage())); 193 | } 194 | // If the class also happens to extend the LdapTools LdapObject class, then set the attributes and type... 195 | if ($user instanceof LdapObject) { 196 | $this->hydrateLdapObjectUser($ldapObject, $user); 197 | } 198 | 199 | return $user; 200 | } 201 | 202 | /** 203 | * @param LdapObject $ldapObject 204 | * @param $user 205 | */ 206 | protected function hydrateLdapObjectUser(LdapObject $ldapObject, LdapObject $user) 207 | { 208 | $user->setBatchCollection(new BatchCollection($ldapObject->get('dn'))); 209 | $user->refresh($ldapObject->toArray()); 210 | 211 | // This is to avoid the constructor 212 | $refObject = new \ReflectionObject($user); 213 | $refProperty = $refObject->getProperty('type'); 214 | $refProperty->setAccessible(true); 215 | $refProperty->setValue($user, $this->options['ldap_object_type']); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /Security/Firewall/LdapFormLoginListener.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace LdapTools\Bundle\LdapToolsBundle\Security\Firewall; 12 | 13 | use Symfony\Component\HttpFoundation\ParameterBag; 14 | use Symfony\Component\Security\Http\ParameterBagUtils; 15 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 16 | use Symfony\Component\HttpFoundation\Request; 17 | use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; 18 | use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; 19 | use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; 20 | use Symfony\Component\Security\Core\Security; 21 | use Symfony\Component\Security\Csrf\CsrfToken; 22 | use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; 23 | use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; 24 | use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; 25 | use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; 26 | use Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener; 27 | use Symfony\Component\Security\Http\HttpUtils; 28 | use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; 29 | use Psr\Log\LoggerInterface; 30 | 31 | /** 32 | * The form login listener for LDAP authentication. 33 | * 34 | * @author Chad Sikorra 35 | */ 36 | class LdapFormLoginListener extends AbstractAuthenticationListener 37 | { 38 | /** 39 | * @var CsrfTokenManagerInterface|null 40 | */ 41 | private $csrfTokenManager; 42 | 43 | /** 44 | * @var array 45 | */ 46 | private $defaultListOpts = [ 47 | 'username_parameter' => '_username', 48 | 'password_parameter' => '_password', 49 | 'domain_parameter' => '_ldap_domain', 50 | 'csrf_parameter' => '_csrf_token', 51 | 'intention' => 'authenticate', 52 | 'post_only' => true, 53 | ]; 54 | 55 | /** 56 | * @param TokenStorageInterface $tokenStorage 57 | * @param AuthenticationManagerInterface $authenticationManager 58 | * @param SessionAuthenticationStrategyInterface $sessionStrategy 59 | * @param HttpUtils $httpUtils 60 | * @param string $providerKey 61 | * @param AuthenticationSuccessHandlerInterface $successHandler 62 | * @param AuthenticationFailureHandlerInterface $failureHandler 63 | * @param array $options 64 | * @param LoggerInterface $logger 65 | * @param EventDispatcherInterface $dispatcher 66 | * @param null|CsrfTokenManagerInterface $csrfTokenManager 67 | */ 68 | public function __construct( 69 | TokenStorageInterface $tokenStorage, 70 | AuthenticationManagerInterface $authenticationManager, 71 | SessionAuthenticationStrategyInterface $sessionStrategy, 72 | HttpUtils $httpUtils, 73 | $providerKey, 74 | AuthenticationSuccessHandlerInterface $successHandler, 75 | AuthenticationFailureHandlerInterface $failureHandler, 76 | array $options = [], 77 | LoggerInterface $logger = null, 78 | EventDispatcherInterface $dispatcher = null, 79 | CsrfTokenManagerInterface $csrfTokenManager = null 80 | ) { 81 | $this->csrfTokenManager = $csrfTokenManager; 82 | 83 | parent::__construct( 84 | $tokenStorage, 85 | $authenticationManager, 86 | $sessionStrategy, 87 | $httpUtils, 88 | $providerKey, 89 | $successHandler, 90 | $failureHandler, 91 | array_merge($this->defaultListOpts, $options), 92 | $logger, 93 | $dispatcher 94 | ); 95 | } 96 | 97 | /** 98 | * {@inheritdoc} 99 | */ 100 | protected function requiresAuthentication(Request $request) 101 | { 102 | if ($this->options['post_only'] && !$request->isMethod('POST')) { 103 | return false; 104 | } 105 | 106 | return parent::requiresAuthentication($request); 107 | } 108 | 109 | /** 110 | * {@inheritdoc} 111 | */ 112 | protected function attemptAuthentication(Request $request) 113 | { 114 | $this->validateCsrfToken($request); 115 | 116 | return $this->authenticationManager->authenticate($this->getUsernamePasswordToken($request)); 117 | } 118 | 119 | /** 120 | * Get the UsernamePasswordToken based off the Request parameters. 121 | * 122 | * @param Request $request 123 | * @return UsernamePasswordToken 124 | */ 125 | protected function getUsernamePasswordToken(Request $request) 126 | { 127 | if ($this->options['post_only']) { 128 | $username = trim($this->getParameterFromBag($request->request, $this->options['username_parameter'])); 129 | $password = $this->getParameterFromBag($request->request, $this->options['password_parameter']); 130 | } else { 131 | $username = trim($this->getParameterFromRequest($request, $this->options['username_parameter'])); 132 | $password = $this->getParameterFromRequest($request, $this->options['password_parameter']); 133 | } 134 | $request->getSession()->set(Security::LAST_USERNAME, $username); 135 | 136 | $token = new UsernamePasswordToken($username, $password, $this->providerKey); 137 | $this->addDomainToTokenIfPresent($request, $token); 138 | 139 | return $token; 140 | } 141 | 142 | /** 143 | * Add the domain name for the login request to the token if specified. 144 | * 145 | * @param Request $request 146 | * @param UsernamePasswordToken $token 147 | */ 148 | protected function addDomainToTokenIfPresent(Request $request, UsernamePasswordToken $token) 149 | { 150 | if ($this->options['post_only'] && $request->request->has($this->options['domain_parameter'])) { 151 | $token->setAttribute( 152 | 'ldap_domain', 153 | trim($this->getParameterFromBag($request->request, $this->options['domain_parameter'])) 154 | ); 155 | } elseif ($domain = trim($this->getParameterFromRequest($request, $this->options['domain_parameter']))) { 156 | $token->setAttribute('ldap_domain', $domain); 157 | } 158 | } 159 | 160 | /** 161 | * Provide a BC wrapper for CSRF token manager/provider compatibility between versions. 162 | * 163 | * @param Request $request 164 | */ 165 | protected function validateCsrfToken(Request $request) 166 | { 167 | if (is_null($this->csrfTokenManager)) { 168 | return; 169 | } 170 | $csrfToken = $this->getParameterFromRequest($request, $this->options['csrf_parameter']); 171 | 172 | if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken($this->options['intention'], $csrfToken))) { 173 | throw new InvalidCsrfTokenException('Invalid CSRF token.'); 174 | } 175 | } 176 | 177 | /** 178 | * Provide a BC wrapper for deep item finding deprecation. 179 | * 180 | * @param Request $request 181 | * @param string $param 182 | * @return mixed 183 | */ 184 | protected function getParameterFromRequest(Request $request, $param) 185 | { 186 | if (!$this->useParameterBagUtils()) { 187 | return $request->get($param, null, true); 188 | } 189 | 190 | return ParameterBagUtils::getRequestParameterValue($request, $param); 191 | } 192 | 193 | /** 194 | * Provide a BC wrapper for deep item finding deprecation. 195 | * 196 | * @param ParameterBag $bag 197 | * @param string $param 198 | * @return mixed 199 | */ 200 | protected function getParameterFromBag($bag, $param) 201 | { 202 | if (!$this->useParameterBagUtils()) { 203 | return $bag->get($param, null, true); 204 | } 205 | 206 | return ParameterBagUtils::getParameterBagValue($bag, $param); 207 | } 208 | 209 | /** 210 | * Whether or not the ParameterBagUtils class exists (2.8 and above...) 211 | * 212 | * @return bool 213 | */ 214 | protected function useParameterBagUtils() 215 | { 216 | return class_exists('Symfony\Component\Security\Http\ParameterBagUtils'); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /Resources/doc/Save-LDAP-Users-to-the-Database-After-Login.md: -------------------------------------------------------------------------------- 1 | Save LDAP Users to the Database After Login 2 | ========== 3 | 4 | Often times you may want to save your LDAP user to a database after they login. This allows more flexibility than using 5 | just the standard LdapUser of this bundle on login. To do this there are a few steps you have to follow. These steps are 6 | designed around a standard Doctrine entity: 7 | 8 | * [Create your Database User](#create-your-database-user) 9 | * [Create your User Provider Chain for LDAP and Doctrine](#create-your-user-provider-chain) 10 | * [Configure your Firewall](#configure-your-firewall-for-the-guard) 11 | * [Create a LDAP Login Success Event](#create-an-event-to-save-your-user-to-the-db-on-login) 12 | 13 | ## Create your Database User 14 | 15 | Your database user should be a typical Doctrine entity. However, it will need to implement an interface: 16 | 17 | * `LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUserInterface` 18 | 19 | Your basic app user will look something like: 20 | 21 | ```php 22 | # src/AppBundle/Entity/AppUser.php 23 | 24 | namespace AppBundle\Entity; 25 | 26 | use Doctrine\ORM\Mapping as ORM; 27 | use LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUserInterface; 28 | use Symfony\Component\Security\Core\User\UserInterface; 29 | 30 | /** 31 | * @ORM\Entity 32 | * @ORM\Table(name="app_user") 33 | */ 34 | class AppUser implements LdapUserInterface, UserInterface 35 | { 36 | /** 37 | * @ORM\Column(type="integer") 38 | * @ORM\Id 39 | * @ORM\GeneratedValue(strategy="AUTO") 40 | */ 41 | private $id; 42 | 43 | /** 44 | * @ORM\Column(type="string", length=100) 45 | */ 46 | private $ldapGuid; 47 | 48 | /** 49 | * @ORM\Column(type="text") 50 | */ 51 | private $username; 52 | 53 | /** 54 | * @var array 55 | * @ORM\Column(type="array") 56 | */ 57 | private $roles = []; 58 | 59 | /** 60 | * Get id 61 | * 62 | * @return integer 63 | */ 64 | public function getId() 65 | { 66 | return $this->id; 67 | } 68 | 69 | /** 70 | * Set ldapGuid 71 | * 72 | * @param string $ldapGuid 73 | * 74 | * @return AppUser 75 | */ 76 | public function setLdapGuid($ldapGuid) 77 | { 78 | $this->ldapGuid = $ldapGuid; 79 | 80 | return $this; 81 | } 82 | 83 | /** 84 | * Get ldapGuid 85 | * 86 | * @return string 87 | */ 88 | public function getLdapGuid() 89 | { 90 | return $this->ldapGuid; 91 | } 92 | 93 | /** 94 | * Set username 95 | * 96 | * @param string $username 97 | * 98 | * @return AppUser 99 | */ 100 | public function setUsername($username) 101 | { 102 | $this->username = $username; 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * Get username 109 | * 110 | * @return string 111 | */ 112 | public function getUsername() 113 | { 114 | return $this->username; 115 | } 116 | 117 | public function eraseCredentials() 118 | { 119 | } 120 | 121 | /** 122 | * @param array $roles 123 | * @return $this 124 | */ 125 | public function setRoles(array $roles) 126 | { 127 | $this->roles = $roles; 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * @return array 134 | */ 135 | public function getRoles() 136 | { 137 | return $this->roles; 138 | } 139 | 140 | /** 141 | * @return null 142 | */ 143 | public function getPassword() 144 | { 145 | return null; 146 | } 147 | 148 | /** 149 | * @return null 150 | */ 151 | public function getSalt() 152 | { 153 | return null; 154 | } 155 | } 156 | ``` 157 | 158 | Set the encoder for the user to just simple plain text: 159 | 160 | ```yaml 161 | # app/config/security.yml 162 | 163 | encoders: 164 | AppBundle\Entity\AppUser: plaintext 165 | ``` 166 | 167 | Note that in this example the password is not stored on the database user. 168 | 169 | Now tell the LdapToolsBundle that it should use this class in its User Provider: 170 | 171 | ```yaml 172 | # app/config/config.yml 173 | 174 | ldap_tools: 175 | security: 176 | user: AppBundle\Entity\AppUser 177 | ``` 178 | 179 | ## Create your User Provider Chain 180 | 181 | You must now chain your user providers (Both LDAP and the Doctrine entity user providers). This way it will try to load 182 | a user from the database first and fallback to LDAP if they are not found in the database. Your chain would look like: 183 | 184 | ```yaml 185 | # app/config/security.yml 186 | providers: 187 | chain_provider: 188 | chain: 189 | providers: [user_db, ldap] 190 | ldap: 191 | id: ldap_tools.security.user.ldap_user_provider 192 | user_db: 193 | entity: { class: AppBundle\Entity\AppUser, property: username } 194 | ``` 195 | 196 | ## Configure your Firewall for the Guard 197 | 198 | You'll need to tell your firewall to use the the LDAP Guard for authentication (as well as the new chained provider above). 199 | Your firewall section should look similar to this: 200 | 201 | ```yaml 202 | # app/config/security.yml 203 | firewalls: 204 | # disables authentication for assets and the profiler, adapt it according to your needs 205 | dev: 206 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 207 | security: false 208 | 209 | main: 210 | anonymous: ~ 211 | # Here we use the chained provider defined previously... 212 | provider: chain_provider 213 | pattern: ^/ 214 | logout: ~ 215 | # Here we tell it to use the LDAP Guard for authentication... 216 | guard: 217 | authenticators: 218 | - ldap_tools.security.ldap_guard_authenticator 219 | 220 | login: 221 | pattern: ^/login$ 222 | anonymous: ~ 223 | ``` 224 | 225 | Also make sure to set your access control properly. It should look something like this: 226 | 227 | ```yaml 228 | # app/config/security.yml 229 | 230 | access_control: 231 | - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } 232 | - { path: ^/, roles: ROLE_USER } 233 | ``` 234 | 235 | ## Create an Event to Save your User to the DB on Login 236 | 237 | To save your user to the database after login (only if they haven't been already) we will hook into an event. So first 238 | create your event listener class: 239 | 240 | ```php 241 | # src/AppBundle/Event/LoginEventListener 242 | 243 | namespace AppBundle\Event; 244 | 245 | use AppBundle\Entity\AppUser; 246 | use Doctrine\ORM\EntityManagerInterface; 247 | use LdapTools\Bundle\LdapToolsBundle\Event\LdapLoginEvent; 248 | 249 | class LoginEventListener 250 | { 251 | /** 252 | * @var EntityManagerInterface 253 | */ 254 | protected $em; 255 | 256 | /** 257 | * @param EntityManagerInterface $em 258 | */ 259 | public function __construct(EntityManagerInterface $em) 260 | { 261 | $this->em = $em; 262 | } 263 | 264 | /** 265 | * @param LdapLoginEvent $event 266 | */ 267 | public function onLoginSuccess(LdapLoginEvent $event) 268 | { 269 | /** @var AppUser $user */ 270 | $user = $event->getUser(); 271 | 272 | // If the ID doesn't exist, then it hasn't been saved to the database. So save it.. 273 | if (!$user->getId()) { 274 | $this->em->persist($user); 275 | $this->em->flush(); 276 | } 277 | 278 | // The credentials on login are also available if needed... 279 | $password = $event->getToken()->getCredentials(); 280 | 281 | // ... 282 | } 283 | } 284 | ``` 285 | 286 | Register your created listener as a service and tag it with the `ldap_tools_bundle.login.success` event using the kernel 287 | event listener: 288 | 289 | ```yaml 290 | # src/AppBundle/Resource/config/services.yml 291 | 292 | services: 293 | app_bundle.event.login_listener: 294 | class: AppBundle\Event\LoginEventListener 295 | arguments: ['@doctrine.orm.entity_manager'] 296 | tags: 297 | - { name: kernel.event_listener, event: ldap_tools_bundle.login.success, method: onLoginSuccess } 298 | ``` 299 | 300 | Now every time someone logs in it will first attempt to load the user from the database. If they do not exist it will 301 | search LDAP for the user. Then the user will get passed to the LDAP Guard authenticator where the credentials are validated 302 | against LDAP. If they login successfully, and the user has not yet been saved to the database, then the user will also be 303 | saved back to the database. 304 | -------------------------------------------------------------------------------- /spec/LdapTools/Bundle/LdapToolsBundle/Security/User/LdapUserProviderSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace spec\LdapTools\Bundle\LdapToolsBundle\Security\User; 12 | 13 | use LdapTools\Bundle\LdapToolsBundle\Event\LoadUserEvent; 14 | use LdapTools\Bundle\LdapToolsBundle\Security\User\LdapRoleMapper; 15 | use LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUser; 16 | use LdapTools\Connection\LdapConnectionInterface; 17 | use LdapTools\DomainConfiguration; 18 | use LdapTools\Exception\EmptyResultException; 19 | use LdapTools\Exception\MultiResultException; 20 | use LdapTools\LdapManager; 21 | use LdapTools\Object\LdapObject; 22 | use LdapTools\Object\LdapObjectType; 23 | use LdapTools\Query\LdapQuery; 24 | use LdapTools\Query\LdapQueryBuilder; 25 | use PhpSpec\ObjectBehavior; 26 | use Prophecy\Argument; 27 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 28 | use Symfony\Component\Security\Core\User\User; 29 | 30 | class LdapUserProviderSpec extends ObjectBehavior 31 | { 32 | protected $attr = [ 33 | 'username' => 'foo', 34 | 'guid' => '26dc475e-aca2-4b45-b3ad-5a2c73d4f8c5', 35 | 'locked' => false, 36 | 'accountExpirationDate' => false, 37 | 'enabled' => true, 38 | 'passwordMustChange' => false, 39 | 'groups' => ['foo', 'bar'], 40 | 'dn' => 'cn=foo,dc=foo,dc=bar', 41 | ]; 42 | 43 | function let(LdapManager $ldap, LdapQueryBuilder $qb, LdapQuery $query, LdapConnectionInterface $connection, EventDispatcherInterface $dispatcher, LdapRoleMapper $roleMapper) 44 | { 45 | $config = new DomainConfiguration('foo.bar'); 46 | 47 | $ldapObject = new LdapObject($this->attr, 'user'); 48 | $query->getSingleResult()->willReturn($ldapObject); 49 | $query->getArrayResult()->willReturn([ 50 | ['name' => 'foo'], 51 | ['name' => 'bar'], 52 | ]); 53 | 54 | $qb->from(LdapObjectType::USER)->willReturn($qb); 55 | $qb->select(["username", "guid", "accountExpirationDate", "enabled", "groups", "locked", "passwordMustChange"])->willReturn($qb); 56 | $qb->select('name')->willReturn($qb); 57 | $qb->where(['username' => 'foo'])->willReturn($qb); 58 | $qb->getLdapQuery()->willReturn($query); 59 | $ldap->buildLdapQuery()->willReturn($qb); 60 | $ldap->getDomainContext()->willReturn('foo.bar'); 61 | 62 | $connection->getConfig()->willReturn($config); 63 | $ldap->getConnection()->willReturn($connection); 64 | 65 | $this->beConstructedWith($ldap, $dispatcher, $roleMapper, []); 66 | } 67 | 68 | function it_is_initializable() 69 | { 70 | $this->shouldHaveType('LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUserProvider'); 71 | } 72 | 73 | function it_should_load_by_username($dispatcher) 74 | { 75 | $dispatcher->dispatch(LoadUserEvent::BEFORE, Argument::type('\LdapTools\Bundle\LdapToolsBundle\Event\LoadUserEvent'))->shouldBeCalledTimes(1); 76 | $dispatcher->dispatch(LoadUserEvent::AFTER, Argument::type('\LdapTools\Bundle\LdapToolsBundle\Event\LoadUserEvent'))->shouldBeCalledTimes(1); 77 | 78 | $this->loadUserByUsername('foo')->shouldBeAnInstanceOf('\LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUser'); 79 | } 80 | 81 | function it_should_set_the_ldap_user_class($ldap, $dispatcher, $roleMapper) 82 | { 83 | $this->beConstructedWith($ldap, $dispatcher, $roleMapper, ['user' => '\Foo']); 84 | 85 | $this->shouldThrow('Symfony\Component\Security\Core\Exception\UnsupportedUserException')->duringLoadUserByUsername('foo'); 86 | } 87 | 88 | function it_should_set_additional_attributes_to_select($ldap, $dispatcher, $roleMapper, $qb) 89 | { 90 | $this->beConstructedWith($ldap, $dispatcher, $roleMapper, ['additional_attributes' => ['foo']]); 91 | 92 | $qb->select(["username", "guid", "accountExpirationDate", "enabled", "groups", "locked", "passwordMustChange", "foo"]) 93 | ->shouldBeCalled()->willReturn($qb); 94 | 95 | $this->loadUserByUsername('foo')->shouldBeAnInstanceOf('\LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUser'); 96 | } 97 | 98 | function it_should_refresh_a_user_by_their_guid($qb, LdapUser $user, $ldap, $dispatcher, $roleMapper) 99 | { 100 | $this->beConstructedWith($ldap, $dispatcher, $roleMapper, ['refresh_user_attributes' => true]); 101 | 102 | $user->getRoles()->willReturn(['ROLE_USER']); 103 | $user->setRoles(['ROLE_USER'])->willReturn($user); 104 | $user->getLdapGuid()->shouldBeCalled()->willReturn($this->attr['guid']); 105 | $qb->where(['guid' => $this->attr['guid']])->shouldBeCalled()->willReturn($qb); 106 | 107 | $this->refreshUser($user)->shouldBeAnInstanceOf('\LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUser'); 108 | } 109 | 110 | function it_should_not_refresh_a_user_it_cannot_support() 111 | { 112 | $this->shouldThrow('\Symfony\Component\Security\Core\Exception\UnsupportedUserException') 113 | ->duringRefreshUser(new User('foo', 'bar')); 114 | } 115 | 116 | function it_should_support_classes_that_extend_LdapUser() 117 | { 118 | $this->supportsClass('\LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUser')->shouldBeEqualTo(true); 119 | $this->supportsClass('\Symfony\Component\Security\Core\User\User')->shouldBeEqualTo(false); 120 | } 121 | 122 | function it_should_throw_a_user_not_found_exception_if_no_user_is_returned_from_ldap($query) 123 | { 124 | $query->getSingleResult()->willThrow(new EmptyResultException()); 125 | 126 | $this->shouldThrow('\Symfony\Component\Security\Core\Exception\UsernameNotFoundException') 127 | ->duringLoadUserByUsername('foo'); 128 | } 129 | 130 | function it_should_throw_a_user_not_found_exception_if_too_many_results_are_returned_from_ldap($query) 131 | { 132 | $query->getSingleResult()->willThrow(new MultiResultException()); 133 | 134 | $this->shouldThrow('\Symfony\Component\Security\Core\Exception\UsernameNotFoundException') 135 | ->duringLoadUserByUsername('foo'); 136 | } 137 | 138 | function it_should_be_able_to_set_the_ldap_object_type_to_use_for_the_search($ldap, $dispatcher, $roleMapper, $qb) 139 | { 140 | $this->beConstructedWith($ldap, $dispatcher, $roleMapper, ['ldap_object_type' => 'foobar']); 141 | 142 | $qb->from('foobar')->shouldBeCalled()->willReturn($qb); 143 | 144 | $this->loadUserByUsername('foo'); 145 | } 146 | 147 | function it_should_be_able_to_set_the_ldap_search_base_when_searching_for_the_user($ldap, $dispatcher, $roleMapper, $qb) 148 | { 149 | $this->beConstructedWith($ldap, $dispatcher, $roleMapper, ['search_base' => 'ou=employees,dc=foo,dc=bar']); 150 | 151 | $qb->setBaseDn('ou=employees,dc=foo,dc=bar')->shouldBeCalled()->willReturn($qb); 152 | 153 | $this->loadUserByUsername('foo'); 154 | } 155 | 156 | function it_should_not_query_ldap_on_a_refresh_if_refresh_attributes_and_roles_is_false($connection, LdapUser $user, $roleMapper, $ldap, $dispatcher) 157 | { 158 | $this->beConstructedWith($ldap, $dispatcher, $roleMapper, ['refresh_user_roles' => false, 'refresh_user_attributes' => false]); 159 | 160 | $user->getRoles()->willReturn([]); 161 | $user->setRoles([])->shouldBeCalled(); 162 | $connection->execute(Argument::any())->shouldNotBeCalled(); 163 | $roleMapper->setRoles(Argument::any())->shouldNotBeCalled(); 164 | 165 | $this->refreshUser($user)->shouldBeEqualTo($user); 166 | } 167 | 168 | function it_should_refresh_attributes_but_not_roles_if_specified($query, LdapUser $user, $qb, $roleMapper, $ldap, $dispatcher) 169 | { 170 | $this->beConstructedWith($ldap, $dispatcher, $roleMapper, ['refresh_user_roles' => false, 'refresh_user_attributes' => true]); 171 | 172 | $query->getSingleResult()->shouldBeCalled(); 173 | $roleMapper->setRoles(Argument::any())->shouldNotBeCalled(); 174 | 175 | $user->getLdapGuid()->shouldBeCalled()->willReturn($this->attr['guid']); 176 | $qb->where(['guid' => $this->attr['guid']])->shouldBeCalled()->willReturn($qb); 177 | $user->getRoles()->willReturn(['ROLE_USER']); 178 | 179 | $this->refreshUser($user)->toArray()->shouldBeEqualTo($this->attr); 180 | $this->refreshUser($user)->getRoles()->shouldBeEqualTo(['ROLE_USER']); 181 | } 182 | 183 | function it_should_refresh_roles_but_not_attributes_if_specified($query, LdapUser $user, $roleMapper, $ldap, $dispatcher) 184 | { 185 | $this->beConstructedWith($ldap, $dispatcher, $roleMapper, ['refresh_user_roles' => true, 'refresh_user_attributes' => false]); 186 | 187 | $user->getRoles()->willReturn(['ROLE_USER']); 188 | $query->getSingleResult()->shouldNotBeCalled(); 189 | $roleMapper->setRoles($user)->shouldBeCalled(); 190 | 191 | $this->refreshUser($user); 192 | } 193 | 194 | function it_should_get_a_ldap_user_object_from_a_specific_attribute_and_value() 195 | { 196 | $this->getLdapUser('username', 'foo')->shouldBeAnInstanceOf('LdapTools\Object\LdapObject'); 197 | } 198 | } 199 | --------------------------------------------------------------------------------