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 public function __construct($arg = []) { 25 $this->ds = false; 26 $this->info = []; 27 $this->cnf = [ 28 'host' => 'ldap://localhost', 29 'port' => '389', 30 'bind_dn' => 'uid=admin,ou=system', 31 'bind_password' => '', 32 'base_dn' => 'ou=users,ou=system', 33 'search_attribute' => 'uid', 34 'userfilter' => '(%{attr}=%{user})', 35 'groupkey' => 'cn', 36 'mapping' => [ 37 'alias' => 'uid', 38 'userid' => 'uidnumbera', 39 'passwd' => 'userpassword' 40 ], 41 'referrals' => 0, 42 'version' => 3, 43 'starttls' => null, 44 'deref' => null 45 ]; 46 47 if (is_array($arg)) { 48 $this->cnf = zbx_array_merge($this->cnf, $arg); 49 } 50 51 $ldap_status = (new CFrontendSetup())->checkPhpLdapModule(); 52 53 if ($ldap_status['result'] != CFrontendSetup::CHECK_OK) { 54 error($ldap_status['error']); 55 return false; 56 } 57 } 58 59 public function connect() { 60 // connection already established 61 if ($this->ds) { 62 return true; 63 } 64 65 $this->bound = 0; 66 67 if (!$this->ds = ldap_connect($this->cnf['host'], $this->cnf['port'])) { 68 error('LDAP: couldn\'t connect to LDAP server.'); 69 70 return false; 71 } 72 73 // set protocol version and dependend options 74 if ($this->cnf['version']) { 75 if (!ldap_set_option($this->ds, LDAP_OPT_PROTOCOL_VERSION, $this->cnf['version'])) { 76 error('Setting LDAP Protocol version '.$this->cnf['version'].' failed.'); 77 } 78 else { 79 // use TLS (needs version 3) 80 if (isset($this->cnf['starttls']) && !ldap_start_tls($this->ds)) { 81 error('Starting TLS failed.'); 82 } 83 84 // needs version 3 85 if (!zbx_empty($this->cnf['referrals']) 86 && !ldap_set_option($this->ds, LDAP_OPT_REFERRALS, $this->cnf['referrals'])) { 87 error('Setting LDAP referrals to off failed.'); 88 } 89 } 90 } 91 92 // set deref mode 93 if (isset($this->cnf['deref']) && !ldap_set_option($this->ds, LDAP_OPT_DEREF, $this->cnf['deref'])) { 94 error('Setting LDAP Deref mode '.$this->cnf['deref'].' failed.'); 95 } 96 97 return true; 98 } 99 100 public function checkPass($user, $pass) { 101 if (!$pass) { 102 return false; 103 } 104 105 if (!$this->connect()) { 106 return false; 107 } 108 109 $dn = null; 110 111 // indirect user bind 112 if (!empty($this->cnf['bind_dn']) && !empty($this->cnf['bind_password'])) { 113 // use superuser credentials 114 if (!ldap_bind($this->ds, $this->cnf['bind_dn'], $this->cnf['bind_password'])) { 115 error('LDAP: cannot bind by given Bind DN.'); 116 117 return false; 118 } 119 120 $this->bound = 2; 121 } 122 elseif (!empty($this->cnf['bind_dn']) && !empty($this->cnf['base_dn']) && !empty($this->cnf['userfilter'])) { 123 // special bind string 124 $dn = $this->makeFilter($this->cnf['bind_dn'], ['user' => $user, 'host' => $this->cnf['host']]); 125 } 126 elseif (strpos($this->cnf['base_dn'], '%{user}')) { 127 // direct user bind 128 $dn = $this->makeFilter($this->cnf['base_dn'], ['user' => $user, 'host' => $this->cnf['host']]); 129 } 130 else { 131 // anonymous bind 132 if (!ldap_bind($this->ds)) { 133 error('LDAP: can not bind anonymously.'); 134 135 return false; 136 } 137 } 138 139 // try to bind to with the dn if we have one. 140 if ($dn) { 141 // user/password bind 142 if (!ldap_bind($this->ds, $dn, $pass)) { 143 return false; 144 } 145 146 $this->bound = 1; 147 148 return true; 149 } 150 else { 151 // see if we can find the user 152 $this->info = $this->getUserData($user); 153 154 if (empty($this->info['dn'])) { 155 return false; 156 } 157 else { 158 $dn = $this->info['dn']; 159 } 160 161 // try to bind with the dn provided 162 if (!ldap_bind($this->ds, $dn, $pass)) { 163 return false; 164 } 165 166 $this->bound = 1; 167 168 return true; 169 } 170 171 return false; 172 } 173 174 private function getUserData($user) { 175 if (!$this->connect()) { 176 return false; 177 } 178 179 // force superuser bind if wanted and not bound as superuser yet 180 if (!empty($this->cnf['bind_dn']) && !empty($this->cnf['bind_password']) && ($this->bound < 2)) { 181 if (!ldap_bind($this->ds, $this->cnf['bind_dn'], $this->cnf['bind_password'])) { 182 return false; 183 } 184 $this->bound = 2; 185 } 186 187 // with no superuser creds we continue as user or anonymous here 188 $info['user'] = $user; 189 $info['host'] = $this->cnf['host']; 190 191 // get info for given user 192 $base = $this->makeFilter($this->cnf['base_dn'], $info); 193 194 if (isset($this->cnf['userfilter']) && !empty($this->cnf['userfilter'])) { 195 $filter = $this->makeFilter($this->cnf['userfilter'], $info); 196 } 197 else { 198 $filter = '(ObjectClass=*)'; 199 } 200 $sr = ldap_search($this->ds, $base, $filter); 201 $result = ldap_get_entries($this->ds, $sr); 202 203 // don't accept more or less than one response 204 if ($result['count'] != 1) { 205 error('LDAP: User not found.'); 206 return false; 207 } 208 209 $user_result = $result[0]; 210 ldap_free_result($sr); 211 212 // general user info 213 $info['dn'] = $user_result['dn']; 214 $info['name'] = $user_result['cn'][0]; 215 $info['grps'] = []; 216 217 // overwrite if other attribs are specified. 218 if (is_array($this->cnf['mapping'])) { 219 foreach ($this->cnf['mapping'] as $localkey => $key) { 220 $info[$localkey] = isset($user_result[$key])?$user_result[$key][0]:null; 221 } 222 } 223 $user_result = zbx_array_merge($info,$user_result); 224 225 // get groups for given user if grouptree is given 226 if (isset($this->cnf['grouptree']) && isset($this->cnf['groupfilter'])) { 227 $base = $this->makeFilter($this->cnf['grouptree'], $user_result); 228 $filter = $this->makeFilter($this->cnf['groupfilter'], $user_result); 229 $sr = ldap_search($this->ds, $base, $filter, [$this->cnf['groupkey']]); 230 231 if (!$sr) { 232 error('LDAP: Reading group memberships failed.'); 233 return false; 234 } 235 236 $result = ldap_get_entries($this->ds, $sr); 237 238 foreach ($result as $grp) { 239 if (!empty($grp[$this->cnf['groupkey']][0])) { 240 $info['grps'][] = $grp[$this->cnf['groupkey']][0]; 241 } 242 } 243 } 244 245 // always add the default group to the list of groups 246 if (isset($conf['defaultgroup']) && !str_in_array($conf['defaultgroup'], $info['grps'])) { 247 $info['grps'][] = $conf['defaultgroup']; 248 } 249 250 return $info; 251 } 252 253 private function makeFilter($filter, $placeholders) { 254 $placeholders['attr'] = $this->cnf['search_attribute']; 255 preg_match_all("/%{([^}]+)/", $filter, $matches, PREG_PATTERN_ORDER); 256 257 // replace each match 258 foreach ($matches[1] as $match) { 259 // take first element if array 260 if (is_array($placeholders[$match])) { 261 $value = $placeholders[$match][0]; 262 } 263 else { 264 $value = $placeholders[$match]; 265 } 266 $filter = str_replace('%{'.$match.'}', $value, $filter); 267 } 268 return $filter; 269 } 270} 271