| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | <?php | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-18 00:56:55 +08:00
										 |  |  | namespace BookStack\Access; | 
					
						
							| 
									
										
										
										
											2016-01-10 03:23:35 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-16 04:31:23 +08:00
										 |  |  | use BookStack\Exceptions\JsonDebugException; | 
					
						
							| 
									
										
										
										
											2018-09-25 23:58:03 +08:00
										 |  |  | use BookStack\Exceptions\LdapException; | 
					
						
							| 
									
										
										
										
											2021-05-25 01:45:08 +08:00
										 |  |  | use BookStack\Uploads\UserAvatars; | 
					
						
							| 
									
										
										
										
											2023-05-18 00:56:55 +08:00
										 |  |  | use BookStack\Users\Models\User; | 
					
						
							| 
									
										
										
										
											2019-12-16 20:38:35 +08:00
										 |  |  | use ErrorException; | 
					
						
							| 
									
										
										
										
											2021-05-25 01:45:08 +08:00
										 |  |  | use Illuminate\Support\Facades\Log; | 
					
						
							| 
									
										
										
										
											2016-01-10 03:23:35 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-01-16 07:21:47 +08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Class LdapService | 
					
						
							|  |  |  |  * Handles any app-specific LDAP tasks. | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2021-10-07 06:05:26 +08:00
										 |  |  | class LdapService | 
					
						
							| 
									
										
										
										
											2016-01-10 03:23:35 +08:00
										 |  |  | { | 
					
						
							| 
									
										
										
										
											2022-03-24 00:34:23 +08:00
										 |  |  |     /** | 
					
						
							| 
									
										
										
										
											2023-05-30 20:10:05 +08:00
										 |  |  |      * @var resource|\LDAP\Connection | 
					
						
							| 
									
										
										
										
											2022-03-24 00:34:23 +08:00
										 |  |  |      */ | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |     protected $ldapConnection; | 
					
						
							| 
									
										
										
										
											2022-03-24 00:34:23 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     protected array $config; | 
					
						
							|  |  |  |     protected bool $enabled; | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-30 20:10:05 +08:00
										 |  |  |     public function __construct( | 
					
						
							|  |  |  |         protected Ldap $ldap, | 
					
						
							|  |  |  |         protected UserAvatars $userAvatars, | 
					
						
							|  |  |  |         protected GroupSyncService $groupSyncService | 
					
						
							|  |  |  |     ) { | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |         $this->config = config('services.ldap'); | 
					
						
							| 
									
										
										
										
											2018-07-16 01:21:45 +08:00
										 |  |  |         $this->enabled = config('auth.method') === 'ldap'; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Check if groups should be synced. | 
					
						
							|  |  |  |      */ | 
					
						
							| 
									
										
										
										
											2021-10-07 06:05:26 +08:00
										 |  |  |     public function shouldSyncGroups(): bool | 
					
						
							| 
									
										
										
										
											2018-07-16 01:21:45 +08:00
										 |  |  |     { | 
					
						
							|  |  |  |         return $this->enabled && $this->config['user_to_groups'] !== false; | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							| 
									
										
										
										
											2019-12-16 20:38:35 +08:00
										 |  |  |      * Search for attributes for a specific user on the ldap. | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |      * | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |      * @throws LdapException | 
					
						
							|  |  |  |      */ | 
					
						
							| 
									
										
										
										
											2019-12-16 20:38:35 +08:00
										 |  |  |     private function getUserWithAttributes(string $userName, array $attributes): ?array | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |     { | 
					
						
							|  |  |  |         $ldapConnection = $this->getConnection(); | 
					
						
							|  |  |  |         $this->bindSystemUser($ldapConnection); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-16 06:35:15 +08:00
										 |  |  |         // Clean attributes
 | 
					
						
							|  |  |  |         foreach ($attributes as $index => $attribute) { | 
					
						
							| 
									
										
										
										
											2023-05-30 20:10:05 +08:00
										 |  |  |             if (str_starts_with($attribute, 'BIN;')) { | 
					
						
							| 
									
										
										
										
											2020-02-16 06:35:15 +08:00
										 |  |  |                 $attributes[$index] = substr($attribute, strlen('BIN;')); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |         // Find user
 | 
					
						
							|  |  |  |         $userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]); | 
					
						
							|  |  |  |         $baseDn = $this->config['base_dn']; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $followReferrals = $this->config['follow_referrals'] ? 1 : 0; | 
					
						
							|  |  |  |         $this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals); | 
					
						
							|  |  |  |         $users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, $attributes); | 
					
						
							|  |  |  |         if ($users['count'] === 0) { | 
					
						
							|  |  |  |             return null; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return $users[0]; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-29 05:14:30 +08:00
										 |  |  |     /** | 
					
						
							| 
									
										
										
										
											2024-12-02 02:42:54 +08:00
										 |  |  |      * Build the user display name from the (potentially multiple) attributes defined by the configuration. | 
					
						
							| 
									
										
										
										
											2024-10-29 05:14:30 +08:00
										 |  |  |      */ | 
					
						
							| 
									
										
										
										
											2024-12-02 02:42:54 +08:00
										 |  |  |     protected function getUserDisplayName(array $userDetails, array $displayNameAttrs, string $defaultValue): string | 
					
						
							| 
									
										
										
										
											2024-10-29 05:14:30 +08:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2024-12-02 02:42:54 +08:00
										 |  |  |         $displayNameParts = []; | 
					
						
							|  |  |  |         foreach ($displayNameAttrs as $dnAttr) { | 
					
						
							| 
									
										
										
										
											2024-10-29 05:14:30 +08:00
										 |  |  |             $dnComponent = $this->getUserResponseProperty($userDetails, $dnAttr, null); | 
					
						
							| 
									
										
										
										
											2024-12-02 02:42:54 +08:00
										 |  |  |             if ($dnComponent) { | 
					
						
							|  |  |  |                 $displayNameParts[] = $dnComponent; | 
					
						
							| 
									
										
										
										
											2024-10-29 05:14:30 +08:00
										 |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-02 02:42:54 +08:00
										 |  |  |         if (empty($displayNameParts)) { | 
					
						
							|  |  |  |             return $defaultValue; | 
					
						
							| 
									
										
										
										
											2024-10-29 05:14:30 +08:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-02 02:42:54 +08:00
										 |  |  |         return implode(' ', $displayNameParts); | 
					
						
							| 
									
										
										
										
											2024-10-29 05:14:30 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Get the details of a user from LDAP using the given username. | 
					
						
							|  |  |  |      * User found via configurable user filter. | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |      * | 
					
						
							| 
									
										
										
										
											2023-05-30 20:10:05 +08:00
										 |  |  |      * @throws LdapException|JsonDebugException | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |      */ | 
					
						
							| 
									
										
										
										
											2019-12-16 20:38:35 +08:00
										 |  |  |     public function getUserDetails(string $userName): ?array | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2019-12-16 20:38:35 +08:00
										 |  |  |         $idAttr = $this->config['id_attribute']; | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |         $emailAttr = $this->config['email_attribute']; | 
					
						
							| 
									
										
										
										
											2024-12-02 02:42:54 +08:00
										 |  |  |         $displayNameAttrs = explode('|', $this->config['display_name_attribute']); | 
					
						
							| 
									
										
										
										
											2020-10-13 00:47:36 +08:00
										 |  |  |         $thumbnailAttr = $this->config['thumbnail_attribute']; | 
					
						
							| 
									
										
										
										
											2019-03-09 12:55:11 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-02 02:42:54 +08:00
										 |  |  |         $user = $this->getUserWithAttributes($userName, array_filter([ | 
					
						
							|  |  |  |             'cn', 'dn', $idAttr, $emailAttr, ...$displayNameAttrs, $thumbnailAttr, | 
					
						
							|  |  |  |         ])); | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-25 01:45:08 +08:00
										 |  |  |         if (is_null($user)) { | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |             return null; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-20 21:06:49 +08:00
										 |  |  |         $nameDefault = $this->getUserResponseProperty($user, 'cn', null); | 
					
						
							|  |  |  |         if (is_null($nameDefault)) { | 
					
						
							|  |  |  |             $nameDefault = ldap_explode_dn($user['dn'], 1)[0] ?? $user['dn']; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-16 04:31:23 +08:00
										 |  |  |         $formatted = [ | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |             'uid'   => $this->getUserResponseProperty($user, $idAttr, $user['dn']), | 
					
						
							| 
									
										
										
										
											2025-02-20 21:06:49 +08:00
										 |  |  |             'name'  => $this->getUserDisplayName($user, $displayNameAttrs, $nameDefault), | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |             'dn'    => $user['dn'], | 
					
						
							| 
									
										
										
										
											2019-03-10 18:54:19 +08:00
										 |  |  |             'email' => $this->getUserResponseProperty($user, $emailAttr, null), | 
					
						
							| 
									
										
										
										
											2022-09-18 08:25:20 +08:00
										 |  |  |             'avatar' => $thumbnailAttr ? $this->getUserResponseProperty($user, $thumbnailAttr, null) : null, | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |         ]; | 
					
						
							| 
									
										
										
										
											2020-02-16 04:31:23 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         if ($this->config['dump_user_details']) { | 
					
						
							|  |  |  |             throw new JsonDebugException([ | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |                 'details_from_ldap'        => $user, | 
					
						
							| 
									
										
										
										
											2020-02-16 04:31:23 +08:00
										 |  |  |                 'details_bookstack_parsed' => $formatted, | 
					
						
							|  |  |  |             ]); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return $formatted; | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-03-10 18:54:19 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Get a property from an LDAP user response fetch. | 
					
						
							|  |  |  |      * Handles properties potentially being part of an array. | 
					
						
							| 
									
										
										
										
											2020-02-16 04:31:23 +08:00
										 |  |  |      * If the given key is prefixed with 'BIN;', that indicator will be stripped | 
					
						
							|  |  |  |      * from the key and any fetched values will be converted from binary to hex. | 
					
						
							| 
									
										
										
										
											2019-03-10 18:54:19 +08:00
										 |  |  |      */ | 
					
						
							|  |  |  |     protected function getUserResponseProperty(array $userDetails, string $propertyKey, $defaultValue) | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2023-05-30 20:10:05 +08:00
										 |  |  |         $isBinary = str_starts_with($propertyKey, 'BIN;'); | 
					
						
							| 
									
										
										
										
											2019-12-16 20:38:35 +08:00
										 |  |  |         $propertyKey = strtolower($propertyKey); | 
					
						
							| 
									
										
										
										
											2020-02-16 04:31:23 +08:00
										 |  |  |         $value = $defaultValue; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if ($isBinary) { | 
					
						
							|  |  |  |             $propertyKey = substr($propertyKey, strlen('BIN;')); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-03-10 18:54:19 +08:00
										 |  |  |         if (isset($userDetails[$propertyKey])) { | 
					
						
							| 
									
										
										
										
											2020-02-16 04:31:23 +08:00
										 |  |  |             $value = (is_array($userDetails[$propertyKey]) ? $userDetails[$propertyKey][0] : $userDetails[$propertyKey]); | 
					
						
							|  |  |  |             if ($isBinary) { | 
					
						
							|  |  |  |                 $value = bin2hex($value); | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2019-03-10 18:54:19 +08:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-16 04:31:23 +08:00
										 |  |  |         return $value; | 
					
						
							| 
									
										
										
										
											2019-03-10 18:54:19 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |     /** | 
					
						
							| 
									
										
										
										
											2019-12-16 20:38:35 +08:00
										 |  |  |      * Check if the given credentials are valid for the given user. | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |      * | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |      * @throws LdapException | 
					
						
							|  |  |  |      */ | 
					
						
							| 
									
										
										
										
											2020-02-15 22:44:36 +08:00
										 |  |  |     public function validateUserCredentials(?array $ldapUserDetails, string $password): bool | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2020-02-15 22:44:36 +08:00
										 |  |  |         if (is_null($ldapUserDetails)) { | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |             return false; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $ldapConnection = $this->getConnection(); | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |         try { | 
					
						
							| 
									
										
										
										
											2020-02-01 19:42:22 +08:00
										 |  |  |             $ldapBind = $this->ldap->bind($ldapConnection, $ldapUserDetails['dn'], $password); | 
					
						
							| 
									
										
										
										
											2019-12-16 20:38:35 +08:00
										 |  |  |         } catch (ErrorException $e) { | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |             $ldapBind = false; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return $ldapBind; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Bind the system user to the LDAP connection using the given credentials | 
					
						
							|  |  |  |      * otherwise anonymous access is attempted. | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |      * | 
					
						
							| 
									
										
										
										
											2023-05-30 20:10:05 +08:00
										 |  |  |      * @param resource|\LDAP\Connection $connection | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |      * | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |      * @throws LdapException | 
					
						
							|  |  |  |      */ | 
					
						
							| 
									
										
										
										
											2023-05-30 20:10:05 +08:00
										 |  |  |     protected function bindSystemUser($connection): void | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |     { | 
					
						
							|  |  |  |         $ldapDn = $this->config['dn']; | 
					
						
							|  |  |  |         $ldapPass = $this->config['pass']; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $isAnonymous = ($ldapDn === false || $ldapPass === false); | 
					
						
							|  |  |  |         if ($isAnonymous) { | 
					
						
							|  |  |  |             $ldapBind = $this->ldap->bind($connection); | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |             $ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (!$ldapBind) { | 
					
						
							|  |  |  |             throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed'))); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Get the connection to the LDAP server. | 
					
						
							|  |  |  |      * Creates a new connection if one does not exist. | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |      * | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |      * @throws LdapException | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |      * | 
					
						
							| 
									
										
										
										
											2023-05-30 20:10:05 +08:00
										 |  |  |      * @return resource|\LDAP\Connection | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |      */ | 
					
						
							|  |  |  |     protected function getConnection() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         if ($this->ldapConnection !== null) { | 
					
						
							|  |  |  |             return $this->ldapConnection; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Check LDAP extension in installed
 | 
					
						
							|  |  |  |         if (!function_exists('ldap_connect') && config('app.env') !== 'testing') { | 
					
						
							|  |  |  |             throw new LdapException(trans('errors.ldap_extension_not_installed')); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-08 04:00:04 +08:00
										 |  |  |         // Disable certificate verification.
 | 
					
						
							|  |  |  |         // This option works globally and must be set before a connection is created.
 | 
					
						
							|  |  |  |         if ($this->config['tls_insecure']) { | 
					
						
							|  |  |  |             $this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-03 05:56:51 +08:00
										 |  |  |         // Configure any user-provided CA cert files for LDAP.
 | 
					
						
							| 
									
										
										
										
											2024-03-27 00:30:04 +08:00
										 |  |  |         // This option works globally and must be set before a connection is created.
 | 
					
						
							| 
									
										
										
										
											2024-05-03 05:56:51 +08:00
										 |  |  |         if ($this->config['tls_ca_cert']) { | 
					
						
							|  |  |  |             $this->configureTlsCaCerts($this->config['tls_ca_cert']); | 
					
						
							| 
									
										
										
										
											2024-03-27 00:30:04 +08:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-30 20:10:05 +08:00
										 |  |  |         $ldapHost = $this->parseServerString($this->config['server']); | 
					
						
							|  |  |  |         $ldapConnection = $this->ldap->connect($ldapHost); | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         if ($ldapConnection === false) { | 
					
						
							|  |  |  |             throw new LdapException(trans('errors.ldap_cannot_connect')); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Set any required options
 | 
					
						
							|  |  |  |         if ($this->config['version']) { | 
					
						
							|  |  |  |             $this->ldap->setVersion($ldapConnection, $this->config['version']); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-02-08 04:00:04 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         // Start and verify TLS if it's enabled
 | 
					
						
							|  |  |  |         if ($this->config['start_tls']) { | 
					
						
							| 
									
										
										
										
											2024-05-03 05:56:51 +08:00
										 |  |  |             try { | 
					
						
							|  |  |  |                 $started = $this->ldap->startTls($ldapConnection); | 
					
						
							|  |  |  |             } catch (\Exception $exception) { | 
					
						
							|  |  |  |                 $error = $exception->getMessage() . ' :: ' . ldap_error($ldapConnection); | 
					
						
							|  |  |  |                 ldap_get_option($ldapConnection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $detail); | 
					
						
							|  |  |  |                 Log::info("LDAP STARTTLS failure: {$error} {$detail}"); | 
					
						
							|  |  |  |                 throw new LdapException('Could not start TLS connection. Further details in the application log.'); | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2021-02-08 04:00:04 +08:00
										 |  |  |             if (!$started) { | 
					
						
							|  |  |  |                 throw new LdapException('Could not start TLS connection'); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         $this->ldapConnection = $ldapConnection; | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |         return $this->ldapConnection; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-03 05:56:51 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Configure TLS CA certs globally for ldap use. | 
					
						
							|  |  |  |      * This will detect if the given path is a directory or file, and set the relevant | 
					
						
							|  |  |  |      * LDAP TLS options appropriately otherwise throw an exception if no file/folder found. | 
					
						
							|  |  |  |      * | 
					
						
							|  |  |  |      * Note: When using a folder, certificates are expected to be correctly named by hash | 
					
						
							|  |  |  |      * which can be done via the c_rehash utility. | 
					
						
							|  |  |  |      * | 
					
						
							|  |  |  |      * @throws LdapException | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected function configureTlsCaCerts(string $caCertPath): void | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $errMessage = "Provided path [{$caCertPath}] for LDAP TLS CA certs could not be resolved to an existing location"; | 
					
						
							|  |  |  |         $path = realpath($caCertPath); | 
					
						
							|  |  |  |         if ($path === false) { | 
					
						
							|  |  |  |             throw new LdapException($errMessage); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (is_dir($path)) { | 
					
						
							|  |  |  |             $this->ldap->setOption(null, LDAP_OPT_X_TLS_CACERTDIR, $path); | 
					
						
							|  |  |  |         } else if (is_file($path)) { | 
					
						
							|  |  |  |             $this->ldap->setOption(null, LDAP_OPT_X_TLS_CACERTFILE, $path); | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |             throw new LdapException($errMessage); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-17 05:47:53 +08:00
										 |  |  |     /** | 
					
						
							| 
									
										
										
										
											2023-05-30 20:10:05 +08:00
										 |  |  |      * Parse an LDAP server string and return the host suitable for a connection. | 
					
						
							| 
									
										
										
										
											2019-12-16 20:38:35 +08:00
										 |  |  |      * Is flexible to formats such as 'ldap.example.com:8069' or 'ldaps://ldap.example.com'. | 
					
						
							| 
									
										
										
										
											2019-04-17 05:47:53 +08:00
										 |  |  |      */ | 
					
						
							| 
									
										
										
										
											2023-05-30 20:10:05 +08:00
										 |  |  |     protected function parseServerString(string $serverString): string | 
					
						
							| 
									
										
										
										
											2019-04-17 05:47:53 +08:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2023-05-30 20:10:05 +08:00
										 |  |  |         if (str_starts_with($serverString, 'ldaps://') || str_starts_with($serverString, 'ldap://')) { | 
					
						
							|  |  |  |             return $serverString; | 
					
						
							| 
									
										
										
										
											2019-04-17 05:47:53 +08:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-30 20:10:05 +08:00
										 |  |  |         return "ldap://{$serverString}"; | 
					
						
							| 
									
										
										
										
											2019-04-17 05:47:53 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Build a filter string by injecting common variables. | 
					
						
							| 
									
										
										
										
											2024-04-28 19:29:57 +08:00
										 |  |  |      * Both "${var}" and "{var}" style placeholders are supported. | 
					
						
							|  |  |  |      * Dollar based are old format but supported for compatibility. | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |      */ | 
					
						
							| 
									
										
										
										
											2019-12-16 20:38:35 +08:00
										 |  |  |     protected function buildFilter(string $filterString, array $attrs): string | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |     { | 
					
						
							|  |  |  |         $newAttrs = []; | 
					
						
							|  |  |  |         foreach ($attrs as $key => $attrText) { | 
					
						
							| 
									
										
										
										
											2024-04-28 19:29:57 +08:00
										 |  |  |             $escapedText = $this->ldap->escape($attrText); | 
					
						
							|  |  |  |             $oldVarKey = '${' . $key . '}'; | 
					
						
							|  |  |  |             $newVarKey = '{' . $key . '}'; | 
					
						
							|  |  |  |             $newAttrs[$oldVarKey] = $escapedText; | 
					
						
							|  |  |  |             $newAttrs[$newVarKey] = $escapedText; | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |         return strtr($filterString, $newAttrs); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							| 
									
										
										
										
											2019-12-16 20:38:35 +08:00
										 |  |  |      * Get the groups a user is a part of on ldap. | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |      * | 
					
						
							| 
									
										
										
										
											2018-07-16 01:21:45 +08:00
										 |  |  |      * @throws LdapException | 
					
						
							| 
									
										
										
										
											2022-03-24 00:34:23 +08:00
										 |  |  |      * @throws JsonDebugException | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |      */ | 
					
						
							| 
									
										
										
										
											2019-12-16 20:38:35 +08:00
										 |  |  |     public function getUserGroups(string $userName): array | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |     { | 
					
						
							|  |  |  |         $groupsAttr = $this->config['group_attribute']; | 
					
						
							|  |  |  |         $user = $this->getUserWithAttributes($userName, [$groupsAttr]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if ($user === null) { | 
					
						
							| 
									
										
										
										
											2018-08-12 20:28:40 +08:00
										 |  |  |             return []; | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |         $userGroups = $this->extractGroupsFromSearchResponseEntry($user); | 
					
						
							| 
									
										
										
										
											2022-03-24 00:34:23 +08:00
										 |  |  |         $allGroups = $this->getGroupsRecursive($userGroups, []); | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |         $formattedGroups = $this->extractGroupNamesFromLdapGroupDns($allGroups); | 
					
						
							| 
									
										
										
										
											2022-03-24 00:34:23 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         if ($this->config['dump_user_groups']) { | 
					
						
							|  |  |  |             throw new JsonDebugException([ | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |                 'details_from_ldap'            => $user, | 
					
						
							|  |  |  |                 'parsed_direct_user_groups'    => $userGroups, | 
					
						
							|  |  |  |                 'parsed_recursive_user_groups' => $allGroups, | 
					
						
							|  |  |  |                 'parsed_resulting_group_names' => $formattedGroups, | 
					
						
							| 
									
										
										
										
											2022-03-24 00:34:23 +08:00
										 |  |  |             ]); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-10-16 23:01:59 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-29 04:16:18 +08:00
										 |  |  |         return $formattedGroups; | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |     protected function extractGroupNamesFromLdapGroupDns(array $groupDNs): array | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $names = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         foreach ($groupDNs as $groupDN) { | 
					
						
							|  |  |  |             $exploded = $this->ldap->explodeDn($groupDN, 1); | 
					
						
							|  |  |  |             if ($exploded !== false && count($exploded) > 0) { | 
					
						
							|  |  |  |                 $names[] = $exploded[0]; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return array_unique($names); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |     /** | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |      * Build an array of all relevant groups DNs after recursively scanning | 
					
						
							|  |  |  |      * across parents of the groups given. | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |      * | 
					
						
							| 
									
										
										
										
											2018-07-16 01:21:45 +08:00
										 |  |  |      * @throws LdapException | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |      */ | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |     protected function getGroupsRecursive(array $groupDNs, array $checked): array | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2019-12-16 20:38:35 +08:00
										 |  |  |         $groupsToAdd = []; | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |         foreach ($groupDNs as $groupDN) { | 
					
						
							|  |  |  |             if (in_array($groupDN, $checked)) { | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |                 continue; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |             $parentGroups = $this->getParentsOfGroup($groupDN); | 
					
						
							| 
									
										
										
										
											2019-12-16 20:38:35 +08:00
										 |  |  |             $groupsToAdd = array_merge($groupsToAdd, $parentGroups); | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |             $checked[] = $groupDN; | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |         $uniqueDNs = array_unique(array_merge($groupDNs, $groupsToAdd), SORT_REGULAR); | 
					
						
							| 
									
										
										
										
											2019-12-16 20:38:35 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         if (empty($groupsToAdd)) { | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |             return $uniqueDNs; | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2019-12-16 20:38:35 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |         return $this->getGroupsRecursive($uniqueDNs, $checked); | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							| 
									
										
										
										
											2018-07-16 01:21:45 +08:00
										 |  |  |      * @throws LdapException | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |      */ | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |     protected function getParentsOfGroup(string $groupDN): array | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |         $groupsAttr = strtolower($this->config['group_attribute']); | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |         $ldapConnection = $this->getConnection(); | 
					
						
							|  |  |  |         $this->bindSystemUser($ldapConnection); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $followReferrals = $this->config['follow_referrals'] ? 1 : 0; | 
					
						
							|  |  |  |         $this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals); | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |         $read = $this->ldap->read($ldapConnection, $groupDN, '(objectClass=*)', [$groupsAttr]); | 
					
						
							|  |  |  |         $results = $this->ldap->getEntries($ldapConnection, $read); | 
					
						
							|  |  |  |         if ($results['count'] === 0) { | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |             return []; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |         return $this->extractGroupsFromSearchResponseEntry($results[0]); | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |      * Extract an array of group DN values from the given LDAP search response entry | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |      */ | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |     protected function extractGroupsFromSearchResponseEntry(array $ldapEntry): array | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |     { | 
					
						
							|  |  |  |         $groupsAttr = strtolower($this->config['group_attribute']); | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |         $groupDNs = []; | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |         $count = 0; | 
					
						
							| 
									
										
										
										
											2018-12-21 04:04:09 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |         if (isset($ldapEntry[$groupsAttr]['count'])) { | 
					
						
							|  |  |  |             $count = (int) $ldapEntry[$groupsAttr]['count']; | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-12-21 04:04:09 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-17 05:47:53 +08:00
										 |  |  |         for ($i = 0; $i < $count; $i++) { | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |             $dn = $ldapEntry[$groupsAttr][$i]; | 
					
						
							|  |  |  |             if (!in_array($dn, $groupDNs)) { | 
					
						
							|  |  |  |                 $groupDNs[] = $dn; | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-12-21 04:04:09 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-28 22:39:05 +08:00
										 |  |  |         return $groupDNs; | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-07-16 01:21:45 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							| 
									
										
										
										
											2019-12-16 20:38:35 +08:00
										 |  |  |      * Sync the LDAP groups to the user roles for the current user. | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |      * | 
					
						
							| 
									
										
										
										
											2018-07-16 01:21:45 +08:00
										 |  |  |      * @throws LdapException | 
					
						
							| 
									
										
										
										
											2022-03-24 00:34:23 +08:00
										 |  |  |      * @throws JsonDebugException | 
					
						
							| 
									
										
										
										
											2018-07-16 01:21:45 +08:00
										 |  |  |      */ | 
					
						
							| 
									
										
										
										
											2023-05-30 20:10:05 +08:00
										 |  |  |     public function syncGroups(User $user, string $username): void | 
					
						
							| 
									
										
										
										
											2018-07-16 01:21:45 +08:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2018-08-19 22:24:42 +08:00
										 |  |  |         $userLdapGroups = $this->getUserGroups($username); | 
					
						
							| 
									
										
										
										
											2021-10-07 06:05:26 +08:00
										 |  |  |         $this->groupSyncService->syncUserWithFoundGroups($user, $userLdapGroups, $this->config['remove_from_groups']); | 
					
						
							| 
									
										
										
										
											2018-07-16 01:21:45 +08:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-05-25 01:45:08 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Save and attach an avatar image, if found in the ldap details, and attach | 
					
						
							|  |  |  |      * to the given user model. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public function saveAndAttachAvatar(User $user, array $ldapUserDetails): void | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         if (is_null(config('services.ldap.thumbnail_attribute')) || is_null($ldapUserDetails['avatar'])) { | 
					
						
							|  |  |  |             return; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         try { | 
					
						
							|  |  |  |             $imageData = $ldapUserDetails['avatar']; | 
					
						
							|  |  |  |             $this->userAvatars->assignToUserFromExistingData($user, $imageData, 'jpg'); | 
					
						
							|  |  |  |         } catch (\Exception $exception) { | 
					
						
							|  |  |  |             Log::info("Failed to use avatar image from LDAP data for user id {$user->id}"); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-07-03 01:27:43 +08:00
										 |  |  | } |