10
10
# Simplified BSD License (see simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
11
11
12
12
import string
13
+ import json
13
14
import re
14
15
15
16
from ansible .module_utils .six import iteritems
@@ -151,13 +152,17 @@ def get_existing_authentication(cursor, user, host):
151
152
152
153
def user_add (cursor , user , host , host_all , password , encrypted ,
153
154
plugin , plugin_hash_string , plugin_auth_string , new_priv ,
154
- tls_requires , check_mode , reuse_existing_password ):
155
+ attributes , tls_requires , reuse_existing_password , module ):
156
+ # If attributes are set, perform a sanity check to ensure server supports user attributes before creating user
157
+ if attributes and not get_attribute_support (cursor ):
158
+ module .fail_json (msg = "user attributes were specified but the server does not support user attributes" )
159
+
155
160
# we cannot create users without a proper hostname
156
161
if host_all :
157
- return {'changed' : False , 'password_changed' : False }
162
+ return {'changed' : False , 'password_changed' : False , 'attributes' : attributes }
158
163
159
- if check_mode :
160
- return {'changed' : True , 'password_changed' : None }
164
+ if module . check_mode :
165
+ return {'changed' : True , 'password_changed' : None , 'attributes' : attributes }
161
166
162
167
# Determine what user management method server uses
163
168
old_user_mgmt = impl .use_old_user_mgmt (cursor )
@@ -205,7 +210,14 @@ def user_add(cursor, user, host, host_all, password, encrypted,
205
210
privileges_grant (cursor , user , host , db_table , priv , tls_requires )
206
211
if tls_requires is not None :
207
212
privileges_grant (cursor , user , host , "*.*" , get_grants (cursor , user , host ), tls_requires )
208
- return {'changed' : True , 'password_changed' : not used_existing_password }
213
+
214
+ final_attributes = None
215
+
216
+ if attributes :
217
+ cursor .execute ("ALTER USER %s@%s ATTRIBUTE %s" , (user , host , json .dumps (attributes )))
218
+ final_attributes = attributes_get (cursor , user , host )
219
+
220
+ return {'changed' : True , 'password_changed' : not used_existing_password , 'attributes' : final_attributes }
209
221
210
222
211
223
def is_hash (password ):
@@ -218,7 +230,7 @@ def is_hash(password):
218
230
219
231
def user_mod (cursor , user , host , host_all , password , encrypted ,
220
232
plugin , plugin_hash_string , plugin_auth_string , new_priv ,
221
- append_privs , subtract_privs , tls_requires , module , role = False , maria_role = False ):
233
+ append_privs , subtract_privs , attributes , tls_requires , module , role = False , maria_role = False ):
222
234
changed = False
223
235
msg = "User unchanged"
224
236
grant_option = False
@@ -278,27 +290,26 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
278
290
if current_pass_hash != encrypted_password :
279
291
password_changed = True
280
292
msg = "Password updated"
281
- if module .check_mode :
282
- return {'changed' : True , 'msg' : msg , 'password_changed' : password_changed }
283
- if old_user_mgmt :
284
- cursor .execute ("SET PASSWORD FOR %s@%s = %s" , (user , host , encrypted_password ))
285
- msg = "Password updated (old style)"
286
- else :
287
- try :
288
- cursor .execute ("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password AS %s" , (user , host , encrypted_password ))
289
- msg = "Password updated (new style)"
290
- except (mysql_driver .Error ) as e :
291
- # https://stackoverflow.com/questions/51600000/authentication-string-of-root-user-on-mysql
292
- # Replacing empty root password with new authentication mechanisms fails with error 1396
293
- if e .args [0 ] == 1396 :
294
- cursor .execute (
295
- "UPDATE mysql.user SET plugin = %s, authentication_string = %s, Password = '' WHERE User = %s AND Host = %s" ,
296
- ('mysql_native_password' , encrypted_password , user , host )
297
- )
298
- cursor .execute ("FLUSH PRIVILEGES" )
299
- msg = "Password forced update"
300
- else :
301
- raise e
293
+ if not module .check_mode :
294
+ if old_user_mgmt :
295
+ cursor .execute ("SET PASSWORD FOR %s@%s = %s" , (user , host , encrypted_password ))
296
+ msg = "Password updated (old style)"
297
+ else :
298
+ try :
299
+ cursor .execute ("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password AS %s" , (user , host , encrypted_password ))
300
+ msg = "Password updated (new style)"
301
+ except (mysql_driver .Error ) as e :
302
+ # https://stackoverflow.com/questions/51600000/authentication-string-of-root-user-on-mysql
303
+ # Replacing empty root password with new authentication mechanisms fails with error 1396
304
+ if e .args [0 ] == 1396 :
305
+ cursor .execute (
306
+ "UPDATE mysql.user SET plugin = %s, authentication_string = %s, Password = '' WHERE User = %s AND Host = %s" ,
307
+ ('mysql_native_password' , encrypted_password , user , host )
308
+ )
309
+ cursor .execute ("FLUSH PRIVILEGES" )
310
+ msg = "Password forced update"
311
+ else :
312
+ raise e
302
313
changed = True
303
314
304
315
# Handle plugin authentication
@@ -352,9 +363,8 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
352
363
if db_table not in new_priv :
353
364
if user != "root" and "PROXY" not in priv :
354
365
msg = "Privileges updated"
355
- if module .check_mode :
356
- return {'changed' : True , 'msg' : msg , 'password_changed' : password_changed }
357
- privileges_revoke (cursor , user , host , db_table , priv , grant_option , maria_role )
366
+ if not module .check_mode :
367
+ privileges_revoke (cursor , user , host , db_table , priv , grant_option , maria_role )
358
368
changed = True
359
369
360
370
# If the user doesn't currently have any privileges on a db.table, then
@@ -363,9 +373,8 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
363
373
for db_table , priv in iteritems (new_priv ):
364
374
if db_table not in curr_priv :
365
375
msg = "New privileges granted"
366
- if module .check_mode :
367
- return {'changed' : True , 'msg' : msg , 'password_changed' : password_changed }
368
- privileges_grant (cursor , user , host , db_table , priv , tls_requires , maria_role )
376
+ if not module .check_mode :
377
+ privileges_grant (cursor , user , host , db_table , priv , tls_requires , maria_role )
369
378
changed = True
370
379
371
380
# If the db.table specification exists in both the user's current privileges
@@ -404,42 +413,82 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
404
413
405
414
if len (grant_privs ) + len (revoke_privs ) > 0 :
406
415
msg = "Privileges updated: granted %s, revoked %s" % (grant_privs , revoke_privs )
407
- if module .check_mode :
408
- return {'changed' : True , 'msg' : msg , 'password_changed' : password_changed }
409
- if len (revoke_privs ) > 0 :
410
- privileges_revoke (cursor , user , host , db_table , revoke_privs , grant_option , maria_role )
411
- if len (grant_privs ) > 0 :
412
- privileges_grant (cursor , user , host , db_table , grant_privs , tls_requires , maria_role )
416
+ if not module .check_mode :
417
+ if len (revoke_privs ) > 0 :
418
+ privileges_revoke (cursor , user , host , db_table , revoke_privs , grant_option , maria_role )
419
+ if len (grant_privs ) > 0 :
420
+ privileges_grant (cursor , user , host , db_table , grant_privs , tls_requires , maria_role )
421
+ else :
422
+ changed = True
413
423
414
424
# after privilege manipulation, compare privileges from before and now
415
425
after_priv = privileges_get (cursor , user , host , maria_role )
416
426
changed = changed or (curr_priv != after_priv )
417
427
428
+ # Handle attributes
429
+ attribute_support = get_attribute_support (cursor )
430
+ final_attributes = {}
431
+
432
+ if attributes :
433
+ if not attribute_support :
434
+ module .fail_json (msg = "user attributes were specified but the server does not support user attributes" )
435
+ else :
436
+ current_attributes = attributes_get (cursor , user , host )
437
+
438
+ if current_attributes is None :
439
+ current_attributes = {}
440
+
441
+ attributes_to_change = {}
442
+
443
+ for key , value in attributes .items ():
444
+ if key not in current_attributes or current_attributes [key ] != value :
445
+ attributes_to_change [key ] = value
446
+
447
+ if attributes_to_change :
448
+ msg = "Attributes updated: %s" % (", " .join (["%s: %s" % (key , value ) for key , value in attributes_to_change .items ()]))
449
+
450
+ # Calculate final attributes by re-running attributes_get when not in check mode, and merge dictionaries when in check mode
451
+ if not module .check_mode :
452
+ cursor .execute ("ALTER USER %s@%s ATTRIBUTE %s" , (user , host , json .dumps (attributes_to_change )))
453
+ final_attributes = attributes_get (cursor , user , host )
454
+ else :
455
+ # Final if statements excludes items whose values are None in attributes_to_change, i.e. attributes that will be deleted
456
+ final_attributes = {k : v for d in (current_attributes , attributes_to_change ) for k , v in d .items () if k not in attributes_to_change or
457
+ attributes_to_change [k ] is not None }
458
+
459
+ # Convert empty dict to None per return value requirements
460
+ final_attributes = final_attributes if final_attributes else None
461
+ changed = True
462
+ else :
463
+ final_attributes = current_attributes
464
+ else :
465
+ if attribute_support :
466
+ final_attributes = attributes_get (cursor , user , host )
467
+
418
468
if role :
419
469
continue
420
470
421
471
# Handle TLS requirements
422
472
current_requires = get_tls_requires (cursor , user , host )
423
473
if current_requires != tls_requires :
424
474
msg = "TLS requires updated"
425
- if module .check_mode :
426
- return {'changed' : True , 'msg' : msg , 'password_changed' : password_changed }
427
- if not old_user_mgmt :
428
- pre_query = "ALTER USER"
429
- else :
430
- pre_query = "GRANT %s ON *.* TO" % "," .join (get_grants (cursor , user , host ))
475
+ if not module .check_mode :
476
+ if not old_user_mgmt :
477
+ pre_query = "ALTER USER"
478
+ else :
479
+ pre_query = "GRANT %s ON *.* TO" % "," .join (get_grants (cursor , user , host ))
431
480
432
- if tls_requires is not None :
433
- query = " " .join ((pre_query , "%s@%s" ))
434
- query_with_args = mogrify_requires (query , (user , host ), tls_requires )
435
- else :
436
- query = " " .join ((pre_query , "%s@%s REQUIRE NONE" ))
437
- query_with_args = query , (user , host )
481
+ if tls_requires is not None :
482
+ query = " " .join ((pre_query , "%s@%s" ))
483
+ query_with_args = mogrify_requires (query , (user , host ), tls_requires )
484
+ else :
485
+ query = " " .join ((pre_query , "%s@%s REQUIRE NONE" ))
486
+ query_with_args = query , (user , host )
438
487
439
- cursor .execute (* query_with_args )
488
+ cursor .execute (* query_with_args )
440
489
changed = True
441
490
442
- return {'changed' : changed , 'msg' : msg , 'password_changed' : password_changed }
491
+ return {'changed' : changed , 'msg' : msg , 'password_changed' : password_changed , 'attributes' : final_attributes }
443
492
444
493
445
494
def user_delete (cursor , user , host , host_all , check_mode ):
@@ -924,6 +973,45 @@ def limit_resources(module, cursor, user, host, resource_limits, check_mode):
924
973
return True
925
974
926
975
976
+ def get_attribute_support (cursor ):
977
+ """Checks if the MySQL server supports user attributes.
978
+
979
+ Args:
980
+ cursor (cursor): DB driver cursor object.
981
+ Returns:
982
+ True if attributes are supported, False if they are not.
983
+ """
984
+ try :
985
+ # information_schema.tables does not hold the tables within information_schema itself
986
+ cursor .execute ("SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES LIMIT 0" )
987
+ cursor .fetchone ()
988
+ except mysql_driver .Error :
989
+ return False
990
+
991
+ return True
992
+
993
+
994
+ def attributes_get (cursor , user , host ):
995
+ """Get attributes for a given user.
996
+
997
+ Args:
998
+ cursor (cursor): DB driver cursor object.
999
+ user (str): User name.
1000
+ host (str): User host name.
1001
+
1002
+ Returns:
1003
+ None if the user does not exist or the user has no attributes set, otherwise a dict of attributes set on the user
1004
+ """
1005
+ cursor .execute ("SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = %s AND host = %s" , (user , host ))
1006
+
1007
+ r = cursor .fetchone ()
1008
+ # convert JSON string stored in row into a dict - mysql enforces that user_attributes entires are in JSON format
1009
+ j = json .loads (r [0 ]) if r and r [0 ] else None
1010
+
1011
+ # if the attributes dict is empty, return None instead
1012
+ return j if j else None
1013
+
1014
+
927
1015
def get_impl (cursor ):
928
1016
global impl
929
1017
cursor .execute ("SELECT VERSION()" )
0 commit comments