1"""
2Manage the password database on BSD 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
11
12import salt.utils.files
13import salt.utils.stringutils
14from salt.exceptions import CommandExecutionError, SaltInvocationError
15
16try:
17    import pwd
18except ImportError:
19    pass
20
21try:
22    import salt.utils.pycrypto
23
24    HAS_CRYPT = True
25except ImportError:
26    HAS_CRYPT = False
27
28# Define the module's virtual name
29__virtualname__ = "shadow"
30
31
32def __virtual__():
33    if "BSD" in __grains__.get("os", ""):
34        return __virtualname__
35    return (
36        False,
37        "The bsd_shadow execution module cannot be loaded: "
38        "only available on BSD family systems.",
39    )
40
41
42def default_hash():
43    """
44    Returns the default hash used for unset passwords
45
46    CLI Example:
47
48    .. code-block:: bash
49
50        salt '*' shadow.default_hash
51    """
52    return "*" if __grains__["os"].lower() == "freebsd" else "*************"
53
54
55def gen_password(password, crypt_salt=None, algorithm="sha512"):
56    """
57    Generate hashed password
58
59    .. note::
60
61        When called this function is called directly via remote-execution,
62        the password argument may be displayed in the system's process list.
63        This may be a security risk on certain systems.
64
65    password
66        Plaintext password to be hashed.
67
68    crypt_salt
69        Crpytographic salt. If not given, a random 8-character salt will be
70        generated.
71
72    algorithm
73        The following hash algorithms are supported:
74
75        * md5
76        * blowfish (not in mainline glibc, only available in distros that add it)
77        * sha256
78        * sha512 (default)
79
80    CLI Example:
81
82    .. code-block:: bash
83
84        salt '*' shadow.gen_password 'I_am_password'
85        salt '*' shadow.gen_password 'I_am_password' crypt_salt='I_am_salt' algorithm=sha256
86    """
87    if not HAS_CRYPT:
88        raise CommandExecutionError(
89            "gen_password is not available on this operating system "
90            'because the "crypt" python module is not available.'
91        )
92    return salt.utils.pycrypto.gen_hash(crypt_salt, password, algorithm)
93
94
95def info(name):
96    """
97    Return information for the specified user
98
99    CLI Example:
100
101    .. code-block:: bash
102
103        salt '*' shadow.info someuser
104    """
105    try:
106        data = pwd.getpwnam(name)
107        ret = {"name": data.pw_name, "passwd": data.pw_passwd}
108    except KeyError:
109        return {"name": "", "passwd": ""}
110
111    if not isinstance(name, str):
112        name = str(name)
113    if ":" in name:
114        raise SaltInvocationError("Invalid username '{}'".format(name))
115
116    if __salt__["cmd.has_exec"]("pw"):
117        change, expire = __salt__["cmd.run_stdout"](
118            ["pw", "user", "show", name], python_shell=False
119        ).split(":")[5:7]
120    elif __grains__["kernel"] in ("NetBSD", "OpenBSD"):
121        try:
122            with salt.utils.files.fopen("/etc/master.passwd", "r") as fp_:
123                for line in fp_:
124                    line = salt.utils.stringutils.to_unicode(line)
125                    if line.startswith("{}:".format(name)):
126                        key = line.split(":")
127                        change, expire = key[5:7]
128                        ret["passwd"] = str(key[1])
129                        break
130        except OSError:
131            change = expire = None
132    else:
133        change = expire = None
134
135    try:
136        ret["change"] = int(change)
137    except ValueError:
138        pass
139
140    try:
141        ret["expire"] = int(expire)
142    except ValueError:
143        pass
144
145    return ret
146
147
148def set_change(name, change):
149    """
150    Sets the time at which the password expires (in seconds since the UNIX
151    epoch). See ``man 8 usermod`` on NetBSD and OpenBSD or ``man 8 pw`` on
152    FreeBSD.
153
154    A value of ``0`` sets the password to never expire.
155
156    CLI Example:
157
158    .. code-block:: bash
159
160        salt '*' shadow.set_change username 1419980400
161    """
162    pre_info = info(name)
163    if change == pre_info["change"]:
164        return True
165    if __grains__["kernel"] == "FreeBSD":
166        cmd = ["pw", "user", "mod", name, "-f", change]
167    else:
168        cmd = ["usermod", "-f", change, name]
169    __salt__["cmd.run"](cmd, python_shell=False)
170    post_info = info(name)
171    if post_info["change"] != pre_info["change"]:
172        return post_info["change"] == change
173
174
175def set_expire(name, expire):
176    """
177    Sets the time at which the account expires (in seconds since the UNIX
178    epoch). See ``man 8 usermod`` on NetBSD and OpenBSD or ``man 8 pw`` on
179    FreeBSD.
180
181    A value of ``0`` sets the account to never expire.
182
183    CLI Example:
184
185    .. code-block:: bash
186
187        salt '*' shadow.set_expire username 1419980400
188    """
189    pre_info = info(name)
190    if expire == pre_info["expire"]:
191        return True
192    if __grains__["kernel"] == "FreeBSD":
193        cmd = ["pw", "user", "mod", name, "-e", expire]
194    else:
195        cmd = ["usermod", "-e", expire, name]
196    __salt__["cmd.run"](cmd, python_shell=False)
197    post_info = info(name)
198    if post_info["expire"] != pre_info["expire"]:
199        return post_info["expire"] == expire
200
201
202def del_password(name):
203    """
204    .. versionadded:: 2015.8.2
205
206    Delete the password from name user
207
208    CLI Example:
209
210    .. code-block:: bash
211
212        salt '*' shadow.del_password username
213    """
214    cmd = "pw user mod {} -w none".format(name)
215    __salt__["cmd.run"](cmd, python_shell=False, output_loglevel="quiet")
216    uinfo = info(name)
217    return not uinfo["passwd"]
218
219
220def set_password(name, password):
221    """
222    Set the password for a named user. The password must be a properly defined
223    hash. The password hash can be generated with this command:
224
225    ``python -c "import crypt; print crypt.crypt('password', ciphersalt)"``
226
227    .. note::
228        When constructing the ``ciphersalt`` string, you must escape any dollar
229        signs, to avoid them being interpolated by the shell.
230
231    ``'password'`` is, of course, the password for which you want to generate
232    a hash.
233
234    ``ciphersalt`` is a combination of a cipher identifier, an optional number
235    of rounds, and the cryptographic salt. The arrangement and format of these
236    fields depends on the cipher and which flavor of BSD you are using. For
237    more information on this, see the manpage for ``crpyt(3)``. On NetBSD,
238    additional information is available in ``passwd.conf(5)``.
239
240    It is important to make sure that a supported cipher is used.
241
242    CLI Example:
243
244    .. code-block:: bash
245
246        salt '*' shadow.set_password someuser '$1$UYCIxa628.9qXjpQCjM4a..'
247    """
248    if __grains__.get("os", "") == "FreeBSD":
249        cmd = ["pw", "user", "mod", name, "-H", "0"]
250        stdin = password
251    else:
252        cmd = ["usermod", "-p", password, name]
253        stdin = None
254    __salt__["cmd.run"](cmd, stdin=stdin, output_loglevel="quiet", python_shell=False)
255    return info(name)["passwd"] == password
256