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