PageRenderTime 26ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/framework/Prefs/lib/Horde/Prefs/Storage/Ldap.php

https://github.com/sgtcarneiro/horde
PHP | 379 lines | 211 code | 54 blank | 114 comment | 40 complexity | 0e0a1a7d8cbee09feca2b92024a118fc MD5 | raw file
  1. <?php
  2. /**
  3. * Preferences storage implementation for LDAP servers.
  4. *
  5. * Copyright 1999-2011 The Horde Project (http://www.horde.org/)
  6. *
  7. * See the enclosed file COPYING for license information (LGPL). If you
  8. * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
  9. *
  10. * @author Jon Parise <jon@horde.org>
  11. * @author Ben Klang <ben@alkaloid.net>
  12. * @author Michael Slusarz <slusarz@horde.org>
  13. * @category Horde
  14. * @package Prefs
  15. */
  16. class Horde_Prefs_Storage_Ldap extends Horde_Prefs_Storage_Base
  17. {
  18. /**
  19. * Handle for the current LDAP connection.
  20. *
  21. * @var resource
  22. */
  23. protected $_connection;
  24. /**
  25. * Boolean indicating whether or not we're connected to the LDAP server.
  26. *
  27. * @var boolean
  28. */
  29. protected $_connected = false;
  30. /**
  31. * String holding the user's DN.
  32. *
  33. * @var string
  34. */
  35. protected $_dn = '';
  36. /**
  37. * Constructor.
  38. *
  39. * @param string $user The username.
  40. * @param array $params Configuration options:
  41. * <pre>
  42. * basedn - (string) [REQUIRED] The base DN for the LDAP server.
  43. * hostspec - (string) [REQUIRED] The hostname of the LDAP server.
  44. * uid - (string) [REQUIRED] The username search key.
  45. * writeas - (string) [REQUIRED] One of "user", "admin", or "search"
  46. *
  47. * Optional parameters:
  48. * binddn - (string) The DN of the administrative account to bind for
  49. * write operations.
  50. * bindpw - (string) binddn's password for bind authentication.
  51. * port - (integer) The port of the LDAP server.
  52. * DEFAULT: 389
  53. * searchdn - (string) The DN of a user with search permissions on the
  54. * directory.
  55. * searchpw - (string) searchdn's password for binding.
  56. * tls - (boolean) Whether to use TLS connections.
  57. * DEFAULT: false
  58. * version - (integer) The version of the LDAP protocol to use.
  59. * DEFAULT: NONE (system default will be used)
  60. * </pre>
  61. */
  62. public function __construct($user, array $params = array())
  63. {
  64. throw new Horde_Prefs_Exception('This driver needs to be refactored to use Horde_Ldap.');
  65. /* If a valid server port has not been specified, set the default. */
  66. if (!isset($params['port']) || !is_integer($params['port'])) {
  67. $params['port'] = 389;
  68. }
  69. parent::__construct($user, $params);
  70. }
  71. /**
  72. */
  73. public function get($scope_ob)
  74. {
  75. $this->_connect();
  76. // Search for the multi-valued field containing the array of
  77. // preferences.
  78. $search = @ldap_search(
  79. $this->_connection,
  80. $this->_params['basedn'],
  81. $this->_params['uid'] . '=' . $this->params['user'],
  82. array($scope_ob->scope . 'Prefs'));
  83. if ($search === false) {
  84. throw new Horde_Prefs_Exception(sprintf('Error while searching for the user\'s prefs: [%d]: %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
  85. }
  86. $result = @ldap_get_entries($this->_connection, $search);
  87. if ($result === false) {
  88. throw new Horde_Prefs_Exception(sprintf('Error while retrieving LDAP search results for the user\'s prefs: [%d]: %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
  89. }
  90. // Preferences are stored as colon-separated name:value pairs.
  91. // Each pair is stored as its own attribute off of the multi-
  92. // value attribute named in: $scope_ob->scope . 'Prefs'
  93. // ldap_get_entries() converts attribute indexes to lowercase.
  94. $field = Horde_String::lower($scope_ob->scope . 'prefs');
  95. $prefs = isset($result[0][$field])
  96. ? $result[0][$field]
  97. : array();
  98. foreach ($prefs as $prefstr) {
  99. // If the string doesn't contain a colon delimiter, skip it.
  100. if (strpos($prefstr, ':') !== false) {
  101. // Split the string into its name:value components.
  102. list($name, $val) = explode(':', $prefstr, 2);
  103. $scope_ob->set($name, base64_decode($val));
  104. }
  105. }
  106. return $scope_ob;
  107. }
  108. /**
  109. */
  110. public function store($scope_ob)
  111. {
  112. $this->_connect();
  113. // Build a hash of the preferences and their values that need
  114. // to be stored on the LDAP server. Because we have to update
  115. // all of the values of a multi-value entry wholesale, we
  116. // can't just pick out the dirty preferences; we must update
  117. // the entire dirty scope.
  118. $new_vals = array();
  119. /* Driver has no support for storing locked status. */
  120. foreach ($scope_ob->getDirty() as $name) {
  121. $new_vals[$scope_ob->scope . 'Prefs'][] = $name . ':' . base64_encode($scope_ob->get($name));
  122. }
  123. // Entries must have the objectclasses 'top' and 'hordeperson'
  124. // to successfully store LDAP prefs. Check for both of them,
  125. // and add them if necessary.
  126. $search = @ldap_search(
  127. $this->_connection,
  128. $this->_params['basedn'],
  129. $this->_params['uid'] . '=' . $this->prefs['user'],
  130. array('objectclass')
  131. );
  132. if ($search === false) {
  133. throw new Horde_Prefs_Exception(sprintf('Error searching the directory for required objectClasses: [%d] %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
  134. }
  135. $result = @ldap_get_entries($this->_connection, $search);
  136. if ($result === false) {
  137. throw new Horde_Prefs_Exception(sprintf('Error retrieving results while checking for required objectClasses: [%d] %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
  138. }
  139. if ($result['count'] > 0) {
  140. $hordeperson = $top = false;
  141. for ($i = 0; $i < $result[0]['objectclass']['count']; ++$i) {
  142. if ($result[0]['objectclass'][$i] == 'top') {
  143. $top = true;
  144. } elseif ($result[0]['objectclass'][$i] == 'hordePerson') {
  145. $hordeperson = true;
  146. }
  147. }
  148. // Add any missing objectclasses.
  149. if (!$top) {
  150. @ldap_mod_add($this->_connection, $this->_dn, array('objectclass' => 'top'));
  151. }
  152. if (!$hordeperson) {
  153. @ldap_mod_add($this->_connection, $this->_dn, array('objectclass' => 'hordePerson'));
  154. }
  155. }
  156. // Send the hash to the LDAP server.
  157. $result = @ldap_mod_replace($this->_connection, $this->_dn, $new_vals);
  158. if ($result === false) {
  159. throw new Horde_Prefs_Exception(sprintf('Unable to modify user\'s objectClass for preferences: [%d] %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
  160. }
  161. }
  162. /**
  163. */
  164. public function remove($scope = null, $pref = null)
  165. {
  166. // TODO: Implement scope/pref-level removal
  167. if (!is_null($scope) || !is_null($pref)) {
  168. throw new Horde_Prefs_Exception('Removal not supported.');
  169. }
  170. $this->_connect();
  171. // TODO: Move to horde/Core
  172. $attrs = $GLOBALS['registry']->listApps(array('inactive', 'active', 'hidden', 'notoolbar', 'admin'));
  173. foreach ($attrs as $key => $val) {
  174. $attrs[$key] = $val . 'Prefs';
  175. }
  176. $search = @ldap_read($this->_connection, $this->_dn,
  177. 'objectClass=hordePerson', $attrs, 1);
  178. if ($search === false) {
  179. throw new Horde_Prefs_Exception(sprintf('Error while getting preferenes from LDAP: [%d] %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
  180. }
  181. $result = @ldap_get_entries($this->_connection, $search);
  182. if ($result === false) {
  183. throw new Horde_Prefs_Exception(sprintf('Error while retrieving results from LDAP: [%d] %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
  184. }
  185. $attrs = array();
  186. for ($i = 0; $i < $result[0]['count']; $i++) {
  187. $attrs[$result[0][$i]] = array();
  188. }
  189. $result = @ldap_mod_del($this->_connection, $this->_dn, $attrs);
  190. if ($result === false) {
  191. throw new Horde_Prefs_Exception(sprintf('Unable to clear user\'s preferences: [%d] %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
  192. }
  193. }
  194. /* LDAP Helper functions.
  195. * TODO: Use Horde_LDAP */
  196. /**
  197. * Opens a connection to the LDAP server.
  198. *
  199. * @throws Horde_Prefs_Exception
  200. */
  201. protected function _connect()
  202. {
  203. if ($this->_connected) {
  204. return;
  205. }
  206. if (!Horde_Util::extensionExists('ldap')) {
  207. throw new Horde_Prefs_Exception('Required LDAP extension not found.');
  208. }
  209. Horde::assertDriverConfig($this->_params, 'prefs',
  210. array('hostspec', 'basedn', 'uid', 'writeas'),
  211. 'preferences LDAP');
  212. /* Connect to the LDAP server anonymously. */
  213. $conn = ldap_connect($this->_params['hostspec'], $this->_params['port']);
  214. if (!$conn) {
  215. throw new Horde_Prefs_Exception(sprintf('Failed to open an LDAP connection to %s.', $this->_params['hostspec']));
  216. }
  217. /* Set the LDAP protocol version. */
  218. if (isset($this->_params['version'])) {
  219. $result = @ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION,
  220. $this->_params['version']);
  221. if ($result === false) {
  222. throw new Horde_Prefs_Exception(sprintf('Set LDAP protocol version to %d failed: [%d] %s', $this->_params['version'], @ldap_errno($conn), @ldap_error($conn)));
  223. }
  224. }
  225. /* Start TLS if we're using it. */
  226. if (!empty($this->_params['tls'])) {
  227. @ldap_start_tls($conn);
  228. }
  229. /* If necessary, bind to the LDAP server as the user with search
  230. * permissions. */
  231. if (!empty($this->_params['searchdn'])) {
  232. $bind = @ldap_bind($conn, $this->_params['searchdn'],
  233. $this->_params['searchpw']);
  234. if ($bind === false) {
  235. throw new Horde_Prefs_Exception(sprintf('Bind to server %s:%d with DN %s failed: [%d] %s', $this->_params['hostspec'], $this->_params['port'], $this->_params['searchdn'], @ldap_errno($conn), @ldap_error($conn)));
  236. }
  237. }
  238. /* Register our callback function to handle referrals. */
  239. if (function_exists('ldap_set_rebind_proc')) {
  240. $result = @ldap_set_rebind_proc($conn, array($this, 'rebindProc'));
  241. if ($result === false) {
  242. throw new Horde_Prefs_Exception(sprintf('Setting referral callback failed: [%d] %s', @ldap_errno($conn), @ldap_error($conn)));
  243. }
  244. }
  245. /* Store the connection handle at the instance level. */
  246. $this->_connection = $conn;
  247. /* Search for the user's full DN. */
  248. $search = @ldap_search($this->_connection, $this->_params['basedn'],
  249. $this->_params['uid'] . '=' . $this->params['user'], array('dn'));
  250. if ($search === false) {
  251. throw new Horde_Prefs_Exception(sprintf('Error while searching the directory for the user\'s DN: [%d]: %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
  252. }
  253. $result = @ldap_get_entries($this->_connection, $search);
  254. if ($result === false) {
  255. throw new Horde_Prefs_Exception(sprintf('Error while retrieving LDAP search results for the user\'s DN: [%d]: %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
  256. }
  257. if ($result['count'] != 1) {
  258. throw new Horde_Prefs_Exception('Zero or more than one DN returned from search; unable to determine user\'s correct DN.');
  259. }
  260. $this->_dn = $result[0]['dn'];
  261. // Now we should have the user's DN. Re-bind as appropriate with write
  262. // permissions to be able to store preferences.
  263. switch($this->_params['writeas']) {
  264. case 'user':
  265. $result = @ldap_bind($this->_connection,
  266. $this->_dn, $this->_opts['password']);
  267. break;
  268. case 'admin':
  269. $result = @ldap_bind($this->_connection,
  270. $this->_params['binddn'],
  271. $this->_params['bindpw']);
  272. break;
  273. case 'search':
  274. // Since we've already bound as the search DN above, no rebinding
  275. // is necessary.
  276. $result = true;
  277. break;
  278. }
  279. if ($result === false) {
  280. throw new Horde_Prefs_Exception(sprintf('Error rebinding for prefs writing: [%d]: %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
  281. }
  282. // We now have a ready-to-use connection.
  283. $this->_connected = true;
  284. }
  285. /**
  286. * Callback function for LDAP referrals. This function is called when an
  287. * LDAP operation returns a referral to an alternate server.
  288. *
  289. * @return integer 1 on error, 0 on success.
  290. */
  291. public function rebindProc($conn, $who)
  292. {
  293. /* Strip out the hostname we're being redirected to. */
  294. $who = preg_replace(array('|^.*://|', '|:\d*$|'), '', $who);
  295. /* Make sure the server we're being redirected to is in our list of
  296. valid servers. */
  297. if (strpos($this->_params['hostspec'], $who) === false) {
  298. if ($this->_opts['logger']) {
  299. $this->_opts['logger']->log(sprintf('Referral target %s for DN %s is not in the authorized server list.', $who, $bind_dn), 'ERR');
  300. }
  301. return 1;
  302. }
  303. /* Figure out the DN of the authenticating user. */
  304. switch($this->_params['writeas']) {
  305. case 'user':
  306. $bind_dn = $this->_dn;
  307. $bind_pw = $this->_opts['password'];
  308. break;
  309. case 'admin':
  310. $bind_dn = $this->_params['binddn'];
  311. $bind_pw = $this->_params['bindpw'];
  312. break;
  313. case 'search':
  314. $bind_dn = $this->_params['searchdn'];
  315. $bind_dn = $this->_params['searchpw'];
  316. break;
  317. }
  318. /* Bind to the new server. */
  319. $bind = @ldap_bind($conn, $bind_dn, $bind_pw);
  320. if (($bind === false) && $this->_opts['logger']) {
  321. $this->_opts['logger']->log(sprintf('Rebind to server %s:%d with DN %s failed: [%d] %s', $this->_params['hostspec'], $this->_params['port'], $bind_dn, @ldap_errno($this->_connection), @ldap_error($this->_connection)), 'ERR');
  322. }
  323. return 0;
  324. }
  325. }