Issues (2963)

LibreNMS/Authentication/LdapAuthorizer.php (1 issue)

1
<?php
2
3
namespace LibreNMS\Authentication;
4
5
use ErrorException;
6
use LibreNMS\Config;
7
use LibreNMS\Exceptions\AuthenticationException;
8
use LibreNMS\Exceptions\LdapMissingException;
9
10
class LdapAuthorizer extends AuthorizerBase
11
{
12
    protected $ldap_connection;
13
    private $userloginname = '';
14
15
    public function authenticate($credentials)
16
    {
17
        $connection = $this->getLdapConnection(true);
18
19
        if (! empty($credentials['username'])) {
20
            $username = $credentials['username'];
21
            $this->userloginname = $username;
22
            if (Config::get('auth_ldap_wildcard_ou', false)) {
23
                $this->setAuthLdapSuffixOu($username);
24
            }
25
26
            if (! empty($credentials['password']) && ldap_bind($connection, $this->getFullDn($username), $credentials['password'])) {
27
                // ldap_bind has done a bind with the user credentials. If binduser is configured, rebind with the auth_ldap_binduser
28
                // normal user has restricted right to search in ldap. auth_ldap_binduser has full search rights
29
                if ((Config::has('auth_ldap_binduser') || Config::has('auth_ldap_binddn')) && Config::has('auth_ldap_bindpassword')) {
30
                    $this->bind();
31
                }
32
                $ldap_groups = $this->getGroupList();
33
                if (empty($ldap_groups)) {
34
                    // no groups, don't check membership
35
                    return true;
36
                } else {
37
                    foreach ($ldap_groups as $ldap_group) {
38
                        if (Config::get('auth_ldap_userdn') === true) {
39
                            $ldap_comparison = ldap_compare(
40
                                $connection,
0 ignored issues
show
It seems like $connection can also be of type false; however, parameter $ldap of ldap_compare() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

40
                                /** @scrutinizer ignore-type */ $connection,
Loading history...
41
                                $ldap_group,
42
                                Config::get('auth_ldap_groupmemberattr', 'memberUid'),
43
                                $this->getFullDn($username)
44
                            );
45
                        } else {
46
                            $ldap_comparison = ldap_compare(
47
                                $connection,
48
                                $ldap_group,
49
                                Config::get('auth_ldap_groupmemberattr', 'memberUid'),
50
                                $this->getMembername($username)
51
                            );
52
                        }
53
                        if ($ldap_comparison === true) {
54
                            return true;
55
                        }
56
                    }
57
                }
58
            }
59
60
            if (empty($credentials['password'])) {
61
                throw new AuthenticationException('A password is required');
62
            }
63
64
            throw new AuthenticationException(ldap_error($connection));
65
        }
66
67
        throw new AuthenticationException();
68
    }
69
70
    public function userExists($username, $throw_exception = false)
71
    {
72
        try {
73
            $connection = $this->getLdapConnection();
74
75
            $filter = '(' . Config::get('auth_ldap_prefix') . $username . ')';
76
            $search = ldap_search($connection, trim(Config::get('auth_ldap_suffix'), ','), $filter);
77
            $entries = ldap_get_entries($connection, $search);
78
            if ($entries['count']) {
79
                return true;
80
            }
81
        } catch (AuthenticationException $e) {
82
            if ($throw_exception) {
83
                throw $e;
84
            } else {
85
                echo $e->getMessage() . PHP_EOL;
86
            }
87
        } catch (ErrorException $e) {
88
            if ($throw_exception) {
89
                throw new AuthenticationException('Could not verify user', false, 0, $e);
90
            } else {
91
                echo $e->getMessage() . PHP_EOL;
92
            }
93
        }
94
95
        return false;
96
    }
97
98
    public function getUserlevel($username)
99
    {
100
        $userlevel = 0;
101
102
        try {
103
            $connection = $this->getLdapConnection();
104
            $groups = Config::get('auth_ldap_groups');
105
106
            // Find all defined groups $username is in
107
            $group_names = array_keys($groups);
108
            $ldap_group_filter = '';
109
            foreach ($group_names as $group_name) {
110
                $ldap_group_filter .= '(cn=' . trim($group_name) . ')';
111
            }
112
            if (count($group_names) > 1) {
113
                $ldap_group_filter = "(|{$ldap_group_filter})";
114
            }
115
            if (Config::get('auth_ldap_userdn') === true) {
116
                $filter = "(&{$ldap_group_filter}(" . trim(Config::get('auth_ldap_groupmemberattr', 'memberUid')) . '=' . $this->getFullDn($username) . '))';
117
            } else {
118
                $filter = "(&{$ldap_group_filter}(" . trim(Config::get('auth_ldap_groupmemberattr', 'memberUid')) . '=' . $this->getMembername($username) . '))';
119
            }
120
            $search = ldap_search($connection, Config::get('auth_ldap_groupbase'), $filter);
121
            $entries = ldap_get_entries($connection, $search);
122
123
            // Loop the list and find the highest level
124
            foreach ($entries as $entry) {
125
                $groupname = $entry['cn'][0];
126
                if ($groups[$groupname]['level'] > $userlevel) {
127
                    $userlevel = $groups[$groupname]['level'];
128
                }
129
            }
130
        } catch (AuthenticationException $e) {
131
            echo $e->getMessage() . PHP_EOL;
132
        }
133
134
        return $userlevel;
135
    }
136
137
    public function getUserid($username)
138
    {
139
        try {
140
            $connection = $this->getLdapConnection();
141
142
            $filter = '(' . Config::get('auth_ldap_prefix') . $username . ')';
143
            $search = ldap_search($connection, trim(Config::get('auth_ldap_suffix'), ','), $filter);
144
            $entries = ldap_get_entries($connection, $search);
145
146
            if ($entries['count']) {
147
                $uid_attr = strtolower(Config::get('auth_ldap_uid_attribute', 'uidnumber'));
148
149
                return $entries[0][$uid_attr][0];
150
            }
151
        } catch (AuthenticationException $e) {
152
            echo $e->getMessage() . PHP_EOL;
153
        }
154
155
        return -1;
156
    }
157
158
    public function getUserlist()
159
    {
160
        $userlist = [];
161
162
        try {
163
            $connection = $this->getLdapConnection();
164
165
            $ldap_groups = $this->getGroupList();
166
            if (empty($ldap_groups)) {
167
                d_echo('No groups defined.  Cannot search for users.');
168
169
                return [];
170
            }
171
172
            $filter = '(' . Config::get('auth_ldap_prefix') . '*)';
173
            if (Config::get('auth_ldap_userlist_filter') != null) {
174
                $filter = '(' . Config::get('auth_ldap_userlist_filter') . ')';
175
            }
176
177
            // build group filter
178
            $group_filter = '';
179
            foreach ($ldap_groups as $group) {
180
                $group_filter .= '(memberOf=' . trim($group) . ')';
181
            }
182
            if (count($ldap_groups) > 1) {
183
                $group_filter = "(|$group_filter)";
184
            }
185
186
            // search using memberOf
187
            $search = ldap_search($connection, trim(Config::get('auth_ldap_suffix'), ','), "(&$filter$group_filter)");
188
            if (ldap_count_entries($connection, $search)) {
189
                foreach (ldap_get_entries($connection, $search) as $entry) {
190
                    $user = $this->ldapToUser($entry);
191
                    $userlist[$user['username']] = $user;
192
                }
193
            } else {
194
                // probably doesn't support memberOf, go through all users, this could be slow
195
                $search = ldap_search($connection, trim(Config::get('auth_ldap_suffix'), ','), $filter);
196
                foreach (ldap_get_entries($connection, $search) as $entry) {
197
                    foreach ($ldap_groups as $ldap_group) {
198
                        if (ldap_compare(
199
                            $connection,
200
                            $ldap_group,
201
                            Config::get('auth_ldap_groupmemberattr', 'memberUid'),
202
                            $this->getMembername($entry['uid'][0])
203
                        )) {
204
                            $user = $this->ldapToUser($entry);
205
                            $userlist[$user['username']] = $user;
206
                        }
207
                    }
208
                }
209
            }
210
        } catch (AuthenticationException $e) {
211
            echo $e->getMessage() . PHP_EOL;
212
        }
213
214
        return $userlist;
215
    }
216
217
    public function getUser($user_id)
218
    {
219
        $connection = $this->getLdapConnection();
220
221
        $filter = '(' . Config::get('auth_ldap_prefix') . $this->userloginname . ')';
222
        if (Config::get('auth_ldap_userlist_filter') != null) {
223
            $filter = '(' . Config::get('auth_ldap_userlist_filter') . ')';
224
        }
225
226
        $search = ldap_search($connection, trim(Config::get('auth_ldap_suffix'), ','), $filter);
227
        $entries = ldap_get_entries($connection, $search);
228
        foreach ($entries as $entry) {
229
            $user = $this->ldapToUser($entry);
230
            if ((int) $user['user_id'] !== (int) $user_id) {
231
                continue;
232
            }
233
234
            return $user;
235
        }
236
237
        return false;
238
    }
239
240
    protected function getMembername($username)
241
    {
242
        $type = Config::get('auth_ldap_groupmembertype');
243
244
        if ($type == 'fulldn') {
245
            return $this->getFullDn($username);
246
        }
247
248
        if ($type == 'puredn') {
249
            try {
250
                $connection = $this->getLdapConnection();
251
                $filter = '(' . Config::get('auth_ldap_attr.uid') . '=' . $username . ')';
252
                $search = ldap_search($connection, Config::get('auth_ldap_groupbase'), $filter);
253
                $entries = ldap_get_entries($connection, $search);
254
255
                return $entries[0]['dn'];
256
            } catch (AuthenticationException $e) {
257
                echo $e->getMessage() . PHP_EOL;
258
            }
259
        }
260
261
        return $username;
262
    }
263
264
    public function getGroupList()
265
    {
266
        $ldap_groups = [];
267
268
        $default_group = 'cn=groupname,ou=groups,dc=example,dc=com';  // in the documentation
269
        if (Config::get('auth_ldap_group', $default_group) !== $default_group) {
270
            $ldap_groups[] = Config::get('auth_ldap_group');
271
        }
272
273
        foreach (Config::get('auth_ldap_groups') as $key => $value) {
274
            $ldap_groups[] = "cn=$key," . Config::get('auth_ldap_groupbase');
275
        }
276
277
        return $ldap_groups;
278
    }
279
280
    /**
281
     * Get the full dn with auth_ldap_prefix and auth_ldap_suffix
282
     *
283
     * @internal
284
     *
285
     * @return string
286
     */
287
    protected function getFullDn($username)
288
    {
289
        return Config::get('auth_ldap_prefix', '') . $username . Config::get('auth_ldap_suffix', '');
290
    }
291
292
    /**
293
     * Set auth_ldap_suffix ou according to $username dn
294
     * useful if Config::get('auth_ldap_wildcard_ou) is set
295
     *
296
     * @internal
297
     *
298
     * @return false|true
299
     */
300
    protected function setAuthLdapSuffixOu($username)
301
    {
302
        $connection = $this->getLdapConnection();
303
        $filter = '(' . Config::get('auth_ldap_attr.uid') . '=' . $username . ')';
304
        $base_dn = preg_replace('/,ou=[^,]+,/', ',', Config::get('auth_ldap_suffix'));
305
        $base_dn = trim($base_dn, ',');
306
        $search = ldap_search($connection, $base_dn, $filter);
307
        foreach (ldap_get_entries($connection, $search) as $entry) {
308
            if ($entry['uid'][0] == $username) {
309
                preg_match('~,ou=([^,]+),~', $entry['dn'], $matches);
310
                $user_ou = $matches[1];
311
                $new_auth_ldap_suffix = preg_replace('/,ou=[^,]+,/', ',ou=' . $user_ou . ',', Config::get('auth_ldap_suffix'));
312
                Config::set('auth_ldap_suffix', $new_auth_ldap_suffix);
313
314
                return true;
315
            }
316
        }
317
318
        return false;
319
    }
320
321
    /**
322
     * Get the ldap connection. If it hasn't been established yet, connect and try to bind.
323
     *
324
     * @internal
325
     *
326
     * @param  bool  $skip_bind  do not attempt to bind on connection
327
     * @return false|resource
328
     *
329
     * @throws AuthenticationException
330
     */
331
    protected function getLdapConnection($skip_bind = false)
332
    {
333
        if ($this->ldap_connection) {
334
            return $this->ldap_connection; // bind already attempted
335
        }
336
337
        if ($skip_bind) {
338
            $this->connect();
339
        } else {
340
            $this->bind();
341
        }
342
343
        return $this->ldap_connection;
344
    }
345
346
    /**
347
     * @param  array  $entry  ldap entry array
348
     * @return array
349
     */
350
    private function ldapToUser($entry)
351
    {
352
        $uid_attr = strtolower(Config::get('auth_ldap_uid_attribute', 'uidnumber'));
353
354
        return [
355
            'username' => $entry['uid'][0],
356
            'realname' => $entry['cn'][0],
357
            'user_id' => (int) $entry[$uid_attr][0],
358
            'email' => $entry[Config::get('auth_ldap_emailattr', 'mail')][0],
359
            'level' => $this->getUserlevel($entry['uid'][0]),
360
        ];
361
    }
362
363
    private function connect()
364
    {
365
        if ($this->ldap_connection) {
366
            return;
367
        }
368
369
        if (! function_exists('ldap_connect')) {
370
            throw new LdapMissingException();
371
        }
372
373
        $this->ldap_connection = @ldap_connect(Config::get('auth_ldap_server'), Config::get('auth_ldap_port', 389));
374
375
        if (! $this->ldap_connection) {
376
            throw new AuthenticationException('Unable to connect to ldap server');
377
        }
378
379
        ldap_set_option($this->ldap_connection, LDAP_OPT_PROTOCOL_VERSION, Config::get('auth_ldap_version', 3));
380
381
        $use_tls = Config::get('auth_ldap_starttls');
382
        if ($use_tls == 'optional' || $use_tls == 'require') {
383
            $tls_success = ldap_start_tls($this->ldap_connection);
384
            if ($use_tls == 'require' && $tls_success === false) {
385
                $error = ldap_error($this->ldap_connection);
386
                throw new AuthenticationException("Fatal error: LDAP TLS required but not successfully negotiated: $error");
387
            }
388
        }
389
    }
390
391
    public function bind($credentials = [])
392
    {
393
        if (Config::get('auth_ldap_debug')) {
394
            ldap_set_option(null, LDAP_OPT_DEBUG_LEVEL, 7);
395
        }
396
397
        $this->connect();
398
399
        $username = $credentials['username'] ?? null;
400
        $password = $credentials['password'] ?? null;
401
402
        if ((Config::has('auth_ldap_binduser') || Config::has('auth_ldap_binddn')) && Config::has('auth_ldap_bindpassword')) {
403
            if (Config::get('auth_ldap_binddn') == null) {
404
                Config::set('auth_ldap_binddn', $this->getFullDn(Config::get('auth_ldap_binduser')));
405
            }
406
            $username = Config::get('auth_ldap_binddn');
407
            $password = Config::get('auth_ldap_bindpassword');
408
        } elseif (! empty($credentials['username'])) {
409
            $username = $this->getFullDn($credentials['username']);
410
        }
411
412
        // With specified bind user
413
        ldap_set_option($this->ldap_connection, LDAP_OPT_NETWORK_TIMEOUT, Config::get('auth_ldap_timeout', 5));
414
        $bind_result = ldap_bind($this->ldap_connection, $username, $password);
415
        ldap_set_option($this->ldap_connection, LDAP_OPT_NETWORK_TIMEOUT, -1); // restore timeout
416
417
        if (Config::get('auth_ldap_debug')) {
418
            echo 'Bind result: ' . ldap_error($this->ldap_connection) . PHP_EOL;
419
        }
420
421
        if ($bind_result) {
422
            return;
423
        }
424
425
        // Anonymous
426
        ldap_set_option($this->ldap_connection, LDAP_OPT_NETWORK_TIMEOUT, Config::get('auth_ldap_timeout', 5));
427
        ldap_bind($this->ldap_connection);
428
        ldap_set_option($this->ldap_connection, LDAP_OPT_NETWORK_TIMEOUT, -1); // restore timeout
429
430
        if (Config::get('auth_ldap_debug')) {
431
            echo 'Anonymous bind result: ' . ldap_error($this->ldap_connection) . PHP_EOL;
432
        }
433
    }
434
}
435