1"""
2Management of MySQL users
3=========================
4
5:depends:   - MySQLdb Python module
6:configuration: See :py:mod:`salt.modules.mysql` for setup instructions.
7
8.. code-block:: yaml
9
10    frank:
11      mysql_user.present:
12        - host: localhost
13        - password: bobcat
14
15
16.. versionadded:: 0.16.2
17    Authentication overrides have been added.
18
19The MySQL authentication information specified in the minion config file can be
20overridden in states using the following arguments: ``connection_host``,
21``connection_port``, ``connection_user``, ``connection_pass``,
22``connection_db``, ``connection_unix_socket``, ``connection_default_file`` and
23``connection_charset``.
24
25.. code-block:: yaml
26
27    frank:
28      mysql_user.present:
29        - host: localhost
30        - password: "bob@cat"
31        - connection_user: someuser
32        - connection_pass: somepass
33        - connection_charset: utf8
34        - saltenv:
35          - LC_ALL: "en_US.utf8"
36
37
38This state is not able to grant permissions for the user. See
39:py:mod:`salt.states.mysql_grants` for further instructions.
40
41"""
42
43import sys
44
45import salt.utils.data
46
47
48def __virtual__():
49    """
50    Only load if the mysql module is in __salt__
51    """
52    if "mysql.user_create" in __salt__:
53        return True
54    return (False, "mysql module could not be loaded")
55
56
57def _get_mysql_error():
58    """
59    Look in module context for a MySQL error. Eventually we should make a less
60    ugly way of doing this.
61    """
62    return sys.modules[__salt__["test.ping"].__module__].__context__.pop(
63        "mysql.error", None
64    )
65
66
67def present(
68    name,
69    host="localhost",
70    password=None,
71    password_hash=None,
72    allow_passwordless=False,
73    unix_socket=False,
74    password_column=None,
75    auth_plugin="mysql_native_password",
76    **connection_args
77):
78    """
79    Ensure that the named user is present with the specified properties. A
80    passwordless user can be configured by omitting ``password`` and
81    ``password_hash``, and setting ``allow_passwordless`` to ``True``.
82
83    name
84        The name of the user to manage
85
86    host
87        Host for which this user/password combo applies
88
89    password
90        The password to use for this user. Will take precedence over the
91        ``password_hash`` option if both are specified.
92
93    password_hash
94        The password in hashed form. Be sure to quote the password because YAML
95        doesn't like the ``*``. A password hash can be obtained from the mysql
96        command-line client like so::
97
98            mysql> SELECT PASSWORD('mypass');
99            +-------------------------------------------+
100            | PASSWORD('mypass')                        |
101            +-------------------------------------------+
102            | *6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4 |
103            +-------------------------------------------+
104            1 row in set (0.00 sec)
105
106    allow_passwordless
107        If ``True``, then ``password`` and ``password_hash`` can be omitted to
108        permit a passwordless login.
109
110        .. versionadded:: 0.16.2
111
112    unix_socket
113        If ``True`` and allow_passwordless is ``True``, the unix_socket auth
114        plugin will be used.
115    """
116    ret = {
117        "name": name,
118        "changes": {},
119        "result": True,
120        "comment": "User {}@{} is already present".format(name, host),
121    }
122
123    passwordless = not any((password, password_hash))
124
125    # check if user exists with the same password (or passwordless login)
126    if passwordless:
127        if not salt.utils.data.is_true(allow_passwordless) and not unix_socket:
128            ret["comment"] = (
129                "Either password or password_hash must be "
130                "specified, unless allow_passwordless is True"
131            )
132            ret["result"] = False
133            return ret
134        else:
135            if __salt__["mysql.user_exists"](
136                name,
137                host,
138                passwordless=True,
139                unix_socket=unix_socket,
140                password_column=password_column,
141                **connection_args
142            ):
143                if allow_passwordless:
144                    ret["comment"] += " with passwordless login"
145                return ret
146            else:
147                err = _get_mysql_error()
148                if err is not None:
149                    ret["comment"] = err
150                    ret["result"] = False
151                    return ret
152    else:
153        if __salt__["mysql.user_exists"](
154            name,
155            host,
156            password,
157            password_hash,
158            unix_socket=unix_socket,
159            password_column=password_column,
160            **connection_args
161        ):
162            if auth_plugin == "mysql_native_password":
163                ret["comment"] += " with the desired password"
164                if password_hash and not password:
165                    ret["comment"] += " hash"
166            else:
167                ret["comment"] += ". Unable to verify password."
168            return ret
169        else:
170            err = _get_mysql_error()
171            if err is not None:
172                ret["comment"] = err
173                ret["result"] = False
174                return ret
175
176    # check if user exists with a different password
177    if __salt__["mysql.user_exists"](
178        name, host, unix_socket=unix_socket, **connection_args
179    ):
180
181        # The user is present, change the password
182        if __opts__["test"]:
183            ret["comment"] = "Password for user {}@{} is set to be ".format(name, host)
184            ret["result"] = None
185            if passwordless:
186                ret["comment"] += "cleared"
187                if not salt.utils.data.is_true(allow_passwordless):
188                    ret["comment"] += ", but allow_passwordless != True"
189                    ret["result"] = False
190            else:
191                ret["comment"] += "changed"
192            return ret
193
194        if __salt__["mysql.user_chpass"](
195            name,
196            host,
197            password,
198            password_hash,
199            allow_passwordless,
200            unix_socket,
201            **connection_args
202        ):
203            ret["comment"] = "Password for user {}@{} has been {}".format(
204                name, host, "cleared" if passwordless else "changed"
205            )
206            ret["changes"][name] = "Updated"
207        else:
208            ret["comment"] = "Failed to {} password for user {}@{}".format(
209                "clear" if passwordless else "change", name, host
210            )
211            err = _get_mysql_error()
212            if err is not None:
213                ret["comment"] += " ({})".format(err)
214            if passwordless and not salt.utils.data.is_true(allow_passwordless):
215                ret["comment"] += (
216                    ". Note: allow_passwordless must be True "
217                    "to permit passwordless login."
218                )
219            ret["result"] = False
220    else:
221
222        err = _get_mysql_error()
223        if err is not None:
224            ret["comment"] = err
225            ret["result"] = False
226            return ret
227
228        # The user is not present, make it!
229        if __opts__["test"]:
230            ret["comment"] = "User {}@{} is set to be added".format(name, host)
231            ret["result"] = None
232            if allow_passwordless:
233                ret["comment"] += " with passwordless login"
234                if not salt.utils.data.is_true(allow_passwordless):
235                    ret["comment"] += ", but allow_passwordless != True"
236                    ret["result"] = False
237            if unix_socket:
238                ret["comment"] += " using unix_socket"
239            return ret
240
241        if __salt__["mysql.user_create"](
242            name,
243            host,
244            password,
245            password_hash,
246            allow_passwordless,
247            unix_socket=unix_socket,
248            password_column=password_column,
249            auth_plugin=auth_plugin,
250            **connection_args
251        ):
252            ret["comment"] = "The user {}@{} has been added".format(name, host)
253            if allow_passwordless:
254                ret["comment"] += " with passwordless login"
255            if unix_socket:
256                ret["comment"] += " using unix_socket"
257            ret["changes"][name] = "Present"
258        else:
259            ret["comment"] = "Failed to create user {}@{}".format(name, host)
260            err = _get_mysql_error()
261            if err is not None:
262                ret["comment"] += " ({})".format(err)
263            ret["result"] = False
264
265    return ret
266
267
268def absent(name, host="localhost", **connection_args):
269    """
270    Ensure that the named user is absent
271
272    name
273        The name of the user to remove
274    """
275    ret = {"name": name, "changes": {}, "result": True, "comment": ""}
276
277    # Check if user exists, and if so, remove it
278    if __salt__["mysql.user_exists"](name, host, **connection_args):
279        if __opts__["test"]:
280            ret["result"] = None
281            ret["comment"] = "User {}@{} is set to be removed".format(name, host)
282            return ret
283        if __salt__["mysql.user_remove"](name, host, **connection_args):
284            ret["comment"] = "User {}@{} has been removed".format(name, host)
285            ret["changes"][name] = "Absent"
286            return ret
287        else:
288            err = _get_mysql_error()
289            if err is not None:
290                ret["comment"] = err
291                ret["result"] = False
292                return ret
293    else:
294        err = _get_mysql_error()
295        if err is not None:
296            ret["comment"] = err
297            ret["result"] = False
298            return ret
299
300    # fallback
301    ret["comment"] = "User {}@{} is not present, so it cannot be removed".format(
302        name, host
303    )
304    return ret
305