From 1f1a8e18ef0a171d83c0b86ea0c81dfb05fe87b0 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Isma=C3=ABl=20Bouya?= Date: Sat, 16 Jun 2018 11:40:00 +0200 Subject: [PATCH] Add ldap --- .travis.yml | 1 + app/AppKernel.php | 4 + .../Version20170710113900.php | 54 +++++++++ app/config/parameters.yml.dist | 20 ++++ app/config/security.yml | 2 + composer.json | 3 + scripts/install.sh | 3 + scripts/update.sh | 3 + .../WallabagUserExtension.php | 30 ++++- src/Wallabag/UserBundle/Entity/User.php | 49 ++++++++- src/Wallabag/UserBundle/LdapHydrator.php | 103 ++++++++++++++++++ .../UserBundle/OAuthStorageLdapWrapper.php | 43 ++++++++ .../UserBundle/Resources/config/ldap.yml | 28 +++++ .../Resources/config/ldap_services.yml | 22 ++++ 14 files changed, 363 insertions(+), 2 deletions(-) create mode 100644 app/DoctrineMigrations/Version20170710113900.php create mode 100644 src/Wallabag/UserBundle/LdapHydrator.php create mode 100644 src/Wallabag/UserBundle/OAuthStorageLdapWrapper.php create mode 100644 src/Wallabag/UserBundle/Resources/config/ldap.yml create mode 100644 src/Wallabag/UserBundle/Resources/config/ldap_services.yml diff --git a/.travis.yml b/.travis.yml index eb409533..873ffe12 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,6 +53,7 @@ branches: before_script: - PHP=$TRAVIS_PHP_VERSION + - echo "extension=ldap.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini - if [[ ! $PHP = hhvm* ]]; then echo "memory_limit=-1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini; fi; # xdebug isn't enable for PHP 7.1 - if [[ ! $PHP = hhvm* ]]; then phpenv config-rm xdebug.ini || echo "xdebug not available"; fi diff --git a/app/AppKernel.php b/app/AppKernel.php index 546794de..b7f34127 100644 --- a/app/AppKernel.php +++ b/app/AppKernel.php @@ -42,6 +42,10 @@ class AppKernel extends Kernel new Wallabag\AnnotationBundle\WallabagAnnotationBundle(), ]; + if (class_exists('FR3D\\LdapBundle\\FR3DLdapBundle')) { + $bundles[] = new FR3D\LdapBundle\FR3DLdapBundle(); + } + if (in_array($this->getEnvironment(), ['dev', 'test'], true)) { $bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle(); $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); diff --git a/app/DoctrineMigrations/Version20170710113900.php b/app/DoctrineMigrations/Version20170710113900.php new file mode 100644 index 00000000..7be83110 --- /dev/null +++ b/app/DoctrineMigrations/Version20170710113900.php @@ -0,0 +1,54 @@ +container = $container; + } + + private function getTable($tableName) + { + return $this->container->getParameter('database_table_prefix').$tableName; + } + + /** + * @param Schema $schema + */ + public function up(Schema $schema) + { + $usersTable = $schema->getTable($this->getTable('user')); + + $this->skipIf($usersTable->hasColumn('dn'), 'It seems that you already played this migration.'); + + $usersTable->addColumn('dn', 'text', [ + 'default' => null, + 'notnull' => false, + ]); + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + $usersTable = $schema->getTable($this->getTable('user')); + $usersTable->dropColumn('dn'); + } +} + diff --git a/app/config/parameters.yml.dist b/app/config/parameters.yml.dist index d21f20e0..c8abd190 100644 --- a/app/config/parameters.yml.dist +++ b/app/config/parameters.yml.dist @@ -65,3 +65,23 @@ parameters: redis_port: 6379 redis_path: null redis_password: null + + # ldap configuration + # To enable, you need to require fr3d/ldap-bundle + ldap_enabled: false + ldap_host: localhost + ldap_port: 389 + ldap_tls: false + ldap_ssl: false + ldap_bind_requires_dn: true + ldap_base: dc=example,dc=com + ldap_manager_dn: ou=Manager,dc=example,dc=com + ldap_manager_pw: password + ldap_filter: (&(ObjectClass=Person)) + # optional (if null: no ldap user is admin) + ldap_admin_filter: (&(memberOf=ou=admins,dc=example,dc=com)(uid=%s)) + ldap_username_attribute: uid + ldap_email_attribute: mail + ldap_name_attribute: cn + # optional (default sets user as enabled unconditionally) + ldap_enabled_attribute: ~ diff --git a/app/config/security.yml b/app/config/security.yml index 0318fce1..56e6a813 100644 --- a/app/config/security.yml +++ b/app/config/security.yml @@ -6,6 +6,7 @@ security: ROLE_ADMIN: ROLE_USER ROLE_SUPER_ADMIN: [ ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH ] + # /!\ This list is modified in WallabagUserBundle when LDAP is enabled providers: administrators: entity: @@ -38,6 +39,7 @@ security: pattern: ^/login$ anonymous: ~ + # /!\ This section is modified in WallabagUserBundle when LDAP is enabled secured_area: logout_on_user_change: true pattern: ^/ diff --git a/composer.json b/composer.json index f209a5c2..c44a78bd 100644 --- a/composer.json +++ b/composer.json @@ -87,6 +87,9 @@ "defuse/php-encryption": "^2.1", "html2text/html2text": "^4.1" }, + "suggest": { + "fr3d/ldap-bundle": "If you want to authenticate via LDAP" + }, "require-dev": { "doctrine/doctrine-fixtures-bundle": "~3.0", "sensio/generator-bundle": "^3.0", diff --git a/scripts/install.sh b/scripts/install.sh index 62a46f4f..5ea3933c 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -12,5 +12,8 @@ ENV=$1 TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) git checkout $TAG +if [ -n "$LDAP_ENABLED" ]; then + SYMFONY_ENV=$ENV $COMPOSER_COMMAND require --no-update fr3d/ldap-bundle +fi SYMFONY_ENV=$ENV $COMPOSER_COMMAND install --no-dev -o --prefer-dist php bin/console wallabag:install --env=$ENV diff --git a/scripts/update.sh b/scripts/update.sh index d0598135..753ccbc3 100644 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -18,6 +18,9 @@ git fetch origin git fetch --tags TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) git checkout $TAG --force +if [ -n "$LDAP_ENABLED" ]; then + SYMFONY_ENV=$ENV $COMPOSER_COMMAND require --no-update fr3d/ldap-bundle +fi SYMFONY_ENV=$ENV $COMPOSER_COMMAND install --no-dev -o --prefer-dist php bin/console doctrine:migrations:migrate --no-interaction --env=$ENV php bin/console cache:clear --env=$ENV diff --git a/src/Wallabag/UserBundle/DependencyInjection/WallabagUserExtension.php b/src/Wallabag/UserBundle/DependencyInjection/WallabagUserExtension.php index 5ca3482e..904a6af1 100644 --- a/src/Wallabag/UserBundle/DependencyInjection/WallabagUserExtension.php +++ b/src/Wallabag/UserBundle/DependencyInjection/WallabagUserExtension.php @@ -6,9 +6,34 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader; use Symfony\Component\HttpKernel\DependencyInjection\Extension; +use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; -class WallabagUserExtension extends Extension +class WallabagUserExtension extends Extension implements PrependExtensionInterface { + public function prepend(ContainerBuilder $container) + { + $ldap = $container->getParameter('ldap_enabled'); + + if ($ldap) { + $container->prependExtensionConfig('security', array( + 'providers' => array( + 'chain_provider' => array(), + ), + )); + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('ldap.yml'); + } elseif ($container->hasExtension('fr3d_ldap')) { + $container->prependExtensionConfig('fr3_d_ldap', array( + 'driver' => array( + 'host' => 'localhost', + ), + 'user' => array( + 'baseDn' => 'dc=example,dc=com', + ), + )); + } + } + public function load(array $configs, ContainerBuilder $container) { $configuration = new Configuration(); @@ -16,6 +41,9 @@ class WallabagUserExtension extends Extension $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $loader->load('services.yml'); + if ($container->getParameter('ldap_enabled')) { + $loader->load('ldap_services.yml'); + } $container->setParameter('wallabag_user.registration_enabled', $config['registration_enabled']); } diff --git a/src/Wallabag/UserBundle/Entity/User.php b/src/Wallabag/UserBundle/Entity/User.php index 48446e3c..f93c59c7 100644 --- a/src/Wallabag/UserBundle/Entity/User.php +++ b/src/Wallabag/UserBundle/Entity/User.php @@ -1,5 +1,15 @@ clients->first(); } } + + /** + * Set dn. + * + * @param string $dn + * + * @return User + */ + public function setDn($dn) + { + $this->dn = $dn; + + return $this; + } + + /** + * Get dn. + * + * @return string + */ + public function getDn() + { + return $this->dn; + } + + public function isLdapUser() + { + return $this->dn !== null; + } } diff --git a/src/Wallabag/UserBundle/LdapHydrator.php b/src/Wallabag/UserBundle/LdapHydrator.php new file mode 100644 index 00000000..cea2450f --- /dev/null +++ b/src/Wallabag/UserBundle/LdapHydrator.php @@ -0,0 +1,103 @@ +userManager = $user_manager; + $this->eventDispatcher = $event_dispatcher; + + $this->attributesMap = array( + 'setUsername' => $attributes_map[0], + 'setEmail' => $attributes_map[1], + 'setName' => $attributes_map[2], + ); + $this->enabledAttribute = $attributes_map[3]; + + $this->ldapBaseDn = $ldap_base_dn; + $this->ldapAdminFilter = $ldap_admin_filter; + $this->ldapDriver = $ldap_driver; + } + + public function hydrate(array $ldapEntry) + { + $user = $this->userManager->findUserBy(array('dn' => $ldapEntry['dn'])); + + if (!$user) { + $user = $this->userManager->createUser(); + $user->setDn($ldapEntry['dn']); + $user->setPassword(''); + $user->setSalt(''); + $this->updateUserFields($user, $ldapEntry); + + $event = new UserEvent($user); + $this->eventDispatcher->dispatch(FOSUserEvents::USER_CREATED, $event); + + $this->userManager->reloadUser($user); + } else { + $this->updateUserFields($user, $ldapEntry); + } + + return $user; + } + + private function updateUserFields($user, $ldapEntry) + { + foreach ($this->attributesMap as $key => $value) { + if (is_array($ldapEntry[$value])) { + $ldap_value = $ldapEntry[$value][0]; + } else { + $ldap_value = $ldapEntry[$value]; + } + + call_user_func([$user, $key], $ldap_value); + } + + if ($this->enabledAttribute !== null) { + $user->setEnabled($ldapEntry[$this->enabledAttribute]); + } else { + $user->setEnabled(true); + } + + if ($this->isAdmin($user)) { + $user->addRole('ROLE_SUPER_ADMIN'); + } else { + $user->removeRole('ROLE_SUPER_ADMIN'); + } + + $this->userManager->updateUser($user, true); + } + + private function isAdmin($user) + { + if ($this->ldapAdminFilter === null) { + return false; + } + + $escaped_username = ldap_escape($user->getUsername(), '', LDAP_ESCAPE_FILTER); + $filter = sprintf($this->ldapAdminFilter, $escaped_username); + $entries = $this->ldapDriver->search($this->ldapBaseDn, $filter); + + return $entries['count'] == 1; + } +} diff --git a/src/Wallabag/UserBundle/OAuthStorageLdapWrapper.php b/src/Wallabag/UserBundle/OAuthStorageLdapWrapper.php new file mode 100644 index 00000000..8a851f12 --- /dev/null +++ b/src/Wallabag/UserBundle/OAuthStorageLdapWrapper.php @@ -0,0 +1,43 @@ +ldapManager = $ldap_manager; + } + + public function checkUserCredentials(IOAuth2Client $client, $username, $password) + { + try { + $user = $this->userProvider->loadUserByUsername($username); + } catch (AuthenticationException $e) { + return false; + } + + if ($user->isLdapUser()) { + return $this->checkLdapUserCredentials($user, $password); + } else { + return parent::checkUserCredentials($client, $username, $password); + } + } + + private function checkLdapUserCredentials($user, $password) + { + if ($this->ldapManager->bind($user, $password)) { + return array( + 'data' => $user, + ); + } else { + return false; + } + } +} diff --git a/src/Wallabag/UserBundle/Resources/config/ldap.yml b/src/Wallabag/UserBundle/Resources/config/ldap.yml new file mode 100644 index 00000000..5ec16088 --- /dev/null +++ b/src/Wallabag/UserBundle/Resources/config/ldap.yml @@ -0,0 +1,28 @@ +fr3d_ldap: + service: + user_hydrator: ldap_user_hydrator + driver: + host: "%ldap_host%" + port: "%ldap_port%" + useSsl: "%ldap_ssl%" + useStartTls: "%ldap_tls%" + bindRequiresDn: "%ldap_bind_requires_dn%" + username: "%ldap_manager_dn%" + password: "%ldap_manager_pw%" + user: + baseDn: "%ldap_base%" + filter: "%ldap_filter%" + usernameAttribute: "%ldap_username_attribute%" +security: + providers: + chain_provider: + chain: + providers: [ fr3d_ldapbundle, fos_userbundle ] + fr3d_ldapbundle: + id: fr3d_ldap.security.user.provider + firewalls: + secured_area: + fr3d_ldap: ~ + form_login: + provider: chain_provider + diff --git a/src/Wallabag/UserBundle/Resources/config/ldap_services.yml b/src/Wallabag/UserBundle/Resources/config/ldap_services.yml new file mode 100644 index 00000000..b3e3fd8a --- /dev/null +++ b/src/Wallabag/UserBundle/Resources/config/ldap_services.yml @@ -0,0 +1,22 @@ +services: + fos_oauth_server.server: + class: OAuth2\OAuth2 + arguments: + - "@oauth_storage_ldap_wrapper" + - "%fos_oauth_server.server.options%" + oauth_storage_ldap_wrapper: + class: Wallabag\UserBundle\OAuthStorageLdapWrapper + parent: fos_oauth_server.storage + calls: + - [setLdapManager, ["@fr3d_ldap.ldap_manager"]] + + ldap_user_hydrator: + class: Wallabag\UserBundle\LdapHydrator + arguments: + - "@fos_user.user_manager" + - "@event_dispatcher" + - [ "%ldap_username_attribute%", "%ldap_email_attribute%", "%ldap_name_attribute%", "%ldap_enabled_attribute%" ] + - "%ldap_base%" + - "%ldap_admin_filter%" + - "@fr3d_ldap.ldap_driver" + -- 2.41.0