Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
14.58% |
14 / 96 |
|
30.00% |
3 / 10 |
CRAP | |
0.00% |
0 / 1 |
LDAP | |
14.58% |
14 / 96 |
|
30.00% |
3 / 10 |
1037.12 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
validateConfig | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
getSetting | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
authenticate | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
checkLdap | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
connect | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
56 | |||
bindForSearch | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
findUsername | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
validateCredentialsInLdap | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
processLDAPUser | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
132 |
1 | <?php |
2 | |
3 | /** |
4 | * LDAP authentication class |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2010. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Authentication |
25 | * @author Franck Borel <franck.borel@gbv.de> |
26 | * @author Demian Katz <demian.katz@villanova.edu> |
27 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
28 | * @link https://vufind.org/wiki/development:plugins:authentication_handlers Wiki |
29 | */ |
30 | |
31 | namespace VuFind\Auth; |
32 | |
33 | use VuFind\Db\Entity\UserEntityInterface; |
34 | use VuFind\Exception\Auth as AuthException; |
35 | |
36 | use function in_array; |
37 | |
38 | /** |
39 | * LDAP authentication class |
40 | * |
41 | * @category VuFind |
42 | * @package Authentication |
43 | * @author Franck Borel <franck.borel@gbv.de> |
44 | * @author Demian Katz <demian.katz@villanova.edu> |
45 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
46 | * @link https://vufind.org/wiki/development:plugins:authentication_handlers Wiki |
47 | */ |
48 | class LDAP extends AbstractBase |
49 | { |
50 | /** |
51 | * Constructor |
52 | * |
53 | * @param ILSAuthenticator $ilsAuthenticator ILS authenticator |
54 | */ |
55 | public function __construct(protected ILSAuthenticator $ilsAuthenticator) |
56 | { |
57 | } |
58 | |
59 | /** |
60 | * Validate configuration parameters. This is a support method for getConfig(), |
61 | * so the configuration MUST be accessed using $this->config; do not call |
62 | * $this->getConfig() from within this method! |
63 | * |
64 | * @throws AuthException |
65 | * @return void |
66 | */ |
67 | protected function validateConfig() |
68 | { |
69 | // Check for missing parameters: |
70 | $requiredParams = ['host', 'port', 'basedn', 'username']; |
71 | foreach ($requiredParams as $param) { |
72 | if ( |
73 | !isset($this->config->LDAP->$param) |
74 | || empty($this->config->LDAP->$param) |
75 | ) { |
76 | throw new AuthException( |
77 | 'One or more LDAP parameters are missing. Check your config.ini!' |
78 | ); |
79 | } |
80 | } |
81 | } |
82 | |
83 | /** |
84 | * Get the requested configuration setting (or blank string if unset). |
85 | * |
86 | * @param string $name Name of parameter to retrieve. |
87 | * |
88 | * @return string |
89 | */ |
90 | protected function getSetting($name) |
91 | { |
92 | $config = $this->getConfig(); |
93 | $value = $config->LDAP->$name ?? ''; |
94 | |
95 | // Normalize all values to lowercase except for potentially case-sensitive |
96 | // bind and basedn credentials. |
97 | $doNotLower = ['bind_username', 'bind_password', 'basedn']; |
98 | return (in_array($name, $doNotLower)) ? $value : strtolower($value); |
99 | } |
100 | |
101 | /** |
102 | * Attempt to authenticate the current user. Throws exception if login fails. |
103 | * |
104 | * @param \Laminas\Http\PhpEnvironment\Request $request Request object containing |
105 | * account credentials. |
106 | * |
107 | * @throws AuthException |
108 | * @return UserEntityInterface Object representing logged-in user. |
109 | */ |
110 | public function authenticate($request) |
111 | { |
112 | $username = trim($request->getPost()->get('username', '')); |
113 | $password = trim($request->getPost()->get('password', '')); |
114 | if ($username == '' || $password == '') { |
115 | throw new AuthException('authentication_error_blank'); |
116 | } |
117 | return $this->checkLdap($username, $password); |
118 | } |
119 | |
120 | /** |
121 | * Communicate with LDAP and obtain user details. |
122 | * |
123 | * @param string $username Username |
124 | * @param string $password Password |
125 | * |
126 | * @throws AuthException |
127 | * @return UserEntityInterface Object representing logged-in user. |
128 | */ |
129 | protected function checkLdap($username, $password) |
130 | { |
131 | // Establish a connection: |
132 | $connection = $this->connect(); |
133 | |
134 | // If necessary, bind in order to perform a search: |
135 | $this->bindForSearch($connection); |
136 | |
137 | // Search for username |
138 | $info = $this->findUsername($connection, $username); |
139 | if ($info['count']) { |
140 | $data = $this->validateCredentialsInLdap($connection, $info, $password); |
141 | if ($data) { |
142 | return $this->processLDAPUser($username, $data); |
143 | } |
144 | } else { |
145 | $this->debug('user not found'); |
146 | } |
147 | |
148 | throw new AuthException('authentication_error_invalid'); |
149 | } |
150 | |
151 | /** |
152 | * Establish the LDAP connection. |
153 | * |
154 | * @return resource |
155 | */ |
156 | protected function connect() |
157 | { |
158 | // Try to connect to LDAP and die if we can't; note that some LDAP setups |
159 | // will successfully return a resource from ldap_connect even if the server |
160 | // is unavailable -- we need to check for bad return values again at search |
161 | // time! |
162 | $host = $this->getSetting('host'); |
163 | $port = $this->getSetting('port'); |
164 | $this->debug("connecting to host=$host, port=$port"); |
165 | $connection = @ldap_connect($host, $port); |
166 | if (!$connection) { |
167 | $this->debug('connection failed'); |
168 | throw new AuthException('authentication_error_technical'); |
169 | } |
170 | |
171 | // Set LDAP options -- use protocol version 3 |
172 | if (!@ldap_set_option($connection, LDAP_OPT_PROTOCOL_VERSION, 3)) { |
173 | $this->debug('Failed to set protocol version 3'); |
174 | } |
175 | |
176 | // if the host parameter is not specified as ldaps:// |
177 | // then (unless TLS is disabled) we need to initiate TLS so we |
178 | // can have a secure connection over the standard LDAP port. |
179 | $disableTls = isset($this->config->LDAP->disable_tls) |
180 | && $this->config->LDAP->disable_tls; |
181 | if (stripos($host, 'ldaps://') === false && !$disableTls) { |
182 | $this->debug('Starting TLS'); |
183 | if (!@ldap_start_tls($connection)) { |
184 | $this->debug('TLS failed'); |
185 | throw new AuthException('authentication_error_technical'); |
186 | } |
187 | } |
188 | |
189 | return $connection; |
190 | } |
191 | |
192 | /** |
193 | * If configured, bind an administrative user in order to perform a search |
194 | * |
195 | * @param resource $connection LDAP connection |
196 | * |
197 | * @return void |
198 | */ |
199 | protected function bindForSearch($connection) |
200 | { |
201 | // If bind_username and bind_password were supplied in the config file, use |
202 | // them to access LDAP before proceeding. In some LDAP setups, these |
203 | // settings can be excluded in order to skip this step. |
204 | $user = $this->getSetting('bind_username'); |
205 | $pass = $this->getSetting('bind_password'); |
206 | if ($user != '' && $pass != '') { |
207 | $this->debug("binding as $user"); |
208 | $ldapBind = @ldap_bind($connection, $user, $pass); |
209 | if (!$ldapBind) { |
210 | $this->debug('bind failed -- ' . ldap_error($connection)); |
211 | throw new AuthException('authentication_error_technical'); |
212 | } |
213 | } |
214 | } |
215 | |
216 | /** |
217 | * Find the specified username in the directory |
218 | * |
219 | * @param resource $connection LDAP connection |
220 | * @param string $username Username |
221 | * |
222 | * @return array |
223 | */ |
224 | protected function findUsername($connection, $username) |
225 | { |
226 | $ldapFilter = $this->getSetting('username') . '=' . $username; |
227 | $basedn = $this->getSetting('basedn'); |
228 | $this->debug("search for $ldapFilter using basedn=$basedn"); |
229 | $ldapSearch = @ldap_search($connection, $basedn, $ldapFilter); |
230 | if (!$ldapSearch) { |
231 | $this->debug('search failed -- ' . ldap_error($connection)); |
232 | throw new AuthException('authentication_error_technical'); |
233 | } |
234 | |
235 | return ldap_get_entries($connection, $ldapSearch); |
236 | } |
237 | |
238 | /** |
239 | * Validate credentials |
240 | * |
241 | * @param resource $connection LDAP connection |
242 | * @param array $info Data from findUsername() |
243 | * @param string $password Password to try |
244 | * |
245 | * @return bool|array Array of user data on success, false otherwise |
246 | */ |
247 | protected function validateCredentialsInLdap($connection, $info, $password) |
248 | { |
249 | // Validate the user credentials by attempting to bind to LDAP: |
250 | $dn = $info[0]['dn']; |
251 | $this->debug("binding as $dn"); |
252 | $ldapBind = @ldap_bind($connection, $dn, $password); |
253 | if (!$ldapBind) { |
254 | $this->debug('bind failed -- ' . ldap_error($connection)); |
255 | return false; |
256 | } |
257 | // If the bind was successful, we can look up the full user info: |
258 | $this->debug('bind successful; reading details'); |
259 | $ldapSearch = ldap_read($connection, $dn, 'objectclass=*'); |
260 | $data = ldap_get_entries($connection, $ldapSearch); |
261 | if ($data === false) { |
262 | $this->debug('Read failed -- ' . ldap_error($connection)); |
263 | throw new AuthException('authentication_error_technical'); |
264 | } |
265 | return $data; |
266 | } |
267 | |
268 | /** |
269 | * Build a User object from details obtained via LDAP. |
270 | * |
271 | * @param string $username Username |
272 | * @param array $data Details from ldap_get_entries call. |
273 | * |
274 | * @return UserEntityInterface Object representing logged-in user. |
275 | */ |
276 | protected function processLDAPUser($username, $data) |
277 | { |
278 | // Database fields that we may be able to load from LDAP: |
279 | $fields = [ |
280 | 'firstname', 'lastname', 'email', 'cat_username', 'cat_password', |
281 | 'college', 'major', |
282 | ]; |
283 | |
284 | // User object to populate from LDAP: |
285 | $user = $this->getOrCreateUserByUsername($username); |
286 | |
287 | // Variable to hold catalog password (handled separately from other |
288 | // attributes since we need to use setUserCatalogCredentials method to store it): |
289 | $catPassword = null; |
290 | |
291 | // Loop through LDAP response and map fields to database object based |
292 | // on configuration settings: |
293 | for ($i = 0; $i < $data['count']; $i++) { |
294 | for ($j = 0; $j < $data[$i]['count']; $j++) { |
295 | foreach ($fields as $field) { |
296 | $configValue = $this->getSetting($field); |
297 | if ($data[$i][$j] == $configValue && !empty($configValue)) { |
298 | $value = $data[$i][$configValue]; |
299 | $separator = $this->config->LDAP->separator; |
300 | // if no separator is given map only the first value |
301 | if (isset($separator)) { |
302 | $tmp = []; |
303 | for ($k = 0; $k < $value['count']; $k++) { |
304 | $tmp[] = $value[$k]; |
305 | } |
306 | $value = implode($separator, $tmp); |
307 | } else { |
308 | $value = $value[0]; |
309 | } |
310 | |
311 | if ($field != 'cat_password') { |
312 | $this->setUserValueByField($user, $field, $value ?? ''); |
313 | } else { |
314 | $catPassword = $value; |
315 | } |
316 | } |
317 | } |
318 | } |
319 | } |
320 | |
321 | // Save credentials if applicable. Note that we want to allow empty |
322 | // passwords (see https://github.com/vufind-org/vufind/pull/532), but |
323 | // we also want to be careful not to replace a non-blank password with a |
324 | // blank one in case the auth mechanism fails to provide a password on |
325 | // an occasion after the user has manually stored one. (For discussion, |
326 | // see https://github.com/vufind-org/vufind/pull/612). Note that in the |
327 | // (unlikely) scenario that a password can actually change from non-blank |
328 | // to blank, additional work may need to be done here. |
329 | if (!empty($catUsername = $user->getCatUsername())) { |
330 | $this->ilsAuthenticator->setUserCatalogCredentials( |
331 | $user, |
332 | $catUsername, |
333 | empty($catPassword) ? $this->ilsAuthenticator->getCatPasswordForUser($user) : $catPassword |
334 | ); |
335 | } |
336 | |
337 | // Update the user in the database, then return it to the caller: |
338 | $this->getUserService()->persistEntity($user); |
339 | return $user; |
340 | } |
341 | } |