]> BookStack Code Mirror - bookstack/commitdiff
LDAP groups sync to Bookstack roles.
authorBrennan Murphy <redacted>
Mon, 2 Jul 2018 17:09:39 +0000 (17:09 +0000)
committerBrennan Murphy <redacted>
Mon, 2 Jul 2018 17:09:39 +0000 (17:09 +0000)
Closes #75

.env.example
app/Http/Controllers/Auth/LoginController.php
app/Repos/LdapRepo.php [new file with mode: 0644]
app/Services/LdapService.php
config/services.php

index ccafaf4fb2c53b0a00ed858a957ca225a5565d38..57e6af6a925c1b0ee33b209375e8e4e316bd0f38 100644 (file)
@@ -67,6 +67,15 @@ LDAP_DN=false
 LDAP_PASS=false
 LDAP_USER_FILTER=false
 LDAP_VERSION=false
+#do you want to sync LDAP groups to BookStack roles for a user
+LDAP_USER_TO_GROUPS=false
+#what is the LDAP attribute for group memberships
+LDAP_GROUP_ATTRIBUTE="memberOf"
+#what LDAP group should the user be a part of to be an admin on BookStack
+LDAP_ADMIN_GROUP="Domain Admins"
+#would you like to remove users from roles on bookstack if they do not match on LDAP
+#if false, the ldap groups-roles sync will only add users to roles
+LDAP_REMOVE_FROM_GROUPS=false
 
 # Mail settings
 MAIL_DRIVER=smtp
index 106b905244229bf2fdf96f4d637bd2e6748be1a2..4c846d0e0fa405f0f6c062a5e375822307164922 100644 (file)
@@ -5,6 +5,7 @@ namespace BookStack\Http\Controllers\Auth;
 use BookStack\Exceptions\AuthException;
 use BookStack\Http\Controllers\Controller;
 use BookStack\Repos\UserRepo;
+use BookStack\Repos\LdapRepo;
 use BookStack\Services\SocialAuthService;
 use Illuminate\Contracts\Auth\Authenticatable;
 use Illuminate\Foundation\Auth\AuthenticatesUsers;
@@ -96,7 +97,14 @@ class LoginController extends Controller
             auth()->login($user);
         }
 
-        $path = session()->pull('url.intended', '/');
+               // ldap groups refresh
+               if (config('services.ldap.user_to_groups') !== false && $request->filled('username')) {
+                       $ldapRepo = new LdapRepo($this->userRepo);
+                       $ldapRepo->syncGroups($user,$request->input('username'));
+               }
+
+
+               $path = session()->pull('url.intended', '/');
         $path = baseUrl($path, true);
         return redirect($path);
     }
diff --git a/app/Repos/LdapRepo.php b/app/Repos/LdapRepo.php
new file mode 100644 (file)
index 0000000..33d05ea
--- /dev/null
@@ -0,0 +1,84 @@
+<?php namespace BookStack\Repos;
+
+use BookStack\Services\Ldap;
+use BookStack\Services\LdapService;
+use BookStack\Role;
+use BookStack\Repos\UserRepo;
+
+class LdapRepo
+{
+
+       protected $ldap = null;
+       protected $ldapService = null;
+
+       protected $config;
+
+       /**
+        * LdapRepo constructor.
+        * @param \BookStack\Repos\UserRepo $userRepo
+        */
+       public function __construct(UserRepo $userRepo)
+       {
+               $this->config = config('services.ldap');
+
+               if (config('auth.method') !== 'ldap') {
+                       return false;
+               }
+
+               $this->ldapService = new LdapService(new Ldap);
+               $this->userRepo = $userRepo;
+       }
+
+       /**
+        * If there is no ldap connection, all methods calls to this library will return null
+        */
+       public function __call($method, $arguments)
+       {
+               if ($this->ldap === null) {
+                       return null;
+               }
+
+               return call_user_func_array(array($this,$method),$arguments);
+       }
+
+       /**
+        * Sync the LDAP groups to the user roles for the current user
+        * @param \BookStack\User $user
+        * @param string $userName
+        * @throws \BookStack\Exceptions\NotFoundException
+        */
+       public function syncGroups($user,$userName)
+       {
+               $userLdapGroups = $this->ldapService->getUserGroups($userName);
+               $userLdapGroups = $this->groupNameFilter($userLdapGroups);
+               // get the ids for the roles from the names
+               $ldapGroupsAsRoles = Role::whereIn('name',$userLdapGroups)->pluck('id');
+               // sync groups
+               if ($this->config['remove_from_groups']) {
+                       $user->roles()->sync($ldapGroupsAsRoles);
+                       $this->userRepo->attachDefaultRole($user);
+               } else {
+                       $user->roles()->syncWithoutDetaching($ldapGroupsAsRoles);
+               }
+
+               // make the user an admin?
+               if (in_array($this->config['admin'],$userLdapGroups)) {
+                       $this->userRepo->attachSystemRole($user,'admin');
+               }
+       }
+
+       /**
+        * Filter to convert the groups from ldap to the format of the roles name on BookStack
+        * Spaces replaced with -, all lowercase letters
+        * @param array $groups
+        * @return array
+        */
+       private function groupNameFilter($groups)
+       {
+               $return = [];
+               foreach ($groups as $groupName) {
+                       $return[] = str_replace(' ', '-', strtolower($groupName));
+               }
+               return $return;
+       }
+}
\ No newline at end of file
index 3eb2f2830e69bec68ea1a21630ad71ee0a4d0959..e56f45e4ea754535b93e3417b0e5554251fa6a52 100644 (file)
@@ -11,155 +11,263 @@ use Illuminate\Contracts\Auth\Authenticatable;
 class LdapService
 {
 
-    protected $ldap;
-    protected $ldapConnection;
-    protected $config;
-
-    /**
-     * LdapService constructor.
-     * @param Ldap $ldap
-     */
-    public function __construct(Ldap $ldap)
-    {
-        $this->ldap = $ldap;
-        $this->config = config('services.ldap');
-    }
-
-    /**
-     * Get the details of a user from LDAP using the given username.
-     * User found via configurable user filter.
-     * @param $userName
-     * @return array|null
-     * @throws LdapException
-     */
-    public function getUserDetails($userName)
-    {
-        $ldapConnection = $this->getConnection();
-        $this->bindSystemUser($ldapConnection);
-
-        // Find user
-        $userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
-        $baseDn = $this->config['base_dn'];
-        $emailAttr = $this->config['email_attribute'];
-        $followReferrals = $this->config['follow_referrals'] ? 1 : 0;
-        $this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
-        $users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', $emailAttr]);
-        if ($users['count'] === 0) {
-            return null;
-        }
-
-        $user = $users[0];
-        return [
-            'uid'   => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
-            'name'  => $user['cn'][0],
-            'dn'    => $user['dn'],
-            'email' => (isset($user[$emailAttr])) ? (is_array($user[$emailAttr]) ? $user[$emailAttr][0] : $user[$emailAttr]) : null
-        ];
-    }
-
-    /**
-     * @param Authenticatable $user
-     * @param string          $username
-     * @param string          $password
-     * @return bool
-     * @throws LdapException
-     */
-    public function validateUserCredentials(Authenticatable $user, $username, $password)
-    {
-        $ldapUser = $this->getUserDetails($username);
-        if ($ldapUser === null) {
-            return false;
-        }
-        if ($ldapUser['uid'] !== $user->external_auth_id) {
-            return false;
-        }
-
-        $ldapConnection = $this->getConnection();
-        try {
-            $ldapBind = $this->ldap->bind($ldapConnection, $ldapUser['dn'], $password);
-        } catch (\ErrorException $e) {
-            $ldapBind = false;
-        }
-
-        return $ldapBind;
-    }
-
-    /**
-     * Bind the system user to the LDAP connection using the given credentials
-     * otherwise anonymous access is attempted.
-     * @param $connection
-     * @throws LdapException
-     */
-    protected function bindSystemUser($connection)
-    {
-        $ldapDn = $this->config['dn'];
-        $ldapPass = $this->config['pass'];
-
-        $isAnonymous = ($ldapDn === false || $ldapPass === false);
-        if ($isAnonymous) {
-            $ldapBind = $this->ldap->bind($connection);
-        } else {
-            $ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
-        }
-
-        if (!$ldapBind) {
-            throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));
-        }
-    }
-
-    /**
-     * Get the connection to the LDAP server.
-     * Creates a new connection if one does not exist.
-     * @return resource
-     * @throws LdapException
-     */
-    protected function getConnection()
-    {
-        if ($this->ldapConnection !== null) {
-            return $this->ldapConnection;
-        }
-
-        // Check LDAP extension in installed
-        if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
-            throw new LdapException(trans('errors.ldap_extension_not_installed'));
-        }
-
-        // Get port from server string and protocol if specified.
-        $ldapServer = explode(':', $this->config['server']);
-        $hasProtocol = preg_match('/^ldaps{0,1}\:\/\//', $this->config['server']) === 1;
-        if (!$hasProtocol) {
-            array_unshift($ldapServer, '');
-        }
-        $hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1];
-        $defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389;
-        $ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);
-
-        if ($ldapConnection === false) {
-            throw new LdapException(trans('errors.ldap_cannot_connect'));
-        }
-
-        // Set any required options
-        if ($this->config['version']) {
-            $this->ldap->setVersion($ldapConnection, $this->config['version']);
-        }
-
-        $this->ldapConnection = $ldapConnection;
-        return $this->ldapConnection;
-    }
-
-    /**
-     * Build a filter string by injecting common variables.
-     * @param string $filterString
-     * @param array $attrs
-     * @return string
-     */
-    protected function buildFilter($filterString, array $attrs)
-    {
-        $newAttrs = [];
-        foreach ($attrs as $key => $attrText) {
-            $newKey = '${' . $key . '}';
-            $newAttrs[$newKey] = $attrText;
-        }
-        return strtr($filterString, $newAttrs);
-    }
-}
+       protected $ldap;
+       protected $ldapConnection;
+       protected $config;
+
+       /**
+        * LdapService constructor.
+        * @param Ldap $ldap
+        */
+       public function __construct(Ldap $ldap)
+       {
+               $this->ldap = $ldap;
+               $this->config = config('services.ldap');
+       }
+
+       /**
+        * Search for attributes for a specific user on the ldap
+        * @param string $userName
+        * @param array $attributes
+        * @return null|array
+        * @throws LdapException
+        */
+       private function getUserWithAttributes($userName,$attributes)
+       {
+               $ldapConnection = $this->getConnection();
+               $this->bindSystemUser($ldapConnection);
+
+               // Find user
+               $userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
+               $baseDn = $this->config['base_dn'];
+
+               $followReferrals = $this->config['follow_referrals'] ? 1 : 0;
+               $this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
+               $users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, $attributes);
+               if ($users['count'] === 0) {
+                       return null;
+               }
+
+               return $users[0];
+       }
+
+       /**
+        * Get the details of a user from LDAP using the given username.
+        * User found via configurable user filter.
+        * @param $userName
+        * @return array|null
+        * @throws LdapException
+        */
+       public function getUserDetails($userName)
+       {
+               $emailAttr = $this->config['email_attribute'];
+               $user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr]);
+
+               if ($user === null) {
+                       return null;
+               }
+
+               return [
+                       'uid'   => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
+                       'name'  => $user['cn'][0],
+                       'dn'    => $user['dn'],
+                       'email' => (isset($user[$emailAttr])) ? (is_array($user[$emailAttr]) ? $user[$emailAttr][0] : $user[$emailAttr]) : null
+               ];
+       }
+
+       /**
+        * @param Authenticatable $user
+        * @param string          $username
+        * @param string          $password
+        * @return bool
+        * @throws LdapException
+        */
+       public function validateUserCredentials(Authenticatable $user, $username, $password)
+       {
+               $ldapUser = $this->getUserDetails($username);
+               if ($ldapUser === null) {
+                       return false;
+               }
+               if ($ldapUser['uid'] !== $user->external_auth_id) {
+                       return false;
+               }
+
+               $ldapConnection = $this->getConnection();
+               try {
+                       $ldapBind = $this->ldap->bind($ldapConnection, $ldapUser['dn'], $password);
+               } catch (\ErrorException $e) {
+                       $ldapBind = false;
+               }
+
+               return $ldapBind;
+       }
+
+       /**
+        * Bind the system user to the LDAP connection using the given credentials
+        * otherwise anonymous access is attempted.
+        * @param $connection
+        * @throws LdapException
+        */
+       protected function bindSystemUser($connection)
+       {
+               $ldapDn = $this->config['dn'];
+               $ldapPass = $this->config['pass'];
+
+               $isAnonymous = ($ldapDn === false || $ldapPass === false);
+               if ($isAnonymous) {
+                       $ldapBind = $this->ldap->bind($connection);
+               } else {
+                       $ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
+               }
+
+               if (!$ldapBind) {
+                       throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));
+               }
+       }
+
+       /**
+        * Get the connection to the LDAP server.
+        * Creates a new connection if one does not exist.
+        * @return resource
+        * @throws LdapException
+        */
+       protected function getConnection()
+       {
+               if ($this->ldapConnection !== null) {
+                       return $this->ldapConnection;
+               }
+
+               // Check LDAP extension in installed
+               if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
+                       throw new LdapException(trans('errors.ldap_extension_not_installed'));
+               }
+
+               // Get port from server string and protocol if specified.
+               $ldapServer = explode(':', $this->config['server']);
+               $hasProtocol = preg_match('/^ldaps{0,1}\:\/\//', $this->config['server']) === 1;
+               if (!$hasProtocol) {
+                       array_unshift($ldapServer, '');
+               }
+               $hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1];
+               $defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389;
+               $ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);
+
+               if ($ldapConnection === false) {
+                       throw new LdapException(trans('errors.ldap_cannot_connect'));
+               }
+
+               // Set any required options
+               if ($this->config['version']) {
+                       $this->ldap->setVersion($ldapConnection, $this->config['version']);
+               }
+
+               $this->ldapConnection = $ldapConnection;
+               return $this->ldapConnection;
+       }
+
+       /**
+        * Build a filter string by injecting common variables.
+        * @param string $filterString
+        * @param array $attrs
+        * @return string
+        */
+       protected function buildFilter($filterString, array $attrs)
+       {
+               $newAttrs = [];
+               foreach ($attrs as $key => $attrText) {
+                       $newKey = '${' . $key . '}';
+                       $newAttrs[$newKey] = $attrText;
+               }
+               return strtr($filterString, $newAttrs);
+       }
+
+       /**
+        * Get the groups a user is a part of on ldap
+        * @param string $userName
+        * @return array|null
+        */
+       public function getUserGroups($userName)
+       {
+               $groupsAttr = $this->config['group_attribute'];
+               $user = $this->getUserWithAttributes($userName, [$groupsAttr]);
+
+               if ($user === null) {
+                       return null;
+               }
+
+               $userGroups = $this->groupFilter($user);
+               $userGroups = $this->getGroupsRecursive($userGroups,[]);
+               return $userGroups;
+       }
+
+       /**
+        * Get the parent groups of an array of groups
+        * @param array $groupsArray
+        * @param array $checked
+        * @return array
+        */
+       private function getGroupsRecursive($groupsArray,$checked) {
+               $groups_to_add = [];
+               foreach ($groupsArray as $groupName) {
+                       if (in_array($groupName,$checked)) continue;
+
+                       $groupsToAdd = $this->getGroupGroups($groupName);
+                       $groups_to_add = array_merge($groups_to_add,$groupsToAdd);
+                       $checked[] = $groupName;
+               }
+               $groupsArray = array_unique(array_merge($groupsArray,$groups_to_add), SORT_REGULAR);
+
+               if (!empty($groups_to_add)) {
+                       return $this->getGroupsRecursive($groupsArray,$checked);
+               } else {
+                       return $groupsArray;
+               }
+       }
+
+       /**
+        * Get the parent groups of a single group
+        * @param string $groupName
+        * @return array
+        */
+       private function getGroupGroups($groupName)
+       {
+               $ldapConnection = $this->getConnection();
+               $this->bindSystemUser($ldapConnection);
+
+               $followReferrals = $this->config['follow_referrals'] ? 1 : 0;
+               $this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
+
+               $baseDn = $this->config['base_dn'];
+               $groupsAttr = strtolower($this->config['group_attribute']);
+
+               $groups = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, 'CN='.$groupName, [$groupsAttr]);
+               if ($groups['count'] === 0) {
+                       return [];
+               }
+
+               $groupGroups = $this->groupFilter($groups[0]);
+               return $groupGroups;
+       }
+
+       /**
+        * Filter out LDAP CN and DN language in a ldap search return
+        * Gets the base CN (common name) of the string
+        * @param string $ldapSearchReturn
+        * @return array
+        */
+       protected function groupFilter($ldapSearchReturn)
+       {
+               $groupsAttr = strtolower($this->config['group_attribute']);
+               $ldapGroups = [];
+               $count = 0;
+               if (isset($ldapSearchReturn[$groupsAttr]['count'])) $count = (int) $ldapSearchReturn[$groupsAttr]['count'];
+               for ($i=0;$i<$count;$i++) {
+                       $dnComponents = ldap_explode_dn($ldapSearchReturn[$groupsAttr][$i],1);
+                       if (!in_array($dnComponents[0],$ldapGroups)) {
+                               $ldapGroups[] = $dnComponents[0];
+                       }
+               }
+               return $ldapGroups;
+       }
+
+}
\ No newline at end of file
index 825b1f109f8bc95a8ce191118a86fa288dcc699f..daa1606437a051e4cfc46ad49eff600a7b28f671 100644 (file)
@@ -118,6 +118,10 @@ return [
         'version' => env('LDAP_VERSION', false),
         'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
         'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false),
-    ]
+               'user_to_groups' => env('LDAP_USER_TO_GROUPS',false),
+               'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
+               'admin' => env('LDAP_ADMIN_GROUP','Domain Admins'),
+               'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS',false),
+       ]
 
 ];