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