@ -42,6 +42,13 @@ options:
- set the user ' s password. (Required when adding a user)
required : false
default : null
encrypted :
description :
- Indicate that the ' password ' field is a ` mysql_native_password ` hash
required : false
choices : [ " yes " , " no " ]
default : " no "
version_added : " 2.0 "
host :
description :
- the ' host ' part of the MySQL username
@ -133,15 +140,19 @@ 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 : " Mark Theunissen (@marktheunissen )"
author : " Jonathan Mainguy (@Jmainguy )"
'''
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
@ -157,7 +168,7 @@ EXAMPLES = """
# Specify grants composed of more than one word
- mysql_user : name = replication password = 12345 priv = * . * : " REPLICATION CLIENT " state = present
# Revoke all privileges for user 'bob' and password '12345'
# Revoke all privileges for user 'bob' and password '12345'
- mysql_user : name = bob password = 12345 priv = * . * : USAGE state = present
# Example privileges string format
@ -176,6 +187,7 @@ password=n<_665{vS43y
import getpass
import tempfile
import re
import string
try :
import MySQLdb
except ImportError :
@ -224,6 +236,19 @@ 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 , host_all ) :
if host_all :
cursor . execute ( " SELECT count(*) FROM user WHERE user = %s " , user )
@ -233,38 +258,95 @@ def user_exists(cursor, user, host, host_all):
count = cursor . fetchone ( )
return count [ 0 ] > 0
def user_add ( cursor , user , host , host_all , password , new_priv) :
def user_add ( cursor , user , host , host_all , password , encrypted, new_priv) :
# we cannot create users without a proper hostname
if host_all :
return False
cursor . execute ( " CREATE USER %s @ %s IDENTIFIED BY %s " , ( user , host , password ) )
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 ) )
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 user_mod ( cursor , user , host , host_all , password , new_priv , append_privs ) :
def is_hash ( password ) :
ishash = False
if len ( password ) == 41 and password [ 0 ] == ' * ' :
if frozenset ( password [ 1 : ] ) . issubset ( string . hexdigits ) :
ishash = True
return ishash
def user_mod ( cursor , user , host , host_all , password , encrypted , new_priv , append_privs ) :
changed = False
grant_option = False
# to simplify code, if we have a specific host and no host_all, we create
# a list with just host and loop over that
if host_all :
hostnames = user_get_hostnames ( cursor , user )
else :
hostnames = [ host ]
for host in hostnames :
# Handle passwords
if password 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 ( )
cursor . execute ( " SELECT PASSWORD( %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 ) )
changed = True
if encrypted :
encrypted_string = ( password )
if is_hash ( password ) :
if current_pass_hash [ 0 ] != encrypted_string :
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 ] :
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
# Handle privileges
if new_priv is not None :
curr_priv = privileges_get ( cursor , user , host )
# If the user has privileges on a db.table that doesn't appear at all in
# the new specification, then revoke all privileges on it.
for db_table , priv in curr_priv . iteritems ( ) :
# If the user has the GRANT OPTION on a db.table, revoke it first.
if " GRANT " in priv :
grant_option = True
if db_table not in new_priv :
if user != " root " and " PROXY " not in priv and not append_privs :
privileges_revoke ( cursor , user , host , db_table , priv , grant_option )
changed = True
# If the user doesn't currently have any privileges on a db.table, then
# we can perform a straight grant operation.
for db_table , priv in new_priv . iteritems ( ) :
if db_table not in curr_priv :
privileges_grant ( cursor , user , host , db_table , priv )
changed = True
# Handle privileges
if new_priv is not None :
@ -440,6 +522,7 @@ def main():
user = dict ( required = True , aliases = [ ' name ' ] ) ,
user_anonymous = dict ( type = " bool " , default = " no " ) ,
password = dict ( default = None , no_log = True ) ,
encrypted = dict ( default = False , type = ' bool ' ) ,
host = dict ( default = " localhost " ) ,
host_all = dict ( type = " bool " , default = " no " ) ,
state = dict ( default = " present " , choices = [ " absent " , " present " ] ) ,
@ -455,6 +538,7 @@ def main():
user = module . params [ " user " ]
user_anonymous = module . params [ " user_anonymous " ]
password = module . params [ " password " ]
encrypted = module . boolean ( module . params [ " encrypted " ] )
host = module . params [ " host " ] . lower ( )
host_all = module . params [ " host_all " ]
state = module . params [ " state " ]
@ -494,9 +578,9 @@ def main():
if user_exists ( cursor , user , host , host_all ) :
try :
if update_password == ' always ' :
changed = user_mod ( cursor , user , host , host_all , password , priv, append_privs )
changed = user_mod ( cursor , user , host , host_all , password , encrypted, priv, append_privs )
else :
changed = user_mod ( cursor , user , host , host_all , None , priv, append_privs )
changed = user_mod ( cursor , user , host , host_all , None , encrypted, priv, append_privs )
except ( SQLParseError , InvalidPrivsError , MySQLdb . Error ) , e :
module . fail_json ( msg = str ( e ) )
@ -506,7 +590,7 @@ def main():
if host_all :
module . fail_json ( msg = " host_all parameter cannot be used when adding a user " )
try :
changed = user_add ( cursor , user , host , host_all , password , priv)
changed = user_add ( cursor , user , host , host_all , password , encrypted, priv)
except ( SQLParseError , InvalidPrivsError , MySQLdb . Error ) , e :
module . fail_json ( msg = str ( e ) )
elif state == " absent " :