Issues (2963)

Authentication/ActiveDirectoryAuthorizer.php (5 issues)

1
<?php
2
3
// easier to rewrite for Active Directory than to bash it into existing LDAP implementation
4
5
// disable certificate checking before connect if required
6
7
namespace LibreNMS\Authentication;
8
9
use LibreNMS\Config;
10
use LibreNMS\Exceptions\AuthenticationException;
11
use LibreNMS\Exceptions\LdapMissingException;
12
13
class ActiveDirectoryAuthorizer extends AuthorizerBase
14
{
15
    use ActiveDirectoryCommon;
16
17
    protected static $CAN_UPDATE_PASSWORDS = false;
18
19
    protected $ldap_connection;
20
    protected $is_bound = false; // this variable tracks if bind has been called so we don't call it multiple times
21
22
    public function authenticate($credentials)
23
    {
24
        $this->connect();
25
26
        if ($this->ldap_connection) {
27
            // bind with sAMAccountName instead of full LDAP DN
28
            if (! empty($credentials['username']) && ! empty($credentials['password']) && ldap_bind($this->ldap_connection, $credentials['username'] . '@' . Config::get('auth_ad_domain'), $credentials['password'])) {
29
                $this->is_bound = true;
30
                // group membership in one of the configured groups is required
31
                if (Config::get('auth_ad_require_groupmembership', true)) {
32
                    // cycle through defined groups, test for memberOf-ship
33
                    foreach (Config::get('auth_ad_groups', []) as $group => $level) {
34
                        if ($this->userInGroup($credentials['username'], $group)) {
35
                            return true;
36
                        }
37
                    }
38
39
                    // failed to find user
40
                    if (Config::get('auth_ad_debug', false)) {
41
                        throw new AuthenticationException('User is not in one of the required groups or user/group is outside the base dn');
42
                    }
43
44
                    throw new AuthenticationException();
45
                } else {
46
                    // group membership is not required and user is valid
47
                    return true;
48
                }
49
            }
50
        }
51
52
        if (empty($credentials['password'])) {
53
            throw new AuthenticationException('A password is required');
54
        } elseif (Config::get('auth_ad_debug', false)) {
55
            ldap_get_option($this->ldap_connection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extended_error);
56
            throw new AuthenticationException(ldap_error($this->ldap_connection) . '<br />' . $extended_error);
57
        }
58
59
        throw new AuthenticationException(ldap_error($this->ldap_connection));
60
    }
61
62
    protected function userInGroup($username, $groupname)
63
    {
64
        $connection = $this->getConnection();
65
66
        // check if user is member of the given group or nested groups
67
        $search_filter = "(&(objectClass=group)(cn=$groupname))";
68
69
        // get DN for auth_ad_group
70
        $search = ldap_search(
71
            $connection,
72
            Config::get('auth_ad_base_dn'),
0 ignored issues
show
It seems like LibreNMS\Config::get('auth_ad_base_dn') can also be of type null; however, parameter $base of ldap_search() does only seem to accept array|string, 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

72
            /** @scrutinizer ignore-type */ Config::get('auth_ad_base_dn'),
Loading history...
73
            $search_filter,
74
            ['cn']
75
        );
76
        $result = ldap_get_entries($connection, $search);
77
78
        if ($result == false || $result['count'] !== 1) {
79
            if (Config::get('auth_ad_debug', false)) {
80
                if ($result == false) {
81
                    // FIXME: what went wrong?
82
                    throw new AuthenticationException("LDAP query failed for group '$groupname' using filter '$search_filter'");
83
                } elseif ($result['count'] == 0) {
84
                    throw new AuthenticationException("Failed to find group matching '$groupname' using filter '$search_filter'");
85
                } elseif ($result['count'] > 1) {
86
                    throw new AuthenticationException("Multiple groups returned for '$groupname' using filter '$search_filter'");
87
                }
88
            }
89
90
            throw new AuthenticationException();
91
        }
92
93
        // special character handling
94
        $group_dn = addcslashes($result[0]['dn'], '()#');
95
96
        $search = ldap_search(
97
            $connection,
98
            Config::get('auth_ad_base_dn'),
99
            // add 'LDAP_MATCHING_RULE_IN_CHAIN to the user filter to search for $username in nested $group_dn
100
            // limiting to "DN" for shorter array
101
            '(&' . $this->userFilter($username) . "(memberOf:1.2.840.113556.1.4.1941:=$group_dn))",
102
            ['DN']
103
        );
104
        $entries = ldap_get_entries($connection, $search);
105
106
        return $entries['count'] > 0;
107
    }
108
109
    public function userExists($username, $throw_exception = false)
110
    {
111
        $connection = $this->getConnection();
112
113
        $search = ldap_search(
114
            $connection,
115
            Config::get('auth_ad_base_dn'),
0 ignored issues
show
It seems like LibreNMS\Config::get('auth_ad_base_dn') can also be of type null; however, parameter $base of ldap_search() does only seem to accept array|string, 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

115
            /** @scrutinizer ignore-type */ Config::get('auth_ad_base_dn'),
Loading history...
116
            $this->userFilter($username),
117
            ['samaccountname']
118
        );
119
        $entries = ldap_get_entries($connection, $search);
120
121
        if ($entries['count']) {
122
            return true;
123
        }
124
125
        return false;
126
    }
127
128
    public function getUserlevel($username)
129
    {
130
        $userlevel = 0;
131
        if (! Config::get('auth_ad_require_groupmembership', true)) {
132
            if (Config::get('auth_ad_global_read', false)) {
133
                $userlevel = 5;
134
            }
135
        }
136
137
        // cycle through defined groups, test for memberOf-ship
138
        foreach (Config::get('auth_ad_groups', []) as $group => $level) {
139
            try {
140
                if ($this->userInGroup($username, $group)) {
141
                    $userlevel = max($userlevel, $level['level']);
142
                }
143
            } catch (AuthenticationException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
144
            }
145
        }
146
147
        return $userlevel;
148
    }
149
150
    public function getUserid($username)
151
    {
152
        $connection = $this->getConnection();
153
154
        $attributes = ['objectsid'];
155
        $search = ldap_search(
156
            $connection,
157
            Config::get('auth_ad_base_dn'),
0 ignored issues
show
It seems like LibreNMS\Config::get('auth_ad_base_dn') can also be of type null; however, parameter $base of ldap_search() does only seem to accept array|string, 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

157
            /** @scrutinizer ignore-type */ Config::get('auth_ad_base_dn'),
Loading history...
158
            $this->userFilter($username),
159
            $attributes
160
        );
161
        $entries = ldap_get_entries($connection, $search);
162
163
        if ($entries['count']) {
164
            return $this->getUseridFromSid($this->sidFromLdap($entries[0]['objectsid'][0]));
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getUseridF...es[0]['objectsid'][0])) returns the type string which is incompatible with the return type mandated by LibreNMS\Interfaces\Auth...Authorizer::getUserid() of integer.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
165
        }
166
167
        return -1;
168
    }
169
170
    /**
171
     * Bind to AD with the bind user if available, otherwise anonymous bind
172
     */
173
    protected function init()
174
    {
175
        if ($this->ldap_connection) {
176
            return;
177
        }
178
179
        $this->connect();
180
        $this->bind();
181
    }
182
183
    protected function connect()
184
    {
185
        if ($this->ldap_connection) {
186
            // no need to re-connect
187
            return;
188
        }
189
190
        if (! function_exists('ldap_connect')) {
191
            throw new LdapMissingException();
192
        }
193
194
        if (Config::has('auth_ad_check_certificates') &&
195
            ! Config::get('auth_ad_check_certificates')) {
196
            putenv('LDAPTLS_REQCERT=never');
197
        }
198
199
        if (Config::has('auth_ad_check_certificates') && Config::get('auth_ad_debug')) {
200
            ldap_set_option(null, LDAP_OPT_DEBUG_LEVEL, 7);
201
        }
202
203
        $this->ldap_connection = @ldap_connect(Config::get('auth_ad_url'));
204
205
        // disable referrals and force ldap version to 3
206
        ldap_set_option($this->ldap_connection, LDAP_OPT_REFERRALS, 0);
207
        ldap_set_option($this->ldap_connection, LDAP_OPT_PROTOCOL_VERSION, 3);
208
    }
209
210
    public function bind($credentials = [])
211
    {
212
        if (! $this->ldap_connection) {
213
            $this->connect();
214
        }
215
216
        $username = $credentials['username'] ?? null;
217
        $password = $credentials['password'] ?? null;
218
219
        if (Config::has('auth_ad_binduser') && Config::has('auth_ad_bindpassword')) {
220
            $username = Config::get('auth_ad_binduser');
221
            $password = Config::get('auth_ad_bindpassword');
222
        }
223
        $username .= '@' . Config::get('auth_ad_domain');
224
225
        ldap_set_option($this->ldap_connection, LDAP_OPT_NETWORK_TIMEOUT, Config::get('auth_ad_timeout', 5));
226
        $bind_result = ldap_bind($this->ldap_connection, $username, $password);
227
        ldap_set_option($this->ldap_connection, LDAP_OPT_NETWORK_TIMEOUT, -1); // restore timeout
228
229
        if ($bind_result) {
230
            return $bind_result;
231
        }
232
233
        ldap_set_option($this->ldap_connection, LDAP_OPT_NETWORK_TIMEOUT, Config::get('auth_ad_timeout', 5));
234
        ldap_bind($this->ldap_connection);
235
        ldap_set_option($this->ldap_connection, LDAP_OPT_NETWORK_TIMEOUT, -1); // restore timeout
236
    }
237
238
    protected function getConnection()
239
    {
240
        $this->init(); // make sure connected and bound
241
242
        return $this->ldap_connection;
243
    }
244
}
245