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