From aba519868f9896d76a5a0a07c9266b94ba4cb6b4 Mon Sep 17 00:00:00 2001 From: Derek Smith Date: Tue, 23 Jun 2015 15:57:18 -0500 Subject: [PATCH] updated examples added mysql 5.7 user password modification support with backwards compatibility resolved mysql server version check and differences in user authentication management explicitly state support for mysql_native_password type and no others. fixed some failing logic and updated samples updated comment to actually match logic. simplified conditionals and a little refactor --- database/mysql/mysql_user.py | 102 +++++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 34 deletions(-) diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index a4f7635e5bc..766eadb10f0 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -37,9 +37,11 @@ options: default: null password_hash: description: - - set the user's password hash (used in place of plain text password) + - Indicate that the 'password' field is a `mysql_native_password` hash required: false - default: null + choices: [ "yes", "no" ] + default: "no" + version_added: "2.0" host: description: - the 'host' part of the MySQL username @@ -123,6 +125,7 @@ notes: without providing any login_user/login_password details. The second must drop a ~/.my.cnf file containing the new root credentials. Subsequent runs of the playbook will then succeed by reading the new credentials from the file." + - Currently, there is only support for the `mysql_native_password` encryted password hash module. requirements: [ "MySQLdb" ] author: "Ansible Core Team" @@ -132,6 +135,9 @@ EXAMPLES = """ # Create database user with name 'bob' and password '12345' with all database privileges - mysql_user: name=bob password=12345 priv=*.*:ALL state=present +# Create database user with name 'bob' and previously hashed mysql native password '*EE0D72C1085C46C5278932678FBE2C6A782821B4' with all database privileges +- mysql_user: name=bob password='*EE0D72C1085C46C5278932678FBE2C6A782821B4' encrypted=yes priv=*.*:ALL state=present + # Creates database user 'bob' and password '12345' with all database privileges and 'WITH GRANT OPTION' - mysql_user: name=bob password=12345 priv=*.*:ALL,GRANT state=present @@ -212,53 +218,78 @@ def connect(module, login_user=None, login_password=None, config_file=''): db_connection = MySQLdb.connect(**config) return db_connection.cursor() +# User Authentication Management was change in MySQL 5.7 +# This is a generic check for if the server version is less than version 5.7 +def server_version_check(cursor): + cursor.execute("SELECT VERSION()"); + result = cursor.fetchone() + version_str = result[0] + version = version_str.split('.') + + if (int(version[0]) <= 5 and int(version[1]) < 7): + return True + else: + return False + def user_exists(cursor, user, host): cursor.execute("SELECT count(*) FROM user WHERE user = %s AND host = %s", (user,host)) count = cursor.fetchone() return count[0] > 0 -def user_add(cursor, user, host, password, password_hash, new_priv): - if password and not password_hash: +def user_add(cursor, user, host, password, encrypted, new_priv): + if password and encrypted: + cursor.execute("CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user,host,password)) + elif password and not encrypted: cursor.execute("CREATE USER %s@%s IDENTIFIED BY %s", (user,host,password)) - elif password_hash: - cursor.execute("CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user,host,password_hash)) if new_priv is not None: for db_table, priv in new_priv.iteritems(): privileges_grant(cursor, user,host,db_table,priv) return True - + def is_hash(password): ishash = False - if len(password) is 41 and password[0] is '*': - ishash = True - for i in password[1:]: - if i not in string.hexdigits: - ishash = False - break + if len(password) == 41 and password[0] == '*': + if frozenset(password[1:]).issubset(string.hexdigits): + ishash = True return ishash def user_mod(cursor, user, host, password, password_hash, new_priv, append_privs): changed = False grant_option = False - - # Handle passwords. - if password is not None or password_hash is not None: - cursor.execute("SELECT password FROM user WHERE user = %s AND host = %s", (user,host)) + + # Handle clear text and hashed passwords. + if bool(password): + # Determine what user management method server uses + old_user_mgmt = server_version_check(cursor) + + if old_user_mgmt: + cursor.execute("SELECT password FROM user WHERE user = %s AND host = %s", (user,host)) + else: + cursor.execute("SELECT authentication_string FROM user WHERE user = %s AND host = %s", (user,host)) current_pass_hash = cursor.fetchone() - if password: - cursor.execute("SELECT PASSWORD(%s)", (password,)) + if encrypted: + if is_hash(password): + if current_pass_hash[0] != encrypted: + if old_user_mgmt: + cursor.execute("SET PASSWORD FOR %s@%s = %s", (user, host, password)) + else: + cursor.execute("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", (user, host, password)) + changed = True + else: + module.fail_json(msg="encrypted was specified however it does not appear to be a valid hash expecting: *SHA1(SHA1(your_password))") + else: + if old_user_mgmt: + cursor.execute("SELECT PASSWORD(%s)", (password,)) + else: + cursor.execute("SELECT CONCAT('*', UCASE(SHA1(UNHEX(SHA1(%s)))))", (password,)) new_pass_hash = cursor.fetchone() if current_pass_hash[0] != new_pass_hash[0]: - cursor.execute("SET PASSWORD FOR %s@%s = PASSWORD(%s)", (user,host,password)) + if old_user_mgmt: + cursor.execute("SET PASSWORD FOR %s@%s = PASSWORD(%s)", (user, host, password)) + else: + cursor.execute("ALTER USER %s@%s IDENTIFIED BY %s", (user, host, password)) changed = True - elif password_hash: - if is_hash(password_hash): - if current_pass_hash[0] != password_hash: - cursor.execute("SET PASSWORD FOR %s@%s = %s", (user, host, password_hash)) - changed = True - else: - module.fail_json(msg="password_hash was specified however it does not appear to be a valid hash expecting: *SHA1(SHA1(your_password))") # Handle privileges if new_priv is not None: @@ -415,8 +446,8 @@ def main(): login_port=dict(default=3306, type='int'), login_unix_socket=dict(default=None), user=dict(required=True, aliases=['name']), - password=dict(default=None), - password_hash=dict(default=None), + password=dict(default=None, no_log=True), + encrypted=dict(default=False, type='bool'), host=dict(default="localhost"), state=dict(default="present", choices=["absent", "present"]), priv=dict(default=None), @@ -430,8 +461,8 @@ def main(): login_password = module.params["login_password"] user = module.params["user"] password = module.params["password"] - password_hash = module.params["password_hash"] - host = module.params["host"] + encrypted = module.boolean(module.params["encrypted"]) + host = module.params["host"].lower() state = module.params["state"] priv = module.params["priv"] check_implicit_admin = module.params['check_implicit_admin'] @@ -466,9 +497,12 @@ def main(): if user_exists(cursor, user, host): changed = user_mod(cursor, user, host, password, password_hash, priv, append_privs) else: - if password is None and password_hash is None: - module.fail_json(msg="password or password_hash parameter required when adding a user") - changed = user_add(cursor, user, host, password, password_hash, priv) + if password is None: + module.fail_json(msg="password parameter required when adding a user") + try: + changed = user_add(cursor, user, host, password, encrypted, priv) + except (SQLParseError, InvalidPrivsError, MySQLdb.Error), e: + module.fail_json(msg=str(e)) elif state == "absent": if user_exists(cursor, user, host): changed = user_delete(cursor, user, host)