PageRenderTime 64ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/framework/Auth/lib/Horde/Auth/Passwd.php

https://github.com/wrobel/horde
PHP | 423 lines | 261 code | 37 blank | 125 comment | 25 complexity | c5940a0515e6bafba93b874360020655 MD5 | raw file
Possible License(s): BSD-2-Clause, AGPL-1.0, LGPL-2.1, LGPL-3.0, BSD-3-Clause, LGPL-2.0, GPL-2.0
  1. <?php
  2. /**
  3. * The Horde_Auth_Passwd:: class provides a passwd-file implementation of
  4. * the Horde authentication system.
  5. *
  6. * Copyright 1997-2007 Rasmus Lerdorf <rasmus@php.net>
  7. * Copyright 2002-2012 Horde LLC (http://www.horde.org/)
  8. *
  9. * See the enclosed file COPYING for license information (LGPL). If you did
  10. * not receive this file, http://www.horde.org/licenses/lgpl21
  11. *
  12. * @author Rasmus Lerdorf <rasmus@php.net>
  13. * @author Chuck Hagenbuch <chuck@horde.org>
  14. * @category Horde
  15. * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1
  16. * @package Auth
  17. */
  18. class Horde_Auth_Passwd extends Horde_Auth_Base
  19. {
  20. /**
  21. * An array of capabilities, so that the driver can report which
  22. * operations it supports and which it doesn't.
  23. *
  24. * @var array
  25. */
  26. protected $_capabilities = array(
  27. 'list' => true,
  28. 'authenticate' => true,
  29. );
  30. /**
  31. * Hash list of users.
  32. *
  33. * @var array
  34. */
  35. protected $_users = null;
  36. /**
  37. * Array of groups and members.
  38. *
  39. * @var array
  40. */
  41. protected $_groups = array();
  42. /**
  43. * Filehandle for lockfile.
  44. *
  45. * @var resource
  46. */
  47. protected $_fplock;
  48. /**
  49. * Locking state.
  50. *
  51. * @var boolean
  52. */
  53. protected $_locked;
  54. /**
  55. * List of users that should be excluded from being listed/handled
  56. * in any way by this driver.
  57. *
  58. * @var array
  59. */
  60. protected $_exclude = array(
  61. 'root', 'daemon', 'bin', 'sys', 'sync', 'games', 'man', 'lp', 'mail',
  62. 'news', 'uucp', 'proxy', 'postgres', 'www-data', 'backup', 'operator',
  63. 'list', 'irc', 'gnats', 'nobody', 'identd', 'sshd', 'gdm', 'postfix',
  64. 'mysql', 'cyrus', 'ftp',
  65. );
  66. /**
  67. * Constructor.
  68. *
  69. * @param array $params Connection parameters:
  70. * <pre>
  71. * 'encryption' - (string) The encryption to use to store the password in
  72. * the table (e.g. plain, crypt, md5-hex, md5-base64, smd5,
  73. * sha, ssha, aprmd5).
  74. * DEFAULT: 'crypt-des'
  75. * 'filename' - (string) [REQUIRED] The passwd file to use.
  76. * 'lock' - (boolean) Should we lock the passwd file? The password file
  77. * cannot be changed (add, edit, or delete users) unless this is
  78. * true.
  79. * DEFAULT: false
  80. * 'show_encryption' - (boolean) Whether or not to prepend the encryption
  81. * in the password field.
  82. * DEFAULT: false
  83. * </pre>
  84. *
  85. * @throws InvalidArgumentException
  86. */
  87. public function __construct(array $params = array())
  88. {
  89. if (!isset($params['filename'])) {
  90. throw new InvalidArgumentException('Missing filename parameter.');
  91. }
  92. $params = array_merge(array(
  93. 'encryption' => 'crypt-des',
  94. 'lock' => false,
  95. 'show_encryption' => false
  96. ), $params);
  97. parent::__construct($params);
  98. }
  99. /**
  100. * Writes changes to passwd file and unlocks it. Takes no arguments and
  101. * has no return value. Called on script shutdown.
  102. */
  103. public function __destruct()
  104. {
  105. if ($this->_locked) {
  106. foreach ($this->_users as $user => $pass) {
  107. $data = $user . ':' . $pass;
  108. if ($this->_users[$user]) {
  109. $data .= ':' . $this->_users[$user];
  110. }
  111. fputs($this->_fplock, $data . "\n");
  112. }
  113. rename($this->_lockfile, $this->_params['filename']);
  114. flock($this->_fplock, LOCK_UN);
  115. $this->_locked = false;
  116. fclose($this->_fplock);
  117. }
  118. }
  119. /**
  120. * Queries the current Auth object to find out if it supports the given
  121. * capability.
  122. *
  123. * @param string $capability The capability to test for.
  124. *
  125. * @return boolean Whether or not the capability is supported.
  126. */
  127. public function hasCapability($capability)
  128. {
  129. if ($this->_params['lock']) {
  130. switch ($capability) {
  131. case 'add':
  132. case 'update':
  133. case 'resetpassword':
  134. case 'remove':
  135. return true;
  136. }
  137. }
  138. return parent::hasCapability($capability);
  139. }
  140. /**
  141. * Read and, if requested, lock the password file.
  142. *
  143. * @throws Horde_Auth_Exception
  144. */
  145. protected function _read()
  146. {
  147. if (is_array($this->_users)) {
  148. return;
  149. }
  150. if (empty($this->_params['filename'])) {
  151. throw new Horde_Auth_Exception('No password file set.');
  152. }
  153. if ($this->_params['lock']) {
  154. $this->_fplock = fopen(Horde_Util::getTempDir() . '/passwd.lock', 'w');
  155. flock($this->_fplock, LOCK_EX);
  156. $this->_locked = true;
  157. }
  158. $fp = fopen($this->_params['filename'], 'r');
  159. if (!$fp) {
  160. throw new Horde_Auth_Exception("Couldn't open '" . $this->_params['filename'] . "'.");
  161. }
  162. $this->_users = array();
  163. while (!feof($fp)) {
  164. $line = trim(fgets($fp, 256));
  165. if (empty($line)) {
  166. continue;
  167. }
  168. $parts = explode(':', $line);
  169. if (!count($parts)) {
  170. continue;
  171. }
  172. $user = $parts[0];
  173. $userinfo = array();
  174. if (strlen($user) && !in_array($user, $this->_exclude)) {
  175. if (isset($parts[1])) {
  176. $userinfo['password'] = $parts[1];
  177. }
  178. if (isset($parts[2])) {
  179. $userinfo['uid'] = $parts[2];
  180. }
  181. if (isset($parts[3])) {
  182. $userinfo['gid'] = $parts[3];
  183. }
  184. if (isset($parts[4])) {
  185. $userinfo['info'] = $parts[4];
  186. }
  187. if (isset($parts[5])) {
  188. $userinfo['home'] = $parts[5];
  189. }
  190. if (isset($parts[6])) {
  191. $userinfo['shell'] = $parts[6];
  192. }
  193. $this->_users[$user] = $userinfo;
  194. }
  195. }
  196. fclose($fp);
  197. if (!empty($this->_params['group_filename'])) {
  198. $fp = fopen($this->_params['group_filename'], 'r');
  199. if (!$fp) {
  200. throw new Horde_Auth_Exception("Couldn't open '" . $this->_params['group_filename'] . "'.");
  201. }
  202. $this->_groups = array();
  203. while (!feof($fp)) {
  204. $line = trim(fgets($fp));
  205. if (empty($line)) {
  206. continue;
  207. }
  208. $parts = explode(':', $line);
  209. $group = array_shift($parts);
  210. $users = array_pop($parts);
  211. $this->_groups[$group] = array_flip(preg_split('/\s*[,\s]\s*/', trim($users), -1, PREG_SPLIT_NO_EMPTY));
  212. }
  213. fclose($fp);
  214. }
  215. }
  216. /**
  217. * Find out if a set of login credentials are valid.
  218. *
  219. * @param string $userId The userId to check.
  220. * @param array $credentials An array of login credentials.
  221. *
  222. * @throws Horde_Auth_Exception
  223. */
  224. protected function _authenticate($userId, $credentials)
  225. {
  226. if (empty($credentials['password'])) {
  227. throw new Horde_Auth_Exception('', Horde_Auth::REASON_BADLOGIN);
  228. }
  229. try {
  230. $this->_read();
  231. } catch (Horde_Auth_Exception $e) {
  232. if ($this->_logger) {
  233. $this->_logger->log($e, 'ERR');
  234. }
  235. throw new Horde_Auth_Exception('', Horde_Auth::REASON_FAILED);
  236. }
  237. if (!isset($this->_users[$userId]) ||
  238. !$this->_comparePasswords($this->_users[$userId]['password'], $credentials['password'])) {
  239. throw new Horde_Auth_Exception('', Horde_Auth::REASON_BADLOGIN);
  240. }
  241. if (!empty($this->_params['required_groups'])) {
  242. $allowed = false;
  243. foreach ($this->_params['required_groups'] as $group) {
  244. if (isset($this->_groups[$group][$userId])) {
  245. $allowed = true;
  246. break;
  247. }
  248. }
  249. if (!$allowed) {
  250. throw new Horde_Auth_Exception('', Horde_Auth::REASON_BADLOGIN);
  251. }
  252. }
  253. }
  254. /**
  255. * List all users in the system.
  256. *
  257. * @return array The array of userIds.
  258. * @throws Horde_Auth_Exception
  259. */
  260. public function listUsers($sort = false)
  261. {
  262. $this->_read();
  263. $users = array_keys($this->_users);
  264. if (empty($this->_params['required_groups'])) {
  265. return $this->_sort($users, $sort);
  266. }
  267. $groupUsers = array();
  268. foreach ($this->_params['required_groups'] as $group) {
  269. $groupUsers = array_merge($groupUsers, array_intersect($users, array_keys($this->_groups[$group])));
  270. }
  271. return $this->_sort($groupUsers, $sort);
  272. }
  273. /**
  274. * Add a set of authentication credentials.
  275. *
  276. * @param string $userId The userId to add.
  277. * @param array $credentials The credentials to add.
  278. *
  279. * @throws Horde_Auth_Exception
  280. */
  281. public function addUser($userId, $credentials)
  282. {
  283. $this->_read();
  284. if (!$this->_locked) {
  285. throw new Horde_Auth_Exception('Password file not locked');
  286. }
  287. if (isset($this->_users[$userId])) {
  288. throw new Horde_Auth_Exception("Couldn't add user '$user', because the user already exists.");
  289. }
  290. $this->_users[$userId] = array(
  291. 'password' => Horde_Auth::getCryptedPassword($credentials['password'],
  292. '',
  293. $this->_params['encryption'],
  294. $this->_params['show_encryption']),
  295. );
  296. }
  297. /**
  298. * Update a set of authentication credentials.
  299. *
  300. * @param string $oldID The old userId.
  301. * @param string $newID The new userId.
  302. * @param array $credentials The new credentials
  303. *
  304. * @throws Horde_Auth_Exception
  305. */
  306. public function updateUser($oldID, $newID, $credentials)
  307. {
  308. $this->_read();
  309. if (!$this->_locked) {
  310. throw new Horde_Auth_Exception('Password file not locked');
  311. }
  312. if (!isset($this->_users[$userId])) {
  313. throw new Horde_Auth_Exception("Couldn't modify user '$oldID', because the user doesn't exist.");
  314. }
  315. $this->_users[$newID] = array(
  316. 'password' => Horde_Auth::getCryptedPassword($credentials['password'],
  317. '',
  318. $this->_params['encryption'],
  319. $this->_params['show_encryption']),
  320. );
  321. return true;
  322. }
  323. /**
  324. * Reset a user's password. Used for example when the user does not
  325. * remember the existing password.
  326. *
  327. * @param string $userId The user id for which to reset the password.
  328. *
  329. * @return string The new password.
  330. * @throws Horde_Auth_Exception
  331. */
  332. public function resetPassword($userId)
  333. {
  334. /* Get a new random password. */
  335. $password = Horde_Auth::genRandomPassword();
  336. $this->updateUser($userId, $userId, array('password' => $password));
  337. return $password;
  338. }
  339. /**
  340. * Delete a set of authentication credentials.
  341. *
  342. * @param string $userId The userId to delete.
  343. *
  344. * @throws Horde_Auth_Exception
  345. */
  346. public function removeUser($userId)
  347. {
  348. $this->_read();
  349. if (!$this->_locked) {
  350. throw new Horde_Auth_Exception('Password file not locked');
  351. }
  352. if (!isset($this->_users[$userId])) {
  353. throw new Horde_Auth_Exception("Couldn't delete user '$oldID', because the user doesn't exist.");
  354. }
  355. unset($this->_users[$userId]);
  356. }
  357. /**
  358. * Compare an encrypted password to a plaintext string to see if
  359. * they match.
  360. *
  361. * @param string $encrypted The crypted password to compare against.
  362. * @param string $plaintext The plaintext password to verify.
  363. *
  364. * @return boolean True if matched, false otherwise.
  365. */
  366. protected function _comparePasswords($encrypted, $plaintext)
  367. {
  368. return $encrypted == Horde_Auth::getCryptedPassword($plaintext,
  369. $encrypted,
  370. $this->_params['encryption'],
  371. $this->_params['show_encryption']);
  372. }
  373. }