From acdde856c5908f61dc4f6d75e8a9d42bc51c3ee8 Mon Sep 17 00:00:00 2001 From: Lee Hardy Date: Thu, 22 Oct 2015 13:45:50 +0100 Subject: [PATCH 1/5] - mysql: add user_anonymous parameter, which interacts with anonymous users - mysql; add host_all parameter, which forces iteration over all 'user'@... matches --- database/mysql/mysql_user.py | 162 ++++++++++++++++++++++++----------- 1 file changed, 111 insertions(+), 51 deletions(-) diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index 1ea54b41b3a..acf093f8490 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -30,6 +30,13 @@ options: description: - name of the user (role) to add or remove required: true + user_anonymous: + description: + - username is to be ignored and anonymous users with no username + handled + required: false + choices: [ "yes", "no" ] + default: no password: description: - set the user's password @@ -40,6 +47,14 @@ options: - the 'host' part of the MySQL username required: false default: localhost + host_all: + description: + - override the host option, making ansible apply changes to + all hostnames for a given user. This option cannot be used + when creating users + required: false + choices: [ "yes", "no" ] + default: "no" login_user: description: - The username used to authenticate with @@ -133,9 +148,12 @@ EXAMPLES = """ # Modify user Bob to require SSL connections. Note that REQUIRESSL is a special privilege that should only apply to *.* by itself. - mysql_user: name=bob append_privs=true priv=*.*:REQUIRESSL state=present -# Ensure no user named 'sally' exists, also passing in the auth credentials. +# Ensure no user named 'sally'@'localhost' exists, also passing in the auth credentials. - mysql_user: login_user=root login_password=123456 name=sally state=absent +# Ensure no user named 'sally' exists at all +- mysql_user: name=sally host_all=yes state=absent + # Specify grants composed of more than one word - mysql_user: name=replication password=12345 priv=*.*:"REPLICATION CLIENT" state=present @@ -206,71 +224,104 @@ def connect(module, login_user=None, login_password=None, config_file=''): db_connection = MySQLdb.connect(**config) return db_connection.cursor() -def user_exists(cursor, user, host): - cursor.execute("SELECT count(*) FROM user WHERE user = %s AND host = %s", (user,host)) +def user_exists(cursor, user, host, host_all): + if host_all: + cursor.execute("SELECT count(*) FROM user WHERE user = %s AND host = %s", (user,host)) + else: + 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, new_priv): +def user_add(cursor, user, host, host_all, password, 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 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, password, new_priv, append_privs): +def user_mod(cursor, user, host, host_all, password, new_priv, append_privs): changed = False grant_option = False - # Handle passwords - if password is not None: - cursor.execute("SELECT password 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 + # 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] - # Handle privileges - if new_priv is not None: - curr_priv = privileges_get(cursor, user,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)) + 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 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) + # 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 - # 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 - - # If the db.table specification exists in both the user's current privileges - # and in the new privileges, then we need to see if there's a difference. - db_table_intersect = set(new_priv.keys()) & set(curr_priv.keys()) - for db_table in db_table_intersect: - priv_diff = set(new_priv[db_table]) ^ set(curr_priv[db_table]) - if (len(priv_diff) > 0): - if not append_privs: - privileges_revoke(cursor, user,host,db_table,curr_priv[db_table],grant_option) - privileges_grant(cursor, user,host,db_table,new_priv[db_table]) - changed = True + # If the db.table specification exists in both the user's current privileges + # and in the new privileges, then we need to see if there's a difference. + db_table_intersect = set(new_priv.keys()) & set(curr_priv.keys()) + for db_table in db_table_intersect: + priv_diff = set(new_priv[db_table]) ^ set(curr_priv[db_table]) + if (len(priv_diff) > 0): + if not append_privs: + privileges_revoke(cursor, user,host,db_table,curr_priv[db_table],grant_option) + privileges_grant(cursor, user,host,db_table,new_priv[db_table]) + changed = True return changed -def user_delete(cursor, user, host): - cursor.execute("DROP USER %s@%s", (user, host)) +def user_delete(cursor, user, host, host_all): + if host_all: + hostnames = user_get_hostnames(cursor, user) + + for hostname in hostnames: + cursor.execute("DROP USER %s@%s", (user, hostname)) + else: + cursor.execute("DROP USER %s@%s", (user, host)) + return True +def user_get_hostnames(cursor, user): + cursor.execute("SELECT Host FROM mysql.user WHERE user = %s", user) + hostnames_raw = cursor.fetchall() + hostnames = [] + + for hostname_raw in hostnames_raw: + hostnames.append(hostname_raw[0]) + + return hostnames + def privileges_get(cursor, user,host): """ MySQL doesn't have a better method of getting privileges aside from the SHOW GRANTS query syntax, which requires us to then parse the returned string. @@ -387,8 +438,10 @@ def main(): login_port=dict(default=3306, type='int'), login_unix_socket=dict(default=None), user=dict(required=True, aliases=['name']), + user_anonymous=dict(type="bool", default="no"), password=dict(default=None, no_log=True), host=dict(default="localhost"), + host_all=dict(type="bool", default="no"), state=dict(default="present", choices=["absent", "present"]), priv=dict(default=None), append_privs=dict(default=False, type='bool'), @@ -400,8 +453,10 @@ def main(): login_user = module.params["login_user"] login_password = module.params["login_password"] user = module.params["user"] + user_anonymous = module.params["user_anonymous"] password = module.params["password"] host = module.params["host"].lower() + host_all = module.params["host_all"] state = module.params["state"] priv = module.params["priv"] check_implicit_admin = module.params['check_implicit_admin'] @@ -409,6 +464,9 @@ def main(): append_privs = module.boolean(module.params["append_privs"]) update_password = module.params['update_password'] + if user_anonymous: + user = '' + config_file = os.path.expanduser(os.path.expandvars(config_file)) if not mysqldb_found: module.fail_json(msg="the python mysqldb module is required") @@ -433,25 +491,27 @@ def main(): module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or ~/.my.cnf has the credentials. Exception message: %s" % e) if state == "present": - if user_exists(cursor, user, host): + if user_exists(cursor, user, host, host_all): try: if update_password == 'always': - changed = user_mod(cursor, user, host, password, priv, append_privs) + changed = user_mod(cursor, user, host, host_all, password, priv, append_privs) else: - changed = user_mod(cursor, user, host, None, priv, append_privs) + changed = user_mod(cursor, user, host, host_all, None, priv, append_privs) except (SQLParseError, InvalidPrivsError, MySQLdb.Error), e: module.fail_json(msg=str(e)) else: if password is None: module.fail_json(msg="password parameter required when adding a user") + if host_all: + module.fail_json(msg="host_all parameter cannot be used when adding a user") try: - changed = user_add(cursor, user, host, password, priv) + changed = user_add(cursor, user, host, host_all, password, 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) + if user_exists(cursor, user, host, host_all): + changed = user_delete(cursor, user, host, host_all) else: changed = False module.exit_json(changed=changed, user=user) From 2aeb188d81a22d030398ff4018b5cf676ca0e5f4 Mon Sep 17 00:00:00 2001 From: Lee Hardy Date: Wed, 4 Nov 2015 16:37:18 +0000 Subject: [PATCH 2/5] - fix user_exists statement with host_all to use only username parameter --- database/mysql/mysql_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index acf093f8490..d63fd41f44f 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -226,7 +226,7 @@ def connect(module, login_user=None, login_password=None, config_file=''): def user_exists(cursor, user, host, host_all): if host_all: - cursor.execute("SELECT count(*) FROM user WHERE user = %s AND host = %s", (user,host)) + cursor.execute("SELECT count(*) FROM user WHERE user = %s", user) else: cursor.execute("SELECT count(*) FROM user WHERE user = %s AND host = %s", (user,host)) From 9dd6cad22460d7595cc6caf8f28da8e09fbd9a91 Mon Sep 17 00:00:00 2001 From: Lee H Date: Mon, 14 Dec 2015 11:46:32 -0500 Subject: [PATCH 3/5] - add example showing removal of anonymous user accounts --- database/mysql/mysql_user.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index 84b52a95d3f..aa7f19a4415 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -147,6 +147,12 @@ author: "Jonathan Mainguy (@Jmainguy)" ''' EXAMPLES = """ +# Removes anonymous user account for localhost (the name parameter is required, but ignored) +- mysql_user: name=anonymous user_anonymous=yes host=localhost state=absent + +# Removes all anonymous user accounts +- mysql_user: name=anonymous user_anonymous=yes host_all=yes state=absent + # Create database user with name 'bob' and password '12345' with all database privileges - mysql_user: name=bob password=12345 priv=*.*:ALL state=present From 85a19c68bd8d5dd6c85342b66ef9b370c67bfbbf Mon Sep 17 00:00:00 2001 From: Lee H Date: Wed, 16 Dec 2015 02:03:30 -0500 Subject: [PATCH 4/5] - remove user_anonymous as the same thing can be accomplished by user='', but leave in the examples for removing anonymous users --- database/mysql/mysql_user.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index aa7f19a4415..09edf8100e7 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -30,13 +30,6 @@ options: description: - name of the user (role) to add or remove required: true - user_anonymous: - description: - - username is to be ignored and anonymous users with no username - handled - required: false - choices: [ "yes", "no" ] - default: no password: description: - set the user's password. (Required when adding a user) @@ -147,11 +140,11 @@ author: "Jonathan Mainguy (@Jmainguy)" ''' EXAMPLES = """ -# Removes anonymous user account for localhost (the name parameter is required, but ignored) -- mysql_user: name=anonymous user_anonymous=yes host=localhost state=absent +# Removes anonymous user account for localhost +- mysql_user: name='' host=localhost state=absent # Removes all anonymous user accounts -- mysql_user: name=anonymous user_anonymous=yes host_all=yes state=absent +- mysql_user: name='' host_all=yes state=absent # Create database user with name 'bob' and password '12345' with all database privileges - mysql_user: name=bob password=12345 priv=*.*:ALL state=present @@ -526,7 +519,6 @@ def main(): login_port=dict(default=3306, type='int'), login_unix_socket=dict(default=None), 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"), @@ -542,7 +534,6 @@ def main(): login_user = module.params["login_user"] login_password = module.params["login_password"] 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() @@ -554,9 +545,6 @@ def main(): append_privs = module.boolean(module.params["append_privs"]) update_password = module.params['update_password'] - if user_anonymous: - user = '' - config_file = os.path.expanduser(os.path.expandvars(config_file)) if not mysqldb_found: module.fail_json(msg="the python mysqldb module is required") From f3b2180e422eab2de74fa789be65f116070e88e8 Mon Sep 17 00:00:00 2001 From: Lee H Date: Wed, 16 Dec 2015 02:06:02 -0500 Subject: [PATCH 5/5] - add version_added as requested to host_all --- database/mysql/mysql_user.py | 1 + 1 file changed, 1 insertion(+) diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index 09edf8100e7..528f7fadd60 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -55,6 +55,7 @@ options: required: false choices: [ "yes", "no" ] default: "no" + version_added: "2.1" login_user: description: - The username used to authenticate with