diff --git a/salt/modules/mysql.py b/salt/modules/mysql.py index 87e2361e2817..35276f8e3522 100644 --- a/salt/modules/mysql.py +++ b/salt/modules/mysql.py @@ -35,6 +35,8 @@ # Import python libs from __future__ import absolute_import, print_function, unicode_literals +import copy +import hashlib import time import logging import re @@ -261,6 +263,12 @@ def __virtual__(): return bool(MySQLdb), 'No python mysql client installed.' if MySQLdb is None else '' +def __mysql_hash_password(password): + _password = hashlib.sha1(password.encode()).digest() + _password = '*{0}'.format(hashlib.sha1(_password).hexdigest().upper()) + return _password + + def __check_table(name, table, **connection_args): dbc = _connect(**connection_args) if dbc is None: @@ -307,6 +315,9 @@ def __optimize_table(name, table, **connection_args): def __password_column(**connection_args): + if 'mysql.password_column'in __context__: + return __context__['mysql.password_column'] + dbc = _connect(**connection_args) if dbc is None: return 'Password' @@ -321,9 +332,34 @@ def __password_column(**connection_args): } _execute(cur, qry, args) if int(cur.rowcount) > 0: - return 'Password' + __context__['mysql.password_column'] = 'Password' + else: + __context__['mysql.password_column'] = 'authentication_string' + + return __context__['mysql.password_column'] + + +def __get_auth_plugin(user, host, **connection_args): + dbc = _connect(**connection_args) + if dbc is None: + return [] + cur = dbc.cursor(MySQLdb.cursors.DictCursor) + try: + qry = 'SELECT plugin FROM mysql.user WHERE User=%(user)s and Host=%(host)s' + args = {'user': user, 'host': host} + _execute(cur, qry, args) + except MySQLdb.OperationalError as exc: + err = 'MySQL Error {0}: {1}'.format(*exc.args) + __context__['mysql.error'] = err + log.error(err) + return 'mysql_native_password' + results = cur.fetchall() + log.debug(results) + + if results: + return results[0].get('plugin', 'mysql_native_password') else: - return 'authentication_string' + return 'mysql_native_password' def _connect(**kwargs): @@ -385,6 +421,10 @@ def _connarg(name, key=None, get_opts=True): # Ensure MySQldb knows the format we use for queries with arguments MySQLdb.paramstyle = 'pyformat' + for key in copy.deepcopy(connargs): + if not connargs[key]: + del connargs[key] + if connargs.get('passwd', True) is None: # If present but set to None. (Extreme edge case.) log.warning('MySQL password of None found. Attempting passwordless login.') connargs.pop('passwd') @@ -855,6 +895,9 @@ def version(**connection_args): salt '*' mysql.version ''' + if 'mysql.version' in __context__: + return __context__['mysql.version'] + dbc = _connect(**connection_args) if dbc is None: return '' @@ -869,7 +912,8 @@ def version(**connection_args): return '' try: - return salt.utils.data.decode(cur.fetchone()[0]) + __context__['mysql.version'] = salt.utils.data.decode(cur.fetchone()[0]) + return __context__['mysql.version'] except IndexError: return '' @@ -1237,6 +1281,82 @@ def user_list(**connection_args): return results +def _mysql_user_exists(user, + host='localhost', + password=None, + password_hash=None, + passwordless=False, + unix_socket=False, + password_column=None, + auth_plugin='mysql_native_password', + **connection_args): + + server_version = salt.utils.data.decode(version(**connection_args)) + compare_version = '8.0.11' + qry = ('SELECT User,Host FROM mysql.user WHERE User = %(user)s AND ' + 'Host = %(host)s') + args = {} + args['user'] = user + args['host'] = host + + if salt.utils.data.is_true(passwordless): + if salt.utils.data.is_true(unix_socket): + qry += ' AND plugin=%(unix_socket)s' + args['unix_socket'] = 'auth_socket' + else: + qry += ' AND ' + password_column + ' = \'\'' + elif password: + if salt.utils.versions.version_cmp(server_version, compare_version) >= 0: + if auth_plugin == 'mysql_native_password': + _password = __mysql_hash_password(six.text_type(password)) + qry += ' AND ' + password_column + ' = %(password)s' + args['password'] = six.text_type(_password) + else: + err = 'Unable to verify password.' + log.error(err) + __context__['mysql.error'] = err + else: + qry += ' AND ' + password_column + ' = PASSWORD(%(password)s)' + args['password'] = six.text_type(password) + elif password_hash: + qry += ' AND ' + password_column + ' = %(password)s' + args['password'] = password_hash + + return qry, args + + +def _mariadb_user_exists(user, + host='localhost', + password=None, + password_hash=None, + passwordless=False, + unix_socket=False, + password_column=None, + auth_plugin='mysql_native_password', + **connection_args): + + qry = ('SELECT User,Host FROM mysql.user WHERE User = %(user)s AND ' + 'Host = %(host)s') + args = {} + args['user'] = user + args['host'] = host + + if salt.utils.data.is_true(passwordless): + if salt.utils.data.is_true(unix_socket): + qry += ' AND plugin=%(unix_socket)s' + args['unix_socket'] = 'unix_socket' + else: + qry += ' AND ' + password_column + ' = \'\'' + elif password: + qry += ' AND ' + password_column + ' = PASSWORD(%(password)s)' + args['password'] = six.text_type(password) + elif password_hash: + qry += ' AND ' + password_column + ' = %(password)s' + args['password'] = password_hash + + return qry, args + + def user_exists(user, host='localhost', password=None, @@ -1269,7 +1389,6 @@ def user_exists(user, err = 'MySQL Error: Unable to fetch current server version. Last error was: "{}"'.format(last_err) log.error(err) return False - compare_version = '10.2.0' if 'MariaDB' in server_version else '8.0.11' dbc = _connect(**connection_args) # Did we fail to connect with the user we are checking # Its password might have previously change with the same command/state @@ -1287,33 +1406,30 @@ def user_exists(user, if not password_column: password_column = __password_column(**connection_args) - cur = dbc.cursor() - qry = ('SELECT User,Host FROM mysql.user WHERE User = %(user)s AND ' - 'Host = %(host)s') - args = {} - args['user'] = user - args['host'] = host + auth_plugin = __get_auth_plugin(user, host, **connection_args) - if salt.utils.data.is_true(passwordless): - if salt.utils.data.is_true(unix_socket): - qry += ' AND plugin=%(unix_socket)s' - args['unix_socket'] = 'unix_socket' - else: - qry += ' AND ' + password_column + ' = \'\'' - elif password: - if salt.utils.versions.version_cmp(server_version, compare_version) >= 0: - run_verify = True - else: - _password = password - qry += ' AND ' + password_column + ' = PASSWORD(%(password)s)' - args['password'] = six.text_type(_password) - elif password_hash: - qry += ' AND ' + password_column + ' = %(password)s' - args['password'] = password_hash + cur = dbc.cursor() + if 'MariaDB' in server_version: + qry, args = _mariadb_user_exists(user, + host, + password, + password_hash, + passwordless, + unix_socket, + password_column=password_column, + auth_plugin=auth_plugin, + **connection_args) + else: + qry, args = _mysql_user_exists(user, + host, + password, + password_hash, + passwordless, + unix_socket, + password_column=password_column, + auth_plugin=auth_plugin, + **connection_args) - if run_verify: - if not verify_login(user, password, **connection_args): - return False try: _execute(cur, qry, args) except MySQLdb.OperationalError as exc: @@ -1358,6 +1474,100 @@ def user_info(user, host='localhost', **connection_args): return result +def _mysql_user_create(user, + host='localhost', + password=None, + password_hash=None, + allow_passwordless=False, + unix_socket=False, + password_column=None, + auth_plugin='mysql_native_password', + **connection_args): + + server_version = salt.utils.data.decode(version(**connection_args)) + compare_version = '8.0.11' + + qry = 'CREATE USER %(user)s@%(host)s' + args = {} + args['user'] = user + args['host'] = host + if password is not None: + if salt.utils.versions.version_cmp(server_version, compare_version) >= 0: + args['auth_plugin'] = auth_plugin + qry += ' IDENTIFIED WITH %(auth_plugin)s BY %(password)s' + else: + qry += ' IDENTIFIED BY %(password)s' + args['password'] = six.text_type(password) + elif password_hash is not None: + if salt.utils.versions.version_cmp(server_version, compare_version) >= 0: + qry += ' IDENTIFIED BY %(password)s' + else: + qry += ' IDENTIFIED BY PASSWORD %(password)s' + args['password'] = password_hash + elif salt.utils.data.is_true(allow_passwordless): + if not plugin_status('auth_socket', **connection_args): + err = 'The auth_socket plugin is not enabled.' + log.error(err) + __context__['mysql.error'] = err + qry = False + else: + if salt.utils.data.is_true(unix_socket): + if host == 'localhost': + qry += ' IDENTIFIED WITH auth_socket' + else: + log.error( + 'Auth via unix_socket can be set only for host=localhost' + ) + else: + log.error('password or password_hash must be specified, unless ' + 'allow_passwordless=True') + qry = False + + return qry, args + + +def _mariadb_user_create(user, + host='localhost', + password=None, + password_hash=None, + allow_passwordless=False, + unix_socket=False, + password_column=None, + auth_plugin='mysql_native_password', + **connection_args): + + qry = 'CREATE USER %(user)s@%(host)s' + args = {} + args['user'] = user + args['host'] = host + if password is not None: + qry += ' IDENTIFIED BY %(password)s' + args['password'] = six.text_type(password) + elif password_hash is not None: + qry += ' IDENTIFIED BY PASSWORD %(password)s' + args['password'] = password_hash + elif salt.utils.data.is_true(allow_passwordless): + if not plugin_status('unix_socket', **connection_args): + err = 'The unix_socket plugin is not enabled.' + log.error(err) + __context__['mysql.error'] = err + qry = False + else: + if salt.utils.data.is_true(unix_socket): + if host == 'localhost': + qry += ' IDENTIFIED VIA unix_socket' + else: + log.error( + 'Auth via unix_socket can be set only for host=localhost' + ) + else: + log.error('password or password_hash must be specified, unless ' + 'allow_passwordless=True') + qry = False + + return qry, args + + def user_create(user, host='localhost', password=None, @@ -1365,6 +1575,7 @@ def user_create(user, allow_passwordless=False, unix_socket=False, password_column=None, + auth_plugin='mysql_native_password', **connection_args): ''' Creates a MySQL user @@ -1396,6 +1607,12 @@ def user_create(user, unix_socket If ``True`` and allow_passwordless is ``True`` then will be used unix_socket auth plugin. + password_column + The password column to use in the user table. + + auth_plugin + The authentication plugin to use, default is to use the mysql_native_password plugin. + .. versionadded:: 0.16.2 The ``allow_passwordless`` option was added. @@ -1413,7 +1630,7 @@ def user_create(user, err = 'MySQL Error: Unable to fetch current server version. Last error was: "{}"'.format(last_err) log.error(err) return False - compare_version = '10.2.0' if 'MariaDB' in server_version else '8.0.11' + if user_exists(user, host, **connection_args): log.info('User \'%s\'@\'%s\' already exists', user, host) return False @@ -1426,34 +1643,29 @@ def user_create(user, password_column = __password_column(**connection_args) cur = dbc.cursor() - qry = 'CREATE USER %(user)s@%(host)s' - args = {} - args['user'] = user - args['host'] = host - if password is not None: - qry += ' IDENTIFIED BY %(password)s' - args['password'] = six.text_type(password) - elif password_hash is not None: - if salt.utils.versions.version_cmp(server_version, compare_version) >= 0: - if 'MariaDB' in server_version: - qry += ' IDENTIFIED BY PASSWORD %(password)s' - else: - qry += ' IDENTIFIED BY %(password)s' - else: - qry += ' IDENTIFIED BY PASSWORD %(password)s' - args['password'] = password_hash - elif salt.utils.data.is_true(allow_passwordless): - if salt.utils.data.is_true(unix_socket): - if host == 'localhost': - qry += ' IDENTIFIED VIA unix_socket' - else: - log.error( - 'Auth via unix_socket can be set only for host=localhost' - ) + if 'MariaDB' in server_version: + qry, args = _mariadb_user_create(user, + host, + password, + password_hash, + allow_passwordless, + unix_socket, + password_column=password_column, + auth_plugin=auth_plugin, + **connection_args) else: - log.error('password or password_hash must be specified, unless ' - 'allow_passwordless=True') - return False + qry, args = _mysql_user_create(user, + host, + password, + password_hash, + allow_passwordless, + unix_socket, + password_column=password_column, + auth_plugin=auth_plugin, + **connection_args) + + if isinstance(qry, bool): + return qry try: _execute(cur, qry, args) @@ -1463,7 +1675,12 @@ def user_create(user, log.error(err) return False - if user_exists(user, host, password, password_hash, password_column=password_column, **connection_args): + if user_exists(user, + host, + password, + password_hash, + password_column=password_column, + **connection_args): msg = 'User \'{0}\'@\'{1}\' has been created'.format(user, host) if not any((password, password_hash)): msg += ' with passwordless login' @@ -1474,6 +1691,121 @@ def user_create(user, return False +def _mysql_user_chpass(user, + host='localhost', + password=None, + password_hash=None, + allow_passwordless=False, + unix_socket=None, + password_column=None, + auth_plugin='mysql_native_password', + **connection_args): + server_version = salt.utils.data.decode(version(**connection_args)) + compare_version = '8.0.11' + + args = {} + + if password is not None: + if salt.utils.versions.version_cmp(server_version, compare_version) >= 0: + password_sql = '%(password)s' + else: + password_sql = 'PASSWORD(%(password)s)' + args['password'] = password + elif password_hash is not None: + password_sql = '%(password)s' + args['password'] = password_hash + elif not salt.utils.data.is_true(allow_passwordless): + log.error('password or password_hash must be specified, unless ' + 'allow_passwordless=True') + return False + else: + password_sql = '\'\'' + + args['user'] = user + args['host'] = host + + if salt.utils.versions.version_cmp(server_version, compare_version) >= 0: + qry = "ALTER USER %(user)s@%(host)s IDENTIFIED BY %(password)s;" + else: + qry = ('UPDATE mysql.user SET ' + password_column + '=' + password_sql + + ' WHERE User=%(user)s AND Host = %(host)s;') + if salt.utils.data.is_true(allow_passwordless) and \ + salt.utils.data.is_true(unix_socket): + if host == 'localhost': + if not plugin_status('auth_socket', **connection_args): + err = 'The auth_socket plugin is not enabled.' + log.error(err) + __context__['mysql.error'] = err + qry = False + else: + args['unix_socket'] = 'auth_socket' + if salt.utils.versions.version_cmp(server_version, compare_version) >= 0: + qry = "ALTER USER %(user)s@%(host)s IDENTIFIED WITH %(unix_socket)s AS %(user)s;" + else: + qry = ('UPDATE mysql.user SET ' + password_column + '=' + + password_sql + ', plugin=%(unix_socket)s' + + ' WHERE User=%(user)s AND Host = %(host)s;') + else: + log.error('Auth via unix_socket can be set only for host=localhost') + + return qry, args + + +def _mariadb_user_chpass(user, + host='localhost', + password=None, + password_hash=None, + allow_passwordless=False, + unix_socket=None, + password_column=None, + auth_plugin='mysql_native_password', + **connection_args): + + server_version = salt.utils.data.decode(version(**connection_args)) + compare_version = '10.4.0' + + args = {} + + if password is not None: + password_sql = 'PASSWORD(%(password)s)' + args['password'] = password + elif password_hash is not None: + password_sql = '%(password)s' + args['password'] = password_hash + elif not salt.utils.data.is_true(allow_passwordless): + log.error('password or password_hash must be specified, unless ' + 'allow_passwordless=True') + return False + else: + password_sql = '\'\'' + + args['user'] = user + args['host'] = host + + if salt.utils.versions.version_cmp(server_version, compare_version) >= 0: + qry = "ALTER USER %(user)s@%(host)s IDENTIFIED BY %(password)s;" + else: + qry = ('UPDATE mysql.user SET ' + password_column + '=' + password_sql + + ' WHERE User=%(user)s AND Host = %(host)s;') + if salt.utils.data.is_true(allow_passwordless) and \ + salt.utils.data.is_true(unix_socket): + if host == 'localhost': + if not plugin_status('unix_socket', **connection_args): + err = 'The unix_socket plugin is not enabled.' + log.error(err) + __context__['mysql.error'] = err + qry = False + else: + args['unix_socket'] = 'unix_socket' + qry = ('UPDATE mysql.user SET ' + password_column + '=' + + password_sql + ', plugin=%(unix_socket)s' + + ' WHERE User=%(user)s AND Host = %(host)s;') + else: + log.error('Auth via unix_socket can be set only for host=localhost') + + return qry, args + + def user_chpass(user, host='localhost', password=None, @@ -1526,54 +1858,44 @@ def user_chpass(user, err = 'MySQL Error: Unable to fetch current server version. Last error was: "{}"'.format(last_err) log.error(err) return False - compare_version = '10.2.0' if 'MariaDB' in server_version else '8.0.11' - args = {} - if password is not None: - if salt.utils.versions.version_cmp(server_version, compare_version) >= 0: - password_sql = '%(password)s' - else: - password_sql = 'PASSWORD(%(password)s)' - args['password'] = password - elif password_hash is not None: - password_sql = '%(password)s' - args['password'] = password_hash - elif not salt.utils.data.is_true(allow_passwordless): - log.error('password or password_hash must be specified, unless ' - 'allow_passwordless=True') + + if not user_exists(user, host, **connection_args): + log.info('User \'%s\'@\'%s\' does not exists', user, host) return False - else: - password_sql = '\'\'' dbc = _connect(**connection_args) + if dbc is None: return False if not password_column: password_column = __password_column(**connection_args) + auth_plugin = __get_auth_plugin(user, host, **connection_args) + cur = dbc.cursor() - args['user'] = user - args['host'] = host - if salt.utils.versions.version_cmp(server_version, compare_version) >= 0: - if 'MariaDB' in server_version and password_hash is not None: - qry = "ALTER USER %(user)s@%(host)s IDENTIFIED BY PASSWORD %(password)s;" - else: - qry = "ALTER USER %(user)s@%(host)s IDENTIFIED BY %(password)s;" + + if 'MariaDB' in server_version: + qry, args = _mariadb_user_chpass(user, + host, + password, + password_hash, + allow_passwordless, + unix_socket, + password_column=password_column, + auth_plugin=auth_plugin, + **connection_args) else: - qry = ('UPDATE mysql.user SET ' + password_column + '=' + password_sql + - ' WHERE User=%(user)s AND Host = %(host)s;') - if salt.utils.data.is_true(allow_passwordless) and \ - salt.utils.data.is_true(unix_socket): - if host == 'localhost': - args['unix_socket'] = 'auth_socket' - if salt.utils.versions.version_cmp(server_version, compare_version) >= 0: - qry = "ALTER USER %(user)s@%(host)s IDENTIFIED WITH %(unix_socket)s AS %(user)s;" - else: - qry = ('UPDATE mysql.user SET ' + password_column + '=' - + password_sql + ', plugin=%(unix_socket)s' + - ' WHERE User=%(user)s AND Host = %(host)s;') - else: - log.error('Auth via unix_socket can be set only for host=localhost') + qry, args = _mysql_user_chpass(user, + host, + password, + password_hash, + allow_passwordless, + unix_socket, + password_column=password_column, + auth_plugin=auth_plugin, + **connection_args) + try: result = _execute(cur, qry, args) except MySQLdb.OperationalError as exc: @@ -1582,8 +1904,17 @@ def user_chpass(user, log.error(err) return False + compare_version = '10.4.0' if 'MariaDB' in server_version else '8.0.11' + res = False if salt.utils.versions.version_cmp(server_version, compare_version) >= 0: _execute(cur, 'FLUSH PRIVILEGES;') + res = True + else: + if result: + _execute(cur, 'FLUSH PRIVILEGES;') + res = True + + if res: log.info( 'Password for user \'%s\'@\'%s\' has been %s', user, host, @@ -1591,21 +1922,12 @@ def user_chpass(user, ) return True else: - if result: - _execute(cur, 'FLUSH PRIVILEGES;') - log.info( - 'Password for user \'%s\'@\'%s\' has been %s', - user, host, - 'changed' if any((password, password_hash)) else 'cleared' - ) - return True - - log.info( - 'Password for user \'%s\'@\'%s\' was not %s', - user, host, - 'changed' if any((password, password_hash)) else 'cleared' - ) - return False + log.info( + 'Password for user \'%s\'@\'%s\' was not %s', + user, host, + 'changed' if any((password, password_hash)) else 'cleared' + ) + return False def user_remove(user, @@ -1620,6 +1942,12 @@ def user_remove(user, salt '*' mysql.user_remove frank localhost ''' + if not user_exists(user, host, **connection_args): + err = 'User \'%s\'@\'%s\' does not exists', user, host + __context__['mysql.error'] = err + log.info(err) + return False + dbc = _connect(**connection_args) if dbc is None: return False @@ -2363,3 +2691,153 @@ def verify_login(user, password=None, **connection_args): del __context__['mysql.error'] return False return True + + +def plugins_list(**connection_args): + ''' + Return a list of plugins and their status + from the ``SHOW PLUGINS`` query. + + CLI Example: + + .. code-block:: bash + + salt '*' mysql.plugins_list + ''' + dbc = _connect(**connection_args) + if dbc is None: + return [] + cur = dbc.cursor() + qry = 'SHOW PLUGINS' + try: + _execute(cur, qry) + except MySQLdb.OperationalError as exc: + err = 'MySQL Error {0}: {1}'.format(*exc.args) + __context__['mysql.error'] = err + log.error(err) + return [] + + ret = [] + results = cur.fetchall() + for dbs in results: + ret.append({'name': dbs[0], 'status': dbs[1]}) + + log.debug(ret) + return ret + + +def plugin_add(name, soname=None, **connection_args): + ''' + Add a plugina. + + CLI Example: + + .. code-block:: bash + + salt '*' mysql.plugin_add auth_socket + ''' + if not name: + log.error('Plugin name is required.') + return False + + if plugin_status(name, **connection_args): + log.error('Plugin %s is already installed.', name) + return True + + dbc = _connect(**connection_args) + if dbc is None: + return False + cur = dbc.cursor() + qry = 'INSTALL PLUGIN {0}'.format(name) + + if soname: + qry += ' SONAME "{0}"'.format(soname) + else: + qry += ' SONAME "{0}.so"'.format(name) + + try: + _execute(cur, qry) + except MySQLdb.OperationalError as exc: + err = 'MySQL Error {0}: {1}'.format(*exc.args) + __context__['mysql.error'] = err + log.error(err) + return False + + return True + + +def plugin_remove(name, **connection_args): + ''' + Remove a plugin. + + CLI Example: + + .. code-block:: bash + + salt '*' mysql.plugin_remove auth_socket + ''' + if not name: + log.error('Plugin name is required.') + return False + + if not plugin_status(name, **connection_args): + log.error('Plugin %s is not installed.', name) + return True + + dbc = _connect(**connection_args) + if dbc is None: + return False + cur = dbc.cursor() + qry = 'UNINSTALL PLUGIN {0}'.format(name) + args = {} + args['name'] = name + + try: + _execute(cur, qry) + except MySQLdb.OperationalError as exc: + err = 'MySQL Error {0}: {1}'.format(*exc.args) + __context__['mysql.error'] = err + log.error(err) + return False + + return True + + +def plugin_status(name, **connection_args): + ''' + Return the status of a plugin. + + CLI Example: + + .. code-block:: bash + + salt '*' mysql.plugin_status auth_socket + ''' + if not name: + log.error('Plugin name is required.') + return False + + dbc = _connect(**connection_args) + if dbc is None: + return '' + cur = dbc.cursor() + qry = 'SELECT PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME = %(name)s' + args = {} + args['name'] = name + + try: + _execute(cur, qry, args) + except MySQLdb.OperationalError as exc: + err = 'MySQL Error {0}: {1}'.format(*exc.args) + __context__['mysql.error'] = err + log.error(err) + return '' + + try: + status = cur.fetchone() + if status is None: + return '' + else: + return status[0] + except IndexError: + return '' diff --git a/salt/states/mysql_user.py b/salt/states/mysql_user.py index 88d92afc6421..43e7cb5ba9c1 100644 --- a/salt/states/mysql_user.py +++ b/salt/states/mysql_user.py @@ -73,6 +73,7 @@ def present(name, allow_passwordless=False, unix_socket=False, password_column=None, + auth_plugin='mysql_native_password', **connection_args): ''' Ensure that the named user is present with the specified properties. A @@ -127,7 +128,11 @@ def present(name, ret['result'] = False return ret else: - if __salt__['mysql.user_exists'](name, host, passwordless=True, unix_socket=unix_socket, password_column=password_column, + if __salt__['mysql.user_exists'](name, + host, + passwordless=True, + unix_socket=unix_socket, + password_column=password_column, **connection_args): ret['comment'] += ' with passwordless login' return ret @@ -138,11 +143,19 @@ def present(name, ret['result'] = False return ret else: - if __salt__['mysql.user_exists'](name, host, password, password_hash, unix_socket=unix_socket, password_column=password_column, + if __salt__['mysql.user_exists'](name, + host, + password, + password_hash, + unix_socket=unix_socket, + password_column=password_column, **connection_args): - ret['comment'] += ' with the desired password' - if password_hash and not password: - ret['comment'] += ' hash' + if auth_plugin == 'mysql_native_password': + ret['comment'] += ' with the desired password' + if password_hash and not password: + ret['comment'] += ' hash' + else: + ret['comment'] += '. Unable to verify password.' return ret else: err = _get_mysql_error() @@ -152,7 +165,10 @@ def present(name, return ret # check if user exists with a different password - if __salt__['mysql.user_exists'](name, host, unix_socket=unix_socket, **connection_args): + if __salt__['mysql.user_exists'](name, + host, + unix_socket=unix_socket, + **connection_args): # The user is present, change the password if __opts__['test']: @@ -168,9 +184,12 @@ def present(name, ret['comment'] += 'changed' return ret - if __salt__['mysql.user_chpass'](name, host, - password, password_hash, - allow_passwordless, unix_socket, + if __salt__['mysql.user_chpass'](name, + host, + password, + password_hash, + allow_passwordless, + unix_socket, **connection_args): ret['comment'] = \ 'Password for user {0}@{1} has been ' \ @@ -209,9 +228,14 @@ def present(name, ret['result'] = False return ret - if __salt__['mysql.user_create'](name, host, - password, password_hash, - allow_passwordless, unix_socket=unix_socket, password_column=password_column, + if __salt__['mysql.user_create'](name, + host, + password, + password_hash, + allow_passwordless, + unix_socket=unix_socket, + password_column=password_column, + auth_plugin=auth_plugin, **connection_args): ret['comment'] = \ 'The user {0}@{1} has been added'.format(name, host) diff --git a/tests/unit/modules/test_mysql.py b/tests/unit/modules/test_mysql.py index 39495120687a..4529336220d7 100644 --- a/tests/unit/modules/test_mysql.py +++ b/tests/unit/modules/test_mysql.py @@ -127,23 +127,28 @@ def test_user_exists(self): ) with patch.object(mysql, 'version', return_value='8.0.11'): - self._test_call(mysql.user_exists, - {'sql': ('SELECT User,Host FROM mysql.user WHERE ' - 'User = %(user)s AND Host = %(host)s'), - 'sql_args': {'host': '%', - 'user': 'mytestuser' - } - }, - user='mytestuser', - host='%', - password='BLUECOW' - ) + with patch.object(mysql, '__get_auth_plugin', MagicMock(return_value='mysql_native_password')): + self._test_call(mysql.user_exists, + {'sql': ('SELECT User,Host FROM mysql.user WHERE ' + 'User = %(user)s AND Host = %(host)s AND ' + 'Password = %(password)s'), + 'sql_args': {'host': '%', + 'password': '*1A01CF8FBE6425398935FB90359AD8B817399102', + 'user': 'mytestuser' + } + }, + user='mytestuser', + host='%', + password='BLUECOW' + ) with patch.object(mysql, 'version', return_value='10.2.21-MariaDB'): self._test_call(mysql.user_exists, {'sql': ('SELECT User,Host FROM mysql.user WHERE ' - 'User = %(user)s AND Host = %(host)s'), + 'User = %(user)s AND Host = %(host)s AND ' + 'Password = PASSWORD(%(password)s)'), 'sql_args': {'host': 'localhost', + 'password': 'BLUECOW', 'user': 'mytestuser' } }, @@ -175,16 +180,59 @@ def test_user_create(self): ''' Test the creation of a MySQL user in mysql exec module ''' - self._test_call(mysql.user_create, - {'sql': 'CREATE USER %(user)s@%(host)s IDENTIFIED BY %(password)s', - 'sql_args': {'password': 'BLUECOW', - 'user': 'testuser', - 'host': 'localhost', - } - }, - 'testuser', - password='BLUECOW' - ) + with patch.object(mysql, 'version', return_value='8.0.10'): + with patch.object(mysql, '__get_auth_plugin', MagicMock(return_value='mysql_native_password')): + self._test_call(mysql.user_create, + {'sql': 'CREATE USER %(user)s@%(host)s IDENTIFIED BY %(password)s', + 'sql_args': {'password': 'BLUECOW', + 'user': 'testuser', + 'host': 'localhost', + } + }, + 'testuser', + password='BLUECOW' + ) + + with patch.object(mysql, 'version', return_value='8.0.11'): + with patch.object(mysql, '__get_auth_plugin', MagicMock(return_value='mysql_native_password')): + self._test_call(mysql.user_create, + {'sql': 'CREATE USER %(user)s@%(host)s IDENTIFIED WITH %(auth_plugin)s BY %(password)s', + 'sql_args': {'password': 'BLUECOW', + 'auth_plugin': 'mysql_native_password', + 'user': 'testuser', + 'host': 'localhost', + } + }, + 'testuser', + password='BLUECOW' + ) + + # Test creating a user with passwordless=True and unix_socket=True + with patch.object(mysql, 'version', return_value='8.0.10'): + with patch.object(mysql, 'plugin_status', MagicMock(return_value='ACTIVE')): + self._test_call(mysql.user_create, + {'sql': 'CREATE USER %(user)s@%(host)s IDENTIFIED WITH auth_socket', + 'sql_args': {'user': 'testuser', + 'host': 'localhost', + } + }, + 'testuser', + allow_passwordless=True, + unix_socket=True, + ) + + with patch.object(mysql, 'version', return_value='10.2.21-MariaDB'): + with patch.object(mysql, 'plugin_status', MagicMock(return_value='ACTIVE')): + self._test_call(mysql.user_create, + {'sql': 'CREATE USER %(user)s@%(host)s IDENTIFIED VIA unix_socket', + 'sql_args': {'user': 'testuser', + 'host': 'localhost', + } + }, + 'testuser', + allow_passwordless=True, + unix_socket=True, + ) def test_user_chpass(self): ''' @@ -193,49 +241,52 @@ def test_user_chpass(self): connect_mock = MagicMock() with patch.object(mysql, '_connect', connect_mock): with patch.object(mysql, 'version', return_value='8.0.10'): - with patch.dict(mysql.__salt__, {'config.option': MagicMock()}): - mysql.user_chpass('testuser', password='BLUECOW') - calls = ( - call().cursor().execute( - 'UPDATE mysql.user SET Password=PASSWORD(%(password)s) WHERE User=%(user)s AND Host = %(host)s;', - {'password': 'BLUECOW', - 'user': 'testuser', - 'host': 'localhost', - } - ), - call().cursor().execute('FLUSH PRIVILEGES;'), - ) - connect_mock.assert_has_calls(calls, any_order=True) + with patch.object(mysql, 'user_exists', MagicMock(return_value=True)): + with patch.dict(mysql.__salt__, {'config.option': MagicMock()}): + mysql.user_chpass('testuser', password='BLUECOW') + calls = ( + call().cursor().execute( + 'UPDATE mysql.user SET Password=PASSWORD(%(password)s) WHERE User=%(user)s AND Host = %(host)s;', + {'password': 'BLUECOW', + 'user': 'testuser', + 'host': 'localhost', + } + ), + call().cursor().execute('FLUSH PRIVILEGES;'), + ) + connect_mock.assert_has_calls(calls, any_order=True) connect_mock = MagicMock() with patch.object(mysql, '_connect', connect_mock): with patch.object(mysql, 'version', return_value='8.0.11'): - with patch.dict(mysql.__salt__, {'config.option': MagicMock()}): - mysql.user_chpass('testuser', password='BLUECOW') - calls = ( - call().cursor().execute( - "ALTER USER %(user)s@%(host)s IDENTIFIED BY %(password)s;", - {'password': 'BLUECOW', - 'user': 'testuser', - 'host': 'localhost', - } - ), - call().cursor().execute('FLUSH PRIVILEGES;'), - ) - connect_mock.assert_has_calls(calls, any_order=True) + with patch.object(mysql, 'user_exists', MagicMock(return_value=True)): + with patch.dict(mysql.__salt__, {'config.option': MagicMock()}): + mysql.user_chpass('testuser', password='BLUECOW') + calls = ( + call().cursor().execute( + "ALTER USER %(user)s@%(host)s IDENTIFIED BY %(password)s;", + {'password': 'BLUECOW', + 'user': 'testuser', + 'host': 'localhost', + } + ), + call().cursor().execute('FLUSH PRIVILEGES;'), + ) + connect_mock.assert_has_calls(calls, any_order=True) def test_user_remove(self): ''' Test the removal of a MySQL user in mysql exec module ''' - self._test_call(mysql.user_remove, - {'sql': 'DROP USER %(user)s@%(host)s', - 'sql_args': {'user': 'testuser', - 'host': 'localhost', - } - }, - 'testuser' - ) + with patch.object(mysql, 'user_exists', MagicMock(return_value=True)): + self._test_call(mysql.user_remove, + {'sql': 'DROP USER %(user)s@%(host)s', + 'sql_args': {'user': 'testuser', + 'host': 'localhost', + } + }, + 'testuser' + ) def test_db_check(self): ''' @@ -458,6 +509,36 @@ def test_query_error(self): expected = 'MySQL Error 9999: Something Went Wrong' self.assertEqual(mysql.__context__['mysql.error'], expected) + def test_plugin_add(self): + ''' + Test the adding/installing a MySQL / MariaDB plugin + ''' + with patch.object(mysql, 'plugin_status', MagicMock(return_value='')): + self._test_call(mysql.plugin_add, + 'INSTALL PLUGIN auth_socket SONAME "auth_socket.so"', + 'auth_socket', + ) + + def test_plugin_remove(self): + ''' + Test the removing/uninstalling a MySQL / MariaDB plugin + ''' + with patch.object(mysql, 'plugin_status', MagicMock(return_value='ACTIVE')): + self._test_call(mysql.plugin_remove, + 'UNINSTALL PLUGIN auth_socket', + 'auth_socket', + ) + + def test_plugin_status(self): + ''' + Test checking the status of a MySQL / MariaDB plugin + ''' + self._test_call(mysql.plugin_status, + {'sql': 'SELECT PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME = %(name)s', + 'sql_args': {'name': 'auth_socket'} + }, + 'auth_socket') + def _test_call(self, function, expected_sql, *args, **kwargs): connect_mock = MagicMock() with patch.object(mysql, '_connect', connect_mock):