Merge pull request #49918 from garethgreenaway/48204_mysql_user_password_exception_part_deux

[2018.3] Fixes to MySQL module
This commit is contained in:
Nicole Thomas 2018-10-10 10:00:10 -04:00 committed by GitHub
commit 18fb5df639
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 129 additions and 51 deletions

View file

@ -35,7 +35,6 @@ Module to provide MySQL compatibility to salt.
# Import python libs
from __future__ import absolute_import, print_function, unicode_literals
import hashlib
import time
import logging
import re
@ -203,12 +202,6 @@ def __virtual__():
return (False, 'The mysql execution module cannot be loaded: neither MySQLdb nor PyMySQL is available.')
def __mysql_hash_password(password):
_password = hashlib.sha1(password).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:
@ -1210,6 +1203,7 @@ def user_exists(user,
salt '*' mysql.user_exists 'username' passwordless=True
salt '*' mysql.user_exists 'username' password_column='authentication_string'
'''
run_verify = False
server_version = version(**connection_args)
dbc = _connect(**connection_args)
# Did we fail to connect with the user we are checking
@ -1242,18 +1236,19 @@ def user_exists(user,
else:
qry += ' AND ' + password_column + ' = \'\''
elif password:
if salt.utils.versions.version_cmp(server_version, '8.0.11') <= 0:
# Hash the password before comparing
_password = __mysql_hash_password(password)
qry += ' AND ' + password_column + ' = %(password)s'
if salt.utils.versions.version_cmp(server_version, '8.0.11') >= 0:
run_verify = True
else:
_password = password
qry += ' AND ' + password_column + ' = PASSWORD(%(password)s)'
args['password'] = six.text_type(_password)
args['password'] = six.text_type(_password)
elif password_hash:
qry += ' AND ' + password_column + ' = %(password)s'
args['password'] = password_hash
if run_verify:
if not verify_login(user, host, password):
return False
try:
_execute(cur, qry, args)
except MySQLdb.OperationalError as exc:
@ -1368,7 +1363,7 @@ def user_create(user,
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, '8.0.11') <= 0:
if salt.utils.versions.version_cmp(server_version, '8.0.11') >= 0:
qry += ' IDENTIFIED BY %(password)s'
else:
qry += ' IDENTIFIED BY PASSWORD %(password)s'
@ -1454,7 +1449,7 @@ def user_chpass(user,
server_version = version(**connection_args)
args = {}
if password is not None:
if salt.utils.versions.version_cmp(server_version, '8.0.11') <= 0:
if salt.utils.versions.version_cmp(server_version, '8.0.11') >= 0:
password_sql = '%(password)s'
else:
password_sql = 'PASSWORD(%(password)s)'
@ -1477,18 +1472,28 @@ def user_chpass(user,
password_column = __password_column(**connection_args)
cur = dbc.cursor()
qry = ('UPDATE mysql.user SET ' + password_column + '='
+ password_sql +
' WHERE User=%(user)s AND Host = %(host)s;')
args['user'] = user
args['host'] = host
if salt.utils.versions.version_cmp(server_version, '8.0.11') >= 0:
qry = ("ALTER USER '" + user + "'@'" + host + "'"
" IDENTIFIED BY '" + password + "';")
args = {}
else:
qry = ('UPDATE mysql.user SET ' + password_column + '='
+ password_sql +
' WHERE User=%(user)s AND Host = %(host)s;')
args['user'] = user
args['host'] = host
if salt.utils.data.is_true(allow_passwordless) and \
salt.utils.data.is_true(unix_socket):
if host == 'localhost':
qry = ('UPDATE mysql.user SET ' + password_column + '='
+ password_sql + ', plugin=%(unix_socket)s' +
' WHERE User=%(user)s AND Host = %(host)s;')
args['unix_socket'] = 'unix_socket'
if salt.utils.versions.version_cmp(server_version, '8.0.11') >= 0:
qry = ("ALTER USER '" + user + "'@'" + host + "'"
" IDENTIFIED BY '" + password + "';")
args = {}
else:
qry = ('UPDATE mysql.user SET ' + password_column + '='
+ password_sql + ', plugin=%(unix_socket)s' +
' WHERE User=%(user)s AND Host = %(host)s;')
args['unix_socket'] = 'unix_socket'
else:
log.error('Auth via unix_socket can be set only for host=localhost')
try:
@ -1499,7 +1504,7 @@ def user_chpass(user,
log.error(err)
return False
if result:
if salt.utils.versions.version_cmp(server_version, '8.0.11') >= 0:
_execute(cur, 'FLUSH PRIVILEGES;')
log.info(
'Password for user \'%s\'@\'%s\' has been %s',
@ -1507,6 +1512,15 @@ def user_chpass(user,
'changed' if any((password, password_hash)) else 'cleared'
)
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',
@ -2216,3 +2230,29 @@ def showglobal(**connection_args):
log.debug('%s-->%s', mod, len(rtnv[0]))
return rtnv
def verify_login(user, host='localhost', password=None, **connection_args):
'''
Attempt to login using the provided credentials.
If successful, return true. Otherwise, return False.
CLI Example:
.. code-block:: bash
salt '*' mysql.verify_login root localhost password
'''
# Override the connection args
connection_args['connection_user'] = user
connection_args['connection_host'] = host
connection_args['connection_pass'] = password
dbc = _connect(**connection_args)
if dbc is None:
# Clear the mysql.error if unable to connect
# if the connection fails, we simply return False
if 'mysql.error' in __context__:
del __context__['mysql.error']
return False
return True

View file

@ -38,29 +38,53 @@ class MySQLTestCase(TestCase, LoaderModuleMockMixin):
Do it before test_user_create_when_user_exists mocks the user_exists call
'''
self._test_call(mysql.user_exists,
{'sql': ('SELECT User,Host FROM mysql.user WHERE '
'User = %(user)s AND Host = %(host)s AND '
'Password = PASSWORD(%(password)s)'),
'sql_args': {'host': 'localhost',
'password': 'BLUECOW',
'user': 'mytestuser'
}
},
user='mytestuser',
host='localhost',
password='BLUECOW'
)
with patch.object(mysql, 'version', return_value='8.0.10'):
self._test_call(mysql.user_exists,
{'sql': ('SELECT User,Host FROM mysql.user WHERE '
'User = %(user)s AND Host = %(host)s AND '
'Password = PASSWORD(%(password)s)'),
'sql_args': {'host': 'localhost',
'password': 'BLUECOW',
'user': 'mytestuser'
}
},
user='mytestuser',
host='localhost',
password='BLUECOW'
)
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': 'localhost',
'user': 'mytestuser'
}
},
user='mytestuser',
host='localhost',
password='BLUECOW'
)
# test_user_create_when_user_exists(self):
# ensure we don't try to create a user when one already exists
# mock the version of MySQL
with patch.object(mysql, 'version', MagicMock(return_value='8.0.10')):
with patch.object(mysql, 'version', return_value='8.0.10'):
with patch.object(mysql, 'user_exists', MagicMock(return_value=True)):
with patch.dict(mysql.__salt__, {'config.option': MagicMock()}):
ret = mysql.user_create('testuser')
self.assertEqual(False, ret)
# test_user_create_when_user_exists(self):
# ensure we don't try to create a user when one already exists
# mock the version of MySQL
with patch.object(mysql, 'version', return_value='8.0.11'):
with patch.object(mysql, 'user_exists', MagicMock(return_value=True)):
with patch.object(mysql, 'verify_login', MagicMock(return_value=True)):
with patch.dict(mysql.__salt__, {'config.option': MagicMock()}):
ret = mysql.user_create('testuser')
self.assertEqual(False, ret)
def test_user_create(self):
'''
Test the creation of a MySQL user in mysql exec module
@ -82,19 +106,33 @@ class MySQLTestCase(TestCase, LoaderModuleMockMixin):
'''
connect_mock = MagicMock()
with patch.object(mysql, '_connect', connect_mock):
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, '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)
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 'testuser'@'localhost' IDENTIFIED BY 'BLUECOW';"
),
call().cursor().execute('FLUSH PRIVILEGES;'),
)
connect_mock.assert_has_calls(calls, any_order=True)
def test_user_remove(self):
'''