]> BookStack Code Mirror - bookstack/blob - app/Services/LdapService.php
LDAP groups sync to Bookstack roles.
[bookstack] / app / Services / LdapService.php
1 <?php namespace BookStack\Services;
2
3 use BookStack\Exceptions\LdapException;
4 use Illuminate\Contracts\Auth\Authenticatable;
5
6 /**
7  * Class LdapService
8  * Handles any app-specific LDAP tasks.
9  * @package BookStack\Services
10  */
11 class LdapService
12 {
13
14         protected $ldap;
15         protected $ldapConnection;
16         protected $config;
17
18         /**
19          * LdapService constructor.
20          * @param Ldap $ldap
21          */
22         public function __construct(Ldap $ldap)
23         {
24                 $this->ldap = $ldap;
25                 $this->config = config('services.ldap');
26         }
27
28         /**
29          * Search for attributes for a specific user on the ldap
30          * @param string $userName
31          * @param array $attributes
32          * @return null|array
33          * @throws LdapException
34          */
35         private function getUserWithAttributes($userName,$attributes)
36         {
37                 $ldapConnection = $this->getConnection();
38                 $this->bindSystemUser($ldapConnection);
39
40                 // Find user
41                 $userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
42                 $baseDn = $this->config['base_dn'];
43
44                 $followReferrals = $this->config['follow_referrals'] ? 1 : 0;
45                 $this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
46                 $users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, $attributes);
47                 if ($users['count'] === 0) {
48                         return null;
49                 }
50
51                 return $users[0];
52         }
53
54         /**
55          * Get the details of a user from LDAP using the given username.
56          * User found via configurable user filter.
57          * @param $userName
58          * @return array|null
59          * @throws LdapException
60          */
61         public function getUserDetails($userName)
62         {
63                 $emailAttr = $this->config['email_attribute'];
64                 $user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr]);
65
66                 if ($user === null) {
67                         return null;
68                 }
69
70                 return [
71                         'uid'   => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
72                         'name'  => $user['cn'][0],
73                         'dn'    => $user['dn'],
74                         'email' => (isset($user[$emailAttr])) ? (is_array($user[$emailAttr]) ? $user[$emailAttr][0] : $user[$emailAttr]) : null
75                 ];
76         }
77
78         /**
79          * @param Authenticatable $user
80          * @param string          $username
81          * @param string          $password
82          * @return bool
83          * @throws LdapException
84          */
85         public function validateUserCredentials(Authenticatable $user, $username, $password)
86         {
87                 $ldapUser = $this->getUserDetails($username);
88                 if ($ldapUser === null) {
89                         return false;
90                 }
91                 if ($ldapUser['uid'] !== $user->external_auth_id) {
92                         return false;
93                 }
94
95                 $ldapConnection = $this->getConnection();
96                 try {
97                         $ldapBind = $this->ldap->bind($ldapConnection, $ldapUser['dn'], $password);
98                 } catch (\ErrorException $e) {
99                         $ldapBind = false;
100                 }
101
102                 return $ldapBind;
103         }
104
105         /**
106          * Bind the system user to the LDAP connection using the given credentials
107          * otherwise anonymous access is attempted.
108          * @param $connection
109          * @throws LdapException
110          */
111         protected function bindSystemUser($connection)
112         {
113                 $ldapDn = $this->config['dn'];
114                 $ldapPass = $this->config['pass'];
115
116                 $isAnonymous = ($ldapDn === false || $ldapPass === false);
117                 if ($isAnonymous) {
118                         $ldapBind = $this->ldap->bind($connection);
119                 } else {
120                         $ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
121                 }
122
123                 if (!$ldapBind) {
124                         throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));
125                 }
126         }
127
128         /**
129          * Get the connection to the LDAP server.
130          * Creates a new connection if one does not exist.
131          * @return resource
132          * @throws LdapException
133          */
134         protected function getConnection()
135         {
136                 if ($this->ldapConnection !== null) {
137                         return $this->ldapConnection;
138                 }
139
140                 // Check LDAP extension in installed
141                 if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
142                         throw new LdapException(trans('errors.ldap_extension_not_installed'));
143                 }
144
145                 // Get port from server string and protocol if specified.
146                 $ldapServer = explode(':', $this->config['server']);
147                 $hasProtocol = preg_match('/^ldaps{0,1}\:\/\//', $this->config['server']) === 1;
148                 if (!$hasProtocol) {
149                         array_unshift($ldapServer, '');
150                 }
151                 $hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1];
152                 $defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389;
153                 $ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);
154
155                 if ($ldapConnection === false) {
156                         throw new LdapException(trans('errors.ldap_cannot_connect'));
157                 }
158
159                 // Set any required options
160                 if ($this->config['version']) {
161                         $this->ldap->setVersion($ldapConnection, $this->config['version']);
162                 }
163
164                 $this->ldapConnection = $ldapConnection;
165                 return $this->ldapConnection;
166         }
167
168         /**
169          * Build a filter string by injecting common variables.
170          * @param string $filterString
171          * @param array $attrs
172          * @return string
173          */
174         protected function buildFilter($filterString, array $attrs)
175         {
176                 $newAttrs = [];
177                 foreach ($attrs as $key => $attrText) {
178                         $newKey = '${' . $key . '}';
179                         $newAttrs[$newKey] = $attrText;
180                 }
181                 return strtr($filterString, $newAttrs);
182         }
183
184         /**
185          * Get the groups a user is a part of on ldap
186          * @param string $userName
187          * @return array|null
188          */
189         public function getUserGroups($userName)
190         {
191                 $groupsAttr = $this->config['group_attribute'];
192                 $user = $this->getUserWithAttributes($userName, [$groupsAttr]);
193
194                 if ($user === null) {
195                         return null;
196                 }
197
198                 $userGroups = $this->groupFilter($user);
199                 $userGroups = $this->getGroupsRecursive($userGroups,[]);
200                 return $userGroups;
201         }
202
203         /**
204          * Get the parent groups of an array of groups
205          * @param array $groupsArray
206          * @param array $checked
207          * @return array
208          */
209         private function getGroupsRecursive($groupsArray,$checked) {
210                 $groups_to_add = [];
211                 foreach ($groupsArray as $groupName) {
212                         if (in_array($groupName,$checked)) continue;
213
214                         $groupsToAdd = $this->getGroupGroups($groupName);
215                         $groups_to_add = array_merge($groups_to_add,$groupsToAdd);
216                         $checked[] = $groupName;
217                 }
218                 $groupsArray = array_unique(array_merge($groupsArray,$groups_to_add), SORT_REGULAR);
219
220                 if (!empty($groups_to_add)) {
221                         return $this->getGroupsRecursive($groupsArray,$checked);
222                 } else {
223                         return $groupsArray;
224                 }
225         }
226
227         /**
228          * Get the parent groups of a single group
229          * @param string $groupName
230          * @return array
231          */
232         private function getGroupGroups($groupName)
233         {
234                 $ldapConnection = $this->getConnection();
235                 $this->bindSystemUser($ldapConnection);
236
237                 $followReferrals = $this->config['follow_referrals'] ? 1 : 0;
238                 $this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
239
240                 $baseDn = $this->config['base_dn'];
241                 $groupsAttr = strtolower($this->config['group_attribute']);
242
243                 $groups = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, 'CN='.$groupName, [$groupsAttr]);
244                 if ($groups['count'] === 0) {
245                         return [];
246                 }
247
248                 $groupGroups = $this->groupFilter($groups[0]);
249                 return $groupGroups;
250         }
251
252         /**
253          * Filter out LDAP CN and DN language in a ldap search return
254          * Gets the base CN (common name) of the string
255          * @param string $ldapSearchReturn
256          * @return array
257          */
258         protected function groupFilter($ldapSearchReturn)
259         {
260                 $groupsAttr = strtolower($this->config['group_attribute']);
261                 $ldapGroups = [];
262                 $count = 0;
263                 if (isset($ldapSearchReturn[$groupsAttr]['count'])) $count = (int) $ldapSearchReturn[$groupsAttr]['count'];
264                 for ($i=0;$i<$count;$i++) {
265                         $dnComponents = ldap_explode_dn($ldapSearchReturn[$groupsAttr][$i],1);
266                         if (!in_array($dnComponents[0],$ldapGroups)) {
267                                 $ldapGroups[] = $dnComponents[0];
268                         }
269                 }
270                 return $ldapGroups;
271         }
272
273 }