This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | namespace Integrations\Services; |
||
4 | |||
5 | use Porteiro\Models\Role; |
||
6 | use App\Models\User; |
||
7 | use Illuminate\Contracts\Auth\Authenticatable; |
||
8 | use Illuminate\Database\Eloquent\Builder; |
||
9 | use Integrations\Exceptions\LdapException; |
||
10 | use Integrations\Models\Access; |
||
11 | use Integrations\Models\UserRepo; |
||
12 | |||
13 | /** |
||
14 | * Class LdapService |
||
15 | * Handles any app-specific LDAP tasks. |
||
16 | * |
||
17 | * @package Integrations\Services |
||
18 | */ |
||
19 | class LdapService |
||
20 | { |
||
21 | protected $ldap; |
||
22 | protected $ldapConnection; |
||
23 | protected $config; |
||
24 | protected $userRepo; |
||
25 | protected $enabled; |
||
26 | |||
27 | /** |
||
28 | * LdapService constructor. |
||
29 | * |
||
30 | * @param Ldap $ldap |
||
31 | * @param \Integrations\Models\UserRepo $userRepo |
||
32 | */ |
||
33 | public function __construct(Access\Ldap $ldap, UserRepo $userRepo) |
||
34 | { |
||
35 | $this->ldap = $ldap; |
||
36 | $this->config = config('services.ldap'); |
||
37 | $this->userRepo = $userRepo; |
||
38 | $this->enabled = config('auth.method') === 'ldap'; |
||
39 | } |
||
40 | |||
41 | /** |
||
42 | * Check if groups should be synced. |
||
43 | * |
||
44 | * @return bool |
||
45 | */ |
||
46 | public function shouldSyncGroups() |
||
47 | { |
||
48 | return $this->enabled && $this->config['user_to_groups'] !== false; |
||
49 | } |
||
50 | |||
51 | /** |
||
52 | * Search for attributes for a specific user on the ldap |
||
53 | * |
||
54 | * @param string $userName |
||
55 | * @param array $attributes |
||
56 | * @return null|array |
||
57 | * @throws LdapException |
||
58 | */ |
||
59 | private function getUserWithAttributes($userName, $attributes) |
||
60 | { |
||
61 | $ldapConnection = $this->getConnection(); |
||
62 | $this->bindSystemUser($ldapConnection); |
||
63 | |||
64 | // Find user |
||
65 | $userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]); |
||
66 | $baseDn = $this->config['base_dn']; |
||
67 | |||
68 | $followReferrals = $this->config['follow_referrals'] ? 1 : 0; |
||
69 | $this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals); |
||
70 | $users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, $attributes); |
||
71 | if ($users['count'] === 0) { |
||
72 | return null; |
||
73 | } |
||
74 | |||
75 | return $users[0]; |
||
76 | } |
||
77 | |||
78 | /** |
||
79 | * Get the details of a user from LDAP using the given username. |
||
80 | * User found via configurable user filter. |
||
81 | * |
||
82 | * @param $userName |
||
83 | * @return array|null |
||
84 | * @throws LdapException |
||
85 | */ |
||
86 | public function getUserDetails($userName) |
||
87 | { |
||
88 | $emailAttr = $this->config['email_attribute']; |
||
89 | $displayNameAttr = $this->config['display_name_attribute']; |
||
90 | |||
91 | $user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr, $displayNameAttr]); |
||
92 | |||
93 | if ($user === null) { |
||
94 | return null; |
||
95 | } |
||
96 | |||
97 | $userCn = $this->getUserResponseProperty($user, 'cn', null); |
||
98 | return [ |
||
99 | 'uid' => $this->getUserResponseProperty($user, 'uid', $user['dn']), |
||
100 | 'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn), |
||
101 | 'dn' => $user['dn'], |
||
102 | 'email' => $this->getUserResponseProperty($user, $emailAttr, null), |
||
103 | ]; |
||
104 | } |
||
105 | |||
106 | /** |
||
107 | * Get a property from an LDAP user response fetch. |
||
108 | * Handles properties potentially being part of an array. |
||
109 | * |
||
110 | * @param array $userDetails |
||
111 | * @param string $propertyKey |
||
112 | * @param $defaultValue |
||
113 | * @return mixed |
||
114 | */ |
||
115 | protected function getUserResponseProperty(array $userDetails, string $propertyKey, $defaultValue) |
||
116 | { |
||
117 | if (isset($userDetails[$propertyKey])) { |
||
118 | return (is_array($userDetails[$propertyKey]) ? $userDetails[$propertyKey][0] : $userDetails[$propertyKey]); |
||
119 | } |
||
120 | |||
121 | return $defaultValue; |
||
122 | } |
||
123 | |||
124 | /** |
||
125 | * @param Authenticatable $user |
||
126 | * @param string $username |
||
127 | * @param string $password |
||
128 | * @return bool |
||
129 | * @throws LdapException |
||
130 | */ |
||
131 | public function validateUserCredentials(Authenticatable $user, $username, $password) |
||
132 | { |
||
133 | $ldapUser = $this->getUserDetails($username); |
||
134 | if ($ldapUser === null) { |
||
135 | return false; |
||
136 | } |
||
137 | |||
138 | if ($ldapUser['uid'] !== $user->external_auth_id) { |
||
0 ignored issues
–
show
|
|||
139 | return false; |
||
140 | } |
||
141 | |||
142 | $ldapConnection = $this->getConnection(); |
||
143 | try { |
||
144 | $ldapBind = $this->ldap->bind($ldapConnection, $ldapUser['dn'], $password); |
||
145 | } catch (\ErrorException $e) { |
||
0 ignored issues
–
show
The class
ErrorException does not exist. Did you forget a USE statement, or did you not list all dependencies?
Scrutinizer analyzes your It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis. ![]() |
|||
146 | $ldapBind = false; |
||
147 | } |
||
148 | |||
149 | return $ldapBind; |
||
150 | } |
||
151 | |||
152 | /** |
||
153 | * Bind the system user to the LDAP connection using the given credentials |
||
154 | * otherwise anonymous access is attempted. |
||
155 | * |
||
156 | * @param $connection |
||
157 | * @throws LdapException |
||
158 | */ |
||
159 | protected function bindSystemUser($connection) |
||
160 | { |
||
161 | $ldapDn = $this->config['dn']; |
||
162 | $ldapPass = $this->config['pass']; |
||
163 | |||
164 | $isAnonymous = ($ldapDn === false || $ldapPass === false); |
||
165 | if ($isAnonymous) { |
||
166 | $ldapBind = $this->ldap->bind($connection); |
||
167 | } else { |
||
168 | $ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass); |
||
169 | } |
||
170 | |||
171 | if (!$ldapBind) { |
||
172 | throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed'))); |
||
173 | } |
||
174 | } |
||
175 | |||
176 | /** |
||
177 | * Get the connection to the LDAP server. |
||
178 | * Creates a new connection if one does not exist. |
||
179 | * |
||
180 | * @return resource |
||
181 | * @throws LdapException |
||
182 | */ |
||
183 | protected function getConnection() |
||
184 | { |
||
185 | if ($this->ldapConnection !== null) { |
||
186 | return $this->ldapConnection; |
||
187 | } |
||
188 | |||
189 | // Check LDAP extension in installed |
||
190 | if (!function_exists('ldap_connect') && config('app.env') !== 'testing') { |
||
191 | throw new LdapException(trans('errors.ldap_extension_not_installed')); |
||
192 | } |
||
193 | |||
194 | // Get port from server string and protocol if specified. |
||
195 | $ldapServer = explode(':', $this->config['server']); |
||
196 | $hasProtocol = preg_match('/^ldaps{0,1}\:\/\//', $this->config['server']) === 1; |
||
197 | if (!$hasProtocol) { |
||
198 | array_unshift($ldapServer, ''); |
||
199 | } |
||
200 | $hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1]; |
||
201 | $defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389; |
||
202 | |||
203 | /* |
||
204 | * Check if TLS_INSECURE is set. The handle is set to NULL due to the nature of |
||
205 | * the LDAP_OPT_X_TLS_REQUIRE_CERT option. It can only be set globally and not |
||
206 | * per handle. |
||
207 | */ |
||
208 | if ($this->config['tls_insecure']) { |
||
209 | $this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER); |
||
210 | } |
||
211 | |||
212 | $ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort); |
||
213 | |||
214 | if ($ldapConnection === false) { |
||
215 | throw new LdapException(trans('errors.ldap_cannot_connect')); |
||
216 | } |
||
217 | |||
218 | // Set any required options |
||
219 | if ($this->config['version']) { |
||
220 | $this->ldap->setVersion($ldapConnection, $this->config['version']); |
||
221 | } |
||
222 | |||
223 | $this->ldapConnection = $ldapConnection; |
||
224 | return $this->ldapConnection; |
||
225 | } |
||
226 | |||
227 | /** |
||
228 | * Build a filter string by injecting common variables. |
||
229 | * |
||
230 | * @param string $filterString |
||
231 | * @param array $attrs |
||
232 | * @return string |
||
233 | */ |
||
234 | protected function buildFilter($filterString, array $attrs) |
||
235 | { |
||
236 | $newAttrs = []; |
||
237 | foreach ($attrs as $key => $attrText) { |
||
238 | $newKey = '${' . $key . '}'; |
||
239 | $newAttrs[$newKey] = $this->ldap->escape($attrText); |
||
240 | } |
||
241 | return strtr($filterString, $newAttrs); |
||
242 | } |
||
243 | |||
244 | /** |
||
245 | * Get the groups a user is a part of on ldap |
||
246 | * |
||
247 | * @param string $userName |
||
248 | * @return array |
||
249 | * @throws LdapException |
||
250 | */ |
||
251 | public function getUserGroups($userName) |
||
252 | { |
||
253 | $groupsAttr = $this->config['group_attribute']; |
||
254 | $user = $this->getUserWithAttributes($userName, [$groupsAttr]); |
||
255 | |||
256 | if ($user === null) { |
||
257 | return []; |
||
258 | } |
||
259 | |||
260 | $userGroups = $this->groupFilter($user); |
||
261 | $userGroups = $this->getGroupsRecursive($userGroups, []); |
||
262 | return $userGroups; |
||
263 | } |
||
264 | |||
265 | /** |
||
266 | * Get the parent groups of an array of groups |
||
267 | * |
||
268 | * @param array $groupsArray |
||
269 | * @param array $checked |
||
270 | * @return array |
||
271 | * @throws LdapException |
||
272 | */ |
||
273 | private function getGroupsRecursive($groupsArray, $checked) |
||
274 | { |
||
275 | $groups_to_add = []; |
||
276 | foreach ($groupsArray as $groupName) { |
||
277 | if (in_array($groupName, $checked)) { |
||
278 | continue; |
||
279 | } |
||
280 | |||
281 | $groupsToAdd = $this->getGroupGroups($groupName); |
||
282 | $groups_to_add = array_merge($groups_to_add, $groupsToAdd); |
||
283 | $checked[] = $groupName; |
||
284 | } |
||
285 | $groupsArray = array_unique(array_merge($groupsArray, $groups_to_add), SORT_REGULAR); |
||
286 | |||
287 | if (!empty($groups_to_add)) { |
||
288 | return $this->getGroupsRecursive($groupsArray, $checked); |
||
289 | } else { |
||
290 | return $groupsArray; |
||
291 | } |
||
292 | } |
||
293 | |||
294 | /** |
||
295 | * Get the parent groups of a single group |
||
296 | * |
||
297 | * @param string $groupName |
||
298 | * @return array |
||
299 | * @throws LdapException |
||
300 | */ |
||
301 | private function getGroupGroups($groupName) |
||
302 | { |
||
303 | $ldapConnection = $this->getConnection(); |
||
304 | $this->bindSystemUser($ldapConnection); |
||
305 | |||
306 | $followReferrals = $this->config['follow_referrals'] ? 1 : 0; |
||
307 | $this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals); |
||
308 | |||
309 | $baseDn = $this->config['base_dn']; |
||
310 | $groupsAttr = strtolower($this->config['group_attribute']); |
||
311 | |||
312 | $groupFilter = 'CN=' . $this->ldap->escape($groupName); |
||
313 | $groups = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $groupFilter, [$groupsAttr]); |
||
314 | if ($groups['count'] === 0) { |
||
315 | return []; |
||
316 | } |
||
317 | |||
318 | $groupGroups = $this->groupFilter($groups[0]); |
||
319 | return $groupGroups; |
||
320 | } |
||
321 | |||
322 | /** |
||
323 | * Filter out LDAP CN and DN language in a ldap search return |
||
324 | * Gets the base CN (common name) of the string |
||
325 | * |
||
326 | * @param array $userGroupSearchResponse |
||
327 | * @return array |
||
328 | */ |
||
329 | protected function groupFilter(array $userGroupSearchResponse) |
||
330 | { |
||
331 | $groupsAttr = strtolower($this->config['group_attribute']); |
||
332 | $ldapGroups = []; |
||
333 | $count = 0; |
||
334 | |||
335 | if (isset($userGroupSearchResponse[$groupsAttr]['count'])) { |
||
336 | $count = (int) $userGroupSearchResponse[$groupsAttr]['count']; |
||
337 | } |
||
338 | |||
339 | for ($i=0; $i<$count; $i++) { |
||
340 | $dnComponents = $this->ldap->explodeDn($userGroupSearchResponse[$groupsAttr][$i], 1); |
||
341 | if (!in_array($dnComponents[0], $ldapGroups)) { |
||
342 | $ldapGroups[] = $dnComponents[0]; |
||
343 | } |
||
344 | } |
||
345 | |||
346 | return $ldapGroups; |
||
347 | } |
||
348 | |||
349 | /** |
||
350 | * Sync the LDAP groups to the user roles for the current user |
||
351 | * |
||
352 | * @param \App\Models\User $user |
||
353 | * @param string $username |
||
354 | * @throws LdapException |
||
355 | */ |
||
356 | public function syncGroups(User $user, string $username) |
||
357 | { |
||
358 | $userLdapGroups = $this->getUserGroups($username); |
||
359 | |||
360 | // Get the ids for the roles from the names |
||
361 | $ldapGroupsAsRoles = $this->matchLdapGroupsToSystemsRoles($userLdapGroups); |
||
362 | |||
363 | // Sync groups |
||
364 | if ($this->config['remove_from_groups']) { |
||
365 | $user->roles()->sync($ldapGroupsAsRoles); |
||
366 | $this->userRepo->attachDefaultRole($user); |
||
367 | } else { |
||
368 | $user->roles()->syncWithoutDetaching($ldapGroupsAsRoles); |
||
369 | } |
||
370 | } |
||
371 | |||
372 | /** |
||
373 | * Match an array of group names from LDAP to App system roles. |
||
374 | * Formats LDAP group names to be lower-case and hyphenated. |
||
375 | * |
||
376 | * @param array $groupNames |
||
377 | * @return \Illuminate\Support\Collection |
||
378 | */ |
||
379 | protected function matchLdapGroupsToSystemsRoles(array $groupNames) |
||
380 | { |
||
381 | foreach ($groupNames as $i => $groupName) { |
||
382 | $groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName))); |
||
383 | } |
||
384 | |||
385 | $roles = Role::query()->where( |
||
386 | function (Builder $query) use ($groupNames) { |
||
387 | $query->whereIn('name', $groupNames); |
||
388 | foreach ($groupNames as $groupName) { |
||
389 | $query->orWhere('external_auth_id', 'LIKE', '%' . $groupName . '%'); |
||
390 | } |
||
391 | } |
||
392 | )->get(); |
||
393 | |||
394 | $matchedRoles = $roles->filter( |
||
395 | function (Role $role) use ($groupNames) { |
||
396 | return $this->roleMatchesGroupNames($role, $groupNames); |
||
397 | } |
||
398 | ); |
||
399 | |||
400 | return $matchedRoles->pluck('id'); |
||
401 | } |
||
402 | |||
403 | /** |
||
404 | * Check a role against an array of group names to see if it matches. |
||
405 | * Checked against role 'external_auth_id' if set otherwise the name of the role. |
||
406 | * |
||
407 | * @param \Porteiro\Models\Role $role |
||
408 | * @param array $groupNames |
||
409 | * @return bool |
||
410 | */ |
||
411 | protected function roleMatchesGroupNames(Role $role, array $groupNames) |
||
412 | { |
||
413 | if ($role->external_auth_id) { |
||
414 | $externalAuthIds = explode(',', strtolower($role->external_auth_id)); |
||
415 | foreach ($externalAuthIds as $externalAuthId) { |
||
416 | if (in_array(trim($externalAuthId), $groupNames)) { |
||
417 | return true; |
||
418 | } |
||
419 | } |
||
420 | return false; |
||
421 | } |
||
422 | |||
423 | $roleName = str_replace(' ', '-', trim(strtolower($role->display_name))); |
||
424 | return in_array($roleName, $groupNames); |
||
425 | } |
||
426 | } |
||
427 |
If you access a property on an interface, you most likely code against a concrete implementation of the interface.
Available Fixes
Adding an additional type check:
Changing the type hint: