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
7namespace LibreNMS\Authentication;
8
9use LibreNMS\Config;
10use LibreNMS\Exceptions\AuthenticationException;
11use LibreNMS\Exceptions\LdapMissingException;
12
13class 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'),
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'),
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) {
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'),
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]));
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