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
Bug
introduced
by
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
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
|
|||||
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
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
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 |