]> BookStack Code Mirror - bookstack/blob - app/Services/LdapService.php
Update files to PSR-2 standards
[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     {
211         $groups_to_add = [];
212         foreach ($groupsArray as $groupName) {
213             if (in_array($groupName, $checked)) {
214                 continue;
215             }
216
217             $groupsToAdd = $this->getGroupGroups($groupName);
218             $groups_to_add = array_merge($groups_to_add, $groupsToAdd);
219             $checked[] = $groupName;
220         }
221         $groupsArray = array_unique(array_merge($groupsArray, $groups_to_add), SORT_REGULAR);
222
223         if (!empty($groups_to_add)) {
224             return $this->getGroupsRecursive($groupsArray, $checked);
225         } else {
226             return $groupsArray;
227         }
228     }
229
230     /**
231      * Get the parent groups of a single group
232      * @param string $groupName
233      * @return array
234      */
235     private function getGroupGroups($groupName)
236     {
237         $ldapConnection = $this->getConnection();
238         $this->bindSystemUser($ldapConnection);
239
240         $followReferrals = $this->config['follow_referrals'] ? 1 : 0;
241         $this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
242
243         $baseDn = $this->config['base_dn'];
244         $groupsAttr = strtolower($this->config['group_attribute']);
245
246         $groups = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, 'CN='.$groupName, [$groupsAttr]);
247         if ($groups['count'] === 0) {
248             return [];
249         }
250
251         $groupGroups = $this->groupFilter($groups[0]);
252         return $groupGroups;
253     }
254
255     /**
256      * Filter out LDAP CN and DN language in a ldap search return
257      * Gets the base CN (common name) of the string
258      * @param string $ldapSearchReturn
259      * @return array
260      */
261     protected function groupFilter($ldapSearchReturn)
262     {
263         $groupsAttr = strtolower($this->config['group_attribute']);
264         $ldapGroups = [];
265         $count = 0;
266         if (isset($ldapSearchReturn[$groupsAttr]['count'])) {
267             $count = (int) $ldapSearchReturn[$groupsAttr]['count'];
268         }
269         for ($i=0; $i<$count; $i++) {
270             $dnComponents = ldap_explode_dn($ldapSearchReturn[$groupsAttr][$i], 1);
271             if (!in_array($dnComponents[0], $ldapGroups)) {
272                 $ldapGroups[] = $dnComponents[0];
273             }
274         }
275         return $ldapGroups;
276     }
277 }