]> git.immae.eu Git - github/wallabag/wallabag.git/commitdiff
Add ldap
authorIsmaël Bouya <ismael.bouya@normalesup.org>
Sat, 16 Jun 2018 09:40:00 +0000 (11:40 +0200)
committerIsmaël Bouya <ismael.bouya@normalesup.org>
Wed, 23 Jan 2019 18:57:28 +0000 (19:57 +0100)
14 files changed:
.travis.yml
app/AppKernel.php
app/DoctrineMigrations/Version20170710113900.php [new file with mode: 0644]
app/config/parameters.yml.dist
app/config/security.yml
composer.json
scripts/install.sh
scripts/update.sh
src/Wallabag/UserBundle/DependencyInjection/WallabagUserExtension.php
src/Wallabag/UserBundle/Entity/User.php
src/Wallabag/UserBundle/LdapHydrator.php [new file with mode: 0644]
src/Wallabag/UserBundle/OAuthStorageLdapWrapper.php [new file with mode: 0644]
src/Wallabag/UserBundle/Resources/config/ldap.yml [new file with mode: 0644]
src/Wallabag/UserBundle/Resources/config/ldap_services.yml [new file with mode: 0644]

index 04cea25816f022c857ac6c2f0f8f5ed7d50358ce..56b1f576d5ef4511fdc7fed189212241f46fbe64 100644 (file)
@@ -58,6 +58,7 @@ install:
 
 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
index 40726f0549cc9c756e08a1a00f2cf989859a43a0..c4f465dc6bf714221ef7511af26971c4f6f48925 100644 (file)
@@ -42,6 +42,10 @@ class AppKernel extends Kernel
             new OldSound\RabbitMqBundle\OldSoundRabbitMqBundle(),
         ];
 
+        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 (file)
index 0000000..7be8311
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+
+namespace Application\Migrations;
+
+use Doctrine\DBAL\Migrations\AbstractMigration;
+use Doctrine\DBAL\Schema\Schema;
+use Symfony\Component\DependencyInjection\ContainerAwareInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Added dn field on wallabag_users
+ */
+class Version20170710113900 extends AbstractMigration implements ContainerAwareInterface
+{
+    /**
+     * @var ContainerInterface
+     */
+    private $container;
+
+    public function setContainer(ContainerInterface $container = null)
+    {
+        $this->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');
+    }
+}
+
index 6b0cb8e84c9c5aa23d4cbd1035e8cfce8bbf07b8..cfd41b6947f660cb23be5f6ee6686b82a75633a7 100644 (file)
@@ -62,3 +62,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: ~
index 02afc9eaaa1f96d31b23e00f3cff61bc2b38cbf3..48fbb553caef9e0fa99a54bcae373fd73affdfd4 100644 (file)
@@ -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:
@@ -36,6 +37,7 @@ security:
             pattern: ^/login$
             anonymous:  ~
 
+        # /!\ This section is modified in WallabagUserBundle when LDAP is enabled
         secured_area:
             pattern: ^/
             form_login:
index 68cfad05d7c695c989053d3895d0a6be3f7d7b17..775a1ebd06032352a5760227c07f9a4681eea7d9 100644 (file)
@@ -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": "~2.2",
         "doctrine/data-fixtures": "~1.1",
index 8b7ea03f5fe77030abafbde6d852e5d425da0384..3a4a33abeb4f03c9f9aa0926470e4bd231bae084 100755 (executable)
@@ -26,5 +26,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
index c62d104a3353a2e1443cc31a33d4d3270f74e2cc..6259a43105acc09e5269868aca9251ec3b2a2f1d 100755 (executable)
@@ -32,6 +32,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
index 5ca3482e6240a5c4abffdc7e9ee6666a51c92944..904a6af191a7a9f1f77ea0fc5155c0a0a9a892c6 100644 (file)
@@ -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']);
     }
 
index 48446e3c1a6e64be30725ac03b77e2a636313fd4..f93c59c7e9aab09959091044e1dc85df10641470 100644 (file)
@@ -1,5 +1,15 @@
 <?php
 
+// This permits to have the LdapUserInterface even when fr3d/ldap-bundle is not
+// in the packages
+namespace FR3D\LdapBundle\Model;
+
+interface LdapUserInterface
+{
+    public function setDn($dn);
+    public function getDn();
+}
+
 namespace Wallabag\UserBundle\Entity;
 
 use Doctrine\Common\Collections\ArrayCollection;
@@ -16,6 +26,7 @@ use Wallabag\ApiBundle\Entity\Client;
 use Wallabag\CoreBundle\Entity\Config;
 use Wallabag\CoreBundle\Entity\Entry;
 use Wallabag\CoreBundle\Helper\EntityTimestampsTrait;
+use FR3D\LdapBundle\Model\LdapUserInterface;
 
 /**
  * User.
@@ -28,7 +39,7 @@ use Wallabag\CoreBundle\Helper\EntityTimestampsTrait;
  * @UniqueEntity("email")
  * @UniqueEntity("username")
  */
-class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterface
+class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterface, LdapUserInterface
 {
     use EntityTimestampsTrait;
 
@@ -67,6 +78,13 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf
      */
     protected $email;
 
+    /**
+     * @var string
+     *
+     * @ORM\Column(name="dn", type="text", nullable=true)
+     */
+    protected $dn;
+
     /**
      * @var \DateTime
      *
@@ -309,4 +327,33 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf
             return $this->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 (file)
index 0000000..cea2450
--- /dev/null
@@ -0,0 +1,103 @@
+<?php
+
+namespace Wallabag\UserBundle;
+
+use FR3D\LdapBundle\Hydrator\HydratorInterface;
+use FOS\UserBundle\FOSUserEvents;
+use FOS\UserBundle\Event\UserEvent;
+
+class LdapHydrator implements HydratorInterface
+{
+    private $userManager;
+    private $eventDispatcher;
+    private $attributesMap;
+    private $enabledAttribute;
+    private $ldapBaseDn;
+    private $ldapAdminFilter;
+    private $ldapDriver;
+
+    public function __construct(
+      $user_manager,
+      $event_dispatcher,
+      array $attributes_map,
+      $ldap_base_dn,
+      $ldap_admin_filter,
+      $ldap_driver
+    ) {
+        $this->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 (file)
index 0000000..8a851f1
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+
+namespace Wallabag\UserBundle;
+
+use FOS\OAuthServerBundle\Storage\OAuthStorage;
+use OAuth2\Model\IOAuth2Client;
+use Symfony\Component\Security\Core\Exception\AuthenticationException;
+
+class OAuthStorageLdapWrapper extends OAuthStorage
+{
+    private $ldapManager;
+
+    public function setLdapManager($ldap_manager)
+    {
+        $this->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 (file)
index 0000000..5ec1608
--- /dev/null
@@ -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 (file)
index 0000000..b3e3fd8
--- /dev/null
@@ -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"
+