1""" 2Manage the shadow file on Linux systems 3 4.. important:: 5 If you feel that Salt should be using this module to manage passwords on a 6 minion, and it is using a different module (or gives an error similar to 7 *'shadow.info' is not available*), see :ref:`here 8 <module-provider-override>`. 9""" 10 11import datetime 12import functools 13import logging 14import os 15 16import salt.utils.data 17import salt.utils.files 18import salt.utils.stringutils 19from salt.exceptions import CommandExecutionError 20 21try: 22 import spwd 23except ImportError: 24 pass 25 26 27try: 28 import salt.utils.pycrypto 29 30 HAS_CRYPT = True 31except ImportError: 32 HAS_CRYPT = False 33 34__virtualname__ = "shadow" 35 36log = logging.getLogger(__name__) 37 38 39def __virtual__(): 40 return __virtualname__ if __grains__.get("kernel", "") == "Linux" else False 41 42 43def default_hash(): 44 """ 45 Returns the default hash used for unset passwords 46 47 CLI Example: 48 49 .. code-block:: bash 50 51 salt '*' shadow.default_hash 52 """ 53 return "!" 54 55 56def info(name, root=None): 57 """ 58 Return information for the specified user 59 60 name 61 User to get the information for 62 63 root 64 Directory to chroot into 65 66 CLI Example: 67 68 .. code-block:: bash 69 70 salt '*' shadow.info root 71 """ 72 if root is not None: 73 getspnam = functools.partial(_getspnam, root=root) 74 else: 75 getspnam = functools.partial(spwd.getspnam) 76 77 try: 78 data = getspnam(name) 79 ret = { 80 "name": data.sp_namp if hasattr(data, "sp_namp") else data.sp_nam, 81 "passwd": data.sp_pwdp if hasattr(data, "sp_pwdp") else data.sp_pwd, 82 "lstchg": data.sp_lstchg, 83 "min": data.sp_min, 84 "max": data.sp_max, 85 "warn": data.sp_warn, 86 "inact": data.sp_inact, 87 "expire": data.sp_expire, 88 } 89 except (KeyError, FileNotFoundError): 90 return { 91 "name": "", 92 "passwd": "", 93 "lstchg": "", 94 "min": "", 95 "max": "", 96 "warn": "", 97 "inact": "", 98 "expire": "", 99 } 100 return ret 101 102 103def _set_attrib(name, key, value, param, root=None, validate=True): 104 """ 105 Set a parameter in /etc/shadow 106 """ 107 pre_info = info(name, root=root) 108 109 # If the user is not present or the attribute is already present, 110 # we return early 111 if not pre_info["name"]: 112 return False 113 114 if value == pre_info[key]: 115 return True 116 117 cmd = ["chage"] 118 119 if root is not None: 120 cmd.extend(("-R", root)) 121 122 cmd.extend((param, value, name)) 123 124 ret = not __salt__["cmd.run"](cmd, python_shell=False) 125 if validate: 126 ret = info(name, root=root).get(key) == value 127 return ret 128 129 130def set_inactdays(name, inactdays, root=None): 131 """ 132 Set the number of days of inactivity after a password has expired before 133 the account is locked. See man chage. 134 135 name 136 User to modify 137 138 inactdays 139 Set password inactive after this number of days 140 141 root 142 Directory to chroot into 143 144 CLI Example: 145 146 .. code-block:: bash 147 148 salt '*' shadow.set_inactdays username 7 149 """ 150 return _set_attrib(name, "inact", inactdays, "-I", root=root) 151 152 153def set_maxdays(name, maxdays, root=None): 154 """ 155 Set the maximum number of days during which a password is valid. 156 See man chage. 157 158 name 159 User to modify 160 161 maxdays 162 Maximum number of days during which a password is valid 163 164 root 165 Directory to chroot into 166 167 CLI Example: 168 169 .. code-block:: bash 170 171 salt '*' shadow.set_maxdays username 90 172 """ 173 return _set_attrib(name, "max", maxdays, "-M", root=root) 174 175 176def set_mindays(name, mindays, root=None): 177 """ 178 Set the minimum number of days between password changes. See man chage. 179 180 name 181 User to modify 182 183 mindays 184 Minimum number of days between password changes 185 186 root 187 Directory to chroot into 188 189 CLI Example: 190 191 .. code-block:: bash 192 193 salt '*' shadow.set_mindays username 7 194 """ 195 return _set_attrib(name, "min", mindays, "-m", root=root) 196 197 198def gen_password(password, crypt_salt=None, algorithm="sha512"): 199 """ 200 .. versionadded:: 2014.7.0 201 202 Generate hashed password 203 204 .. note:: 205 206 When called this function is called directly via remote-execution, 207 the password argument may be displayed in the system's process list. 208 This may be a security risk on certain systems. 209 210 password 211 Plaintext password to be hashed. 212 213 crypt_salt 214 Crpytographic salt. If not given, a random 8-character salt will be 215 generated. 216 217 algorithm 218 The following hash algorithms are supported: 219 220 * md5 221 * blowfish (not in mainline glibc, only available in distros that add it) 222 * sha256 223 * sha512 (default) 224 225 CLI Example: 226 227 .. code-block:: bash 228 229 salt '*' shadow.gen_password 'I_am_password' 230 salt '*' shadow.gen_password 'I_am_password' crypt_salt='I_am_salt' algorithm=sha256 231 """ 232 if not HAS_CRYPT: 233 raise CommandExecutionError( 234 "gen_password is not available on this operating system " 235 'because the "crypt" python module is not available.' 236 ) 237 return salt.utils.pycrypto.gen_hash(crypt_salt, password, algorithm) 238 239 240def del_password(name, root=None): 241 """ 242 .. versionadded:: 2014.7.0 243 244 Delete the password from name user 245 246 name 247 User to delete 248 249 root 250 Directory to chroot into 251 252 CLI Example: 253 254 .. code-block:: bash 255 256 salt '*' shadow.del_password username 257 """ 258 cmd = ["passwd"] 259 if root is not None: 260 cmd.extend(("-R", root)) 261 cmd.extend(("-d", name)) 262 263 __salt__["cmd.run"](cmd, python_shell=False, output_loglevel="quiet") 264 uinfo = info(name, root=root) 265 return not uinfo["passwd"] and uinfo["name"] == name 266 267 268def lock_password(name, root=None): 269 """ 270 .. versionadded:: 2016.11.0 271 272 Lock the password from specified user 273 274 name 275 User to lock 276 277 root 278 Directory to chroot into 279 280 CLI Example: 281 282 .. code-block:: bash 283 284 salt '*' shadow.lock_password username 285 """ 286 pre_info = info(name, root=root) 287 if not pre_info["name"]: 288 return False 289 290 if pre_info["passwd"].startswith("!"): 291 return True 292 293 cmd = ["passwd"] 294 295 if root is not None: 296 cmd.extend(("-R", root)) 297 298 cmd.extend(("-l", name)) 299 300 __salt__["cmd.run"](cmd, python_shell=False) 301 return info(name, root=root)["passwd"].startswith("!") 302 303 304def unlock_password(name, root=None): 305 """ 306 .. versionadded:: 2016.11.0 307 308 Unlock the password from name user 309 310 name 311 User to unlock 312 313 root 314 Directory to chroot into 315 316 CLI Example: 317 318 .. code-block:: bash 319 320 salt '*' shadow.unlock_password username 321 """ 322 pre_info = info(name, root=root) 323 if not pre_info["name"]: 324 return False 325 326 if not pre_info["passwd"].startswith("!"): 327 return True 328 329 cmd = ["passwd"] 330 331 if root is not None: 332 cmd.extend(("-R", root)) 333 334 cmd.extend(("-u", name)) 335 336 __salt__["cmd.run"](cmd, python_shell=False) 337 return not info(name, root=root)["passwd"].startswith("!") 338 339 340def set_password(name, password, use_usermod=False, root=None): 341 """ 342 Set the password for a named user. The password must be a properly defined 343 hash. The password hash can be generated with this command: 344 345 ``python -c "import crypt; print crypt.crypt('password', 346 '\\$6\\$SALTsalt')"`` 347 348 ``SALTsalt`` is the 8-character crpytographic salt. Valid characters in the 349 salt are ``.``, ``/``, and any alphanumeric character. 350 351 Keep in mind that the $6 represents a sha512 hash, if your OS is using a 352 different hashing algorithm this needs to be changed accordingly 353 354 name 355 User to set the password 356 357 password 358 Password already hashed 359 360 use_usermod 361 Use usermod command to better compatibility 362 363 root 364 Directory to chroot into 365 366 CLI Example: 367 368 .. code-block:: bash 369 370 salt '*' shadow.set_password root '$1$UYCIxa628.9qXjpQCjM4a..' 371 """ 372 if __salt__["cmd.retcode"](["id", name], ignore_retcode=True) != 0: 373 log.warning("user %s does not exist, cannot set password", name) 374 return False 375 376 if not salt.utils.data.is_true(use_usermod): 377 # Edit the shadow file directly 378 # ALT Linux uses tcb to store password hashes. More information found 379 # in manpage (http://docs.altlinux.org/manpages/tcb.5.html) 380 if __grains__["os"] == "ALT": 381 s_file = "/etc/tcb/{}/shadow".format(name) 382 else: 383 s_file = "/etc/shadow" 384 if root: 385 s_file = os.path.join(root, os.path.relpath(s_file, os.path.sep)) 386 387 ret = {} 388 if not os.path.isfile(s_file): 389 return ret 390 lines = [] 391 user_found = False 392 lstchg = str((datetime.datetime.today() - datetime.datetime(1970, 1, 1)).days) 393 with salt.utils.files.fopen(s_file, "rb") as fp_: 394 for line in fp_: 395 line = salt.utils.stringutils.to_unicode(line) 396 comps = line.strip().split(":") 397 if comps[0] == name: 398 user_found = True 399 comps[1] = password 400 comps[2] = lstchg 401 line = ":".join(comps) + "\n" 402 lines.append(line) 403 if not user_found: 404 log.warning("shadow entry not present for user %s, adding", name) 405 with salt.utils.files.fopen(s_file, "a+") as fp_: 406 fp_.write( 407 "{name}:{password}:{lstchg}::::::\n".format( 408 name=name, password=password, lstchg=lstchg 409 ) 410 ) 411 else: 412 with salt.utils.files.fopen(s_file, "w+") as fp_: 413 lines = [salt.utils.stringutils.to_str(_l) for _l in lines] 414 fp_.writelines(lines) 415 uinfo = info(name, root=root) 416 return uinfo["passwd"] == password 417 else: 418 # Use usermod -p (less secure, but more feature-complete) 419 cmd = ["usermod"] 420 if root is not None: 421 cmd.extend(("-R", root)) 422 cmd.extend(("-p", password, name)) 423 424 __salt__["cmd.run"](cmd, python_shell=False, output_loglevel="quiet") 425 uinfo = info(name, root=root) 426 return uinfo["passwd"] == password 427 428 429def set_warndays(name, warndays, root=None): 430 """ 431 Set the number of days of warning before a password change is required. 432 See man chage. 433 434 name 435 User to modify 436 437 warndays 438 Number of days of warning before a password change is required 439 440 root 441 Directory to chroot into 442 443 CLI Example: 444 445 .. code-block:: bash 446 447 salt '*' shadow.set_warndays username 7 448 """ 449 return _set_attrib(name, "warn", warndays, "-W", root=root) 450 451 452def set_date(name, date, root=None): 453 """ 454 Sets the value for the date the password was last changed to days since the 455 epoch (January 1, 1970). See man chage. 456 457 name 458 User to modify 459 460 date 461 Date the password was last changed 462 463 root 464 Directory to chroot into 465 466 CLI Example: 467 468 .. code-block:: bash 469 470 salt '*' shadow.set_date username 0 471 """ 472 return _set_attrib(name, "lstchg", date, "-d", root=root, validate=False) 473 474 475def set_expire(name, expire, root=None): 476 """ 477 .. versionchanged:: 2014.7.0 478 479 Sets the value for the date the account expires as days since the epoch 480 (January 1, 1970). Using a value of -1 will clear expiration. See man 481 chage. 482 483 name 484 User to modify 485 486 date 487 Date the account expires 488 489 root 490 Directory to chroot into 491 492 CLI Example: 493 494 .. code-block:: bash 495 496 salt '*' shadow.set_expire username -1 497 """ 498 return _set_attrib(name, "expire", expire, "-E", root=root, validate=False) 499 500 501def list_users(root=None): 502 """ 503 .. versionadded:: 2018.3.0 504 505 Return a list of all shadow users 506 507 root 508 Directory to chroot into 509 510 CLI Example: 511 512 .. code-block:: bash 513 514 salt '*' shadow.list_users 515 """ 516 if root is not None: 517 getspall = functools.partial(_getspall, root=root) 518 else: 519 getspall = functools.partial(spwd.getspall) 520 521 return sorted( 522 [ 523 user.sp_namp if hasattr(user, "sp_namp") else user.sp_nam 524 for user in getspall() 525 ] 526 ) 527 528 529def _getspnam(name, root=None): 530 """ 531 Alternative implementation for getspnam, that use only /etc/shadow 532 """ 533 root = "/" if not root else root 534 passwd = os.path.join(root, "etc/shadow") 535 with salt.utils.files.fopen(passwd) as fp_: 536 for line in fp_: 537 line = salt.utils.stringutils.to_unicode(line) 538 comps = line.strip().split(":") 539 if comps[0] == name: 540 # Generate a getspnam compatible output 541 for i in range(2, 9): 542 comps[i] = int(comps[i]) if comps[i] else -1 543 return spwd.struct_spwd(comps) 544 raise KeyError 545 546 547def _getspall(root=None): 548 """ 549 Alternative implementation for getspnam, that use only /etc/shadow 550 """ 551 root = "/" if not root else root 552 passwd = os.path.join(root, "etc/shadow") 553 with salt.utils.files.fopen(passwd) as fp_: 554 for line in fp_: 555 line = salt.utils.stringutils.to_unicode(line) 556 comps = line.strip().split(":") 557 # Generate a getspall compatible output 558 for i in range(2, 9): 559 comps[i] = int(comps[i]) if comps[i] else -1 560 yield spwd.struct_spwd(comps) 561