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