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'), |
||
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])); |
||
0 ignored issues
–
show
|
|||
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 |
In the issue above, the returned value is violating the contract defined by the mentioned interface.
Let's take a look at an example: