1<?php 2/* 3** Zabbix 4** Copyright (C) 2001-2021 Zabbix SIA 5** 6** This program is free software; you can redistribute it and/or modify 7** it under the terms of the GNU General Public License as published by 8** the Free Software Foundation; either version 2 of the License, or 9** (at your option) any later version. 10** 11** This program is distributed in the hope that it will be useful, 12** but WITHOUT ANY WARRANTY; without even the implied warranty of 13** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14** GNU General Public License for more details. 15** 16** You should have received a copy of the GNU General Public License 17** along with this program; if not, write to the Free Software 18** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19**/ 20 21 22class CLdap { 23 24 const ERR_PHP_EXTENSION = 1; 25 const ERR_SERVER_UNAVAILABLE = 2; 26 const ERR_BIND_FAILED = 3; 27 const ERR_BIND_ANON_FAILED = 4; 28 const ERR_USER_NOT_FOUND = 5; 29 const ERR_OPT_PROTOCOL_FAILED = 10; 30 const ERR_OPT_TLS_FAILED = 11; 31 const ERR_OPT_REFERRALS_FAILED = 12; 32 const ERR_OPT_DEREF_FAILED = 13; 33 34 /** 35 * @var int 36 */ 37 public $error; 38 39 public function __construct($arg = []) { 40 $this->ds = false; 41 $this->info = []; 42 $this->cnf = [ 43 'host' => 'ldap://localhost', 44 'port' => '389', 45 'bind_dn' => 'uid=admin,ou=system', 46 'bind_password' => '', 47 'base_dn' => 'ou=users,ou=system', 48 'search_attribute' => 'uid', 49 'userfilter' => '(%{attr}=%{user})', 50 'groupkey' => 'cn', 51 'mapping' => [ 52 'username' => 'uid', 53 'userid' => 'uidnumbera', 54 'passwd' => 'userpassword' 55 ], 56 'referrals' => 0, 57 'version' => 3, 58 'starttls' => null, 59 'deref' => null 60 ]; 61 62 if (is_array($arg)) { 63 $this->cnf = zbx_array_merge($this->cnf, $arg); 64 } 65 66 $this->error = $this->moduleEnabled() ? 0 : static::ERR_PHP_EXTENSION; 67 } 68 69 /** 70 * Check is the PHP extension enabled. 71 * 72 * @return bool 73 */ 74 public function moduleEnabled() { 75 return function_exists('ldap_connect') && function_exists('ldap_set_option') && function_exists('ldap_bind') 76 && function_exists('ldap_search') && function_exists('ldap_get_entries') 77 && function_exists('ldap_free_result') && function_exists('ldap_start_tls'); 78 } 79 80 public function connect() { 81 $this->error = 0; 82 83 // connection already established 84 if ($this->ds) { 85 return true; 86 } 87 88 $this->bound = 0; 89 90 if (!$this->ds = @ldap_connect($this->cnf['host'], $this->cnf['port'])) { 91 $this->error = static::ERR_SERVER_UNAVAILABLE; 92 93 return false; 94 } 95 96 // Set protocol version and dependent options. 97 if ($this->cnf['version']) { 98 if (!@ldap_set_option($this->ds, LDAP_OPT_PROTOCOL_VERSION, $this->cnf['version'])) { 99 $this->error = static::ERR_OPT_PROTOCOL_FAILED; 100 } 101 else { 102 // use TLS (needs version 3) 103 if (isset($this->cnf['starttls']) && !@ldap_start_tls($this->ds)) { 104 $this->error = static::ERR_OPT_TLS_FAILED; 105 } 106 107 // needs version 3 108 if (!zbx_empty($this->cnf['referrals']) 109 && !@ldap_set_option($this->ds, LDAP_OPT_REFERRALS, $this->cnf['referrals'])) { 110 $this->error = static::ERR_OPT_REFERRALS_FAILED; 111 } 112 } 113 } 114 115 // set deref mode 116 if (isset($this->cnf['deref']) && !@ldap_set_option($this->ds, LDAP_OPT_DEREF, $this->cnf['deref'])) { 117 $this->error = static::ERR_OPT_DEREF_FAILED; 118 } 119 120 return !$this->error; 121 } 122 123 public function checkPass($user, $pass) { 124 if (!$pass) { 125 $this->error = static::ERR_USER_NOT_FOUND; 126 127 return false; 128 } 129 130 if (!$this->connect()) { 131 return false; 132 } 133 134 $dn = null; 135 136 // indirect user bind 137 if (!empty($this->cnf['bind_dn']) && !empty($this->cnf['bind_password'])) { 138 // use superuser credentials 139 if (!@ldap_bind($this->ds, $this->cnf['bind_dn'], $this->cnf['bind_password'])) { 140 $this->error = static::ERR_BIND_FAILED; 141 142 return false; 143 } 144 145 $this->bound = 2; 146 } 147 elseif (!empty($this->cnf['bind_dn']) && !empty($this->cnf['base_dn']) && !empty($this->cnf['userfilter'])) { 148 // special bind string 149 $dn = $this->makeFilter($this->cnf['bind_dn'], ['user' => $user, 'host' => $this->cnf['host']]); 150 } 151 elseif (strpos($this->cnf['base_dn'], '%{user}')) { 152 // direct user bind 153 $dn = $this->makeFilter($this->cnf['base_dn'], ['user' => $user, 'host' => $this->cnf['host']]); 154 } 155 else { 156 // anonymous bind 157 if (!@ldap_bind($this->ds)) { 158 $this->error = static::ERR_BIND_ANON_FAILED; 159 160 return false; 161 } 162 } 163 164 // try to bind to with the dn if we have one. 165 if ($dn) { 166 // user/password bind 167 if (!@ldap_bind($this->ds, $dn, $pass)) { 168 $this->error = static::ERR_USER_NOT_FOUND; 169 170 return false; 171 } 172 173 $this->bound = 1; 174 175 return true; 176 } 177 else { 178 // see if we can find the user 179 $this->info = $this->getUserData($user); 180 181 if (empty($this->info['dn'])) { 182 return false; 183 } 184 else { 185 $dn = $this->info['dn']; 186 } 187 188 // try to bind with the dn provided 189 if (!@ldap_bind($this->ds, $dn, $pass)) { 190 $this->error = static::ERR_USER_NOT_FOUND; 191 192 return false; 193 } 194 195 $this->bound = 1; 196 197 return true; 198 } 199 200 return false; 201 } 202 203 private function getUserData($user) { 204 if (!$this->connect()) { 205 return false; 206 } 207 208 // force superuser bind if wanted and not bound as superuser yet 209 if (!empty($this->cnf['bind_dn']) && !empty($this->cnf['bind_password']) && ($this->bound < 2)) { 210 if (!@ldap_bind($this->ds, $this->cnf['bind_dn'], $this->cnf['bind_password'])) { 211 $this->error = static::ERR_BIND_FAILED; 212 213 return false; 214 } 215 $this->bound = 2; 216 } 217 218 // with no superuser creds we continue as user or anonymous here 219 $info['user'] = $user; 220 $info['host'] = $this->cnf['host']; 221 222 // get info for given user 223 $base = $this->makeFilter($this->cnf['base_dn'], $info); 224 225 if (isset($this->cnf['userfilter']) && !empty($this->cnf['userfilter'])) { 226 $filter = $this->makeFilter($this->cnf['userfilter'], $info); 227 } 228 else { 229 $filter = '(ObjectClass=*)'; 230 } 231 $sr = @ldap_search($this->ds, $base, $filter); 232 $result = is_resource($sr) ? @ldap_get_entries($this->ds, $sr) : []; 233 234 // don't accept more or less than one response 235 if (!$result || $result['count'] != 1) { 236 $this->error = $result ? static::ERR_USER_NOT_FOUND : static::ERR_BIND_FAILED; 237 238 return false; 239 } 240 241 $user_result = $result[0]; 242 ldap_free_result($sr); 243 244 // general user info 245 $info['dn'] = $user_result['dn']; 246 $info['name'] = $user_result['cn'][0]; 247 $info['grps'] = []; 248 249 // overwrite if other attribs are specified. 250 if (is_array($this->cnf['mapping'])) { 251 foreach ($this->cnf['mapping'] as $localkey => $key) { 252 $info[$localkey] = isset($user_result[$key])?$user_result[$key][0]:null; 253 } 254 } 255 $user_result = zbx_array_merge($info,$user_result); 256 257 return $info; 258 } 259 260 private function makeFilter($filter, $placeholders) { 261 $placeholders['attr'] = $this->cnf['search_attribute']; 262 preg_match_all("/%{([^}]+)/", $filter, $matches, PREG_PATTERN_ORDER); 263 264 // replace each match 265 foreach ($matches[1] as $match) { 266 // take first element if array 267 if (is_array($placeholders[$match])) { 268 $value = $placeholders[$match][0]; 269 } 270 else { 271 $value = $placeholders[$match]; 272 } 273 $filter = str_replace('%{'.$match.'}', $value, $filter); 274 } 275 return $filter; 276 } 277} 278