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