@@ -49,12 +49,25 @@ def sql(
49
49
@operation
50
50
def user (
51
51
user ,
52
- # Desired user settings
53
52
present = True ,
54
- user_hostname = 'localhost' , password = None , privileges = None ,
53
+ user_hostname = 'localhost' ,
54
+ password = None ,
55
+ privileges = None ,
56
+ # MySQL REQUIRE SSL/TLS options
57
+ require = None , # SSL or X509
58
+ require_cipher = False ,
59
+ require_issuer = False ,
60
+ require_subject = False ,
61
+ # MySQL WITH resource limit options
62
+ max_connections = None ,
63
+ max_queries_per_hour = None ,
64
+ max_updates_per_hour = None ,
65
+ max_connections_per_hour = None ,
55
66
# Details for speaking to MySQL via `mysql` CLI via `mysql` CLI
56
- mysql_user = None , mysql_password = None ,
57
- mysql_host = None , mysql_port = None ,
67
+ mysql_user = None ,
68
+ mysql_password = None ,
69
+ mysql_host = None ,
70
+ mysql_port = None ,
58
71
state = None , host = None ,
59
72
):
60
73
'''
@@ -85,16 +98,52 @@ def user(
85
98
user='pyinfra',
86
99
password='somepassword',
87
100
)
101
+
102
+ # Create a user with resource limits
103
+ mysql.user(
104
+ name='Create the pyinfra@localhost MySQL user',
105
+ user='pyinfra',
106
+ max_connections=50,
107
+ max_updates_per_hour=10,
108
+ )
109
+
110
+ # Create a user that requires SSL for connections
111
+ mysql.user(
112
+ name='Create the pyinfra@localhost MySQL user',
113
+ user='pyinfra',
114
+ password='somepassword',
115
+ require='SSL',
116
+ )
117
+
118
+ # Create a user that requires a specific certificate
119
+ mysql.user(
120
+ name='Create the pyinfra@localhost MySQL user',
121
+ user='pyinfra',
122
+ password='somepassword',
123
+ require='X509',
124
+ require_issuer='/C=SE/ST=Stockholm...',
125
+ require_cipher='EDH-RSA-DES-CBC3-SHA',
126
+ )
88
127
'''
89
128
129
+ if require and require not in ('SSL' , 'X509' ):
130
+ raise OperationError ('Invalid `require` value, must be: "SSL" or "X509"' )
131
+
132
+ if require != 'X509' :
133
+ if require_cipher :
134
+ raise OperationError ('Cannot set `require_cipher` if `require` is not "X509"' )
135
+ if require_issuer :
136
+ raise OperationError ('Cannot set `require_issuer` if `require` is not "X509"' )
137
+ if require_subject :
138
+ raise OperationError ('Cannot set `require_subject` if `require` is not "X509"' )
139
+
90
140
current_users = host .fact .mysql_users (
91
141
mysql_user , mysql_password , mysql_host , mysql_port ,
92
142
)
93
143
94
144
user_host = '{0}@{1}' .format (user , user_hostname )
95
145
is_present = user_host in current_users
96
146
97
- # User not wanted?
98
147
if not present :
99
148
if is_present :
100
149
yield make_execute_mysql_command (
@@ -104,25 +153,124 @@ def user(
104
153
host = mysql_host ,
105
154
port = mysql_port ,
106
155
)
156
+ current_users .pop (user_host )
107
157
else :
108
158
host .noop ('mysql user {0}@{1} does not exist' .format (user , user_hostname ))
109
159
return
110
160
111
- # If we want the user and they don't exist
161
+ new_or_updated_user_fact = {
162
+ 'ssl_type' : 'ANY' if require == 'SSL' else require ,
163
+ 'ssl_cipher' : require_cipher ,
164
+ 'x509_issuer' : require_issuer ,
165
+ 'x509_subject' : require_subject ,
166
+ 'max_user_connections' : max_connections ,
167
+ 'max_questions' : max_queries_per_hour ,
168
+ 'max_updates' : max_updates_per_hour ,
169
+ 'max_connections' : max_connections_per_hour ,
170
+ }
171
+
112
172
if present and not is_present :
113
173
sql_bits = ['CREATE USER "{0}"@"{1}"' .format (user , user_hostname )]
114
174
if password :
115
175
sql_bits .append (MaskString ('IDENTIFIED BY "{0}"' .format (password )))
116
176
177
+ if require == 'SSL' :
178
+ sql_bits .append ('REQUIRE SSL' )
179
+
180
+ if require == 'X509' :
181
+ sql_bits .append ('REQUIRE' )
182
+ require_bits = []
183
+
184
+ if require_cipher :
185
+ require_bits .append ('CIPHER "{0}"' .format (require_cipher ))
186
+ if require_issuer :
187
+ require_bits .append ('ISSUER "{0}"' .format (require_issuer ))
188
+ if require_subject :
189
+ require_bits .append ('SUBJECT "{0}"' .format (require_subject ))
190
+
191
+ if not require_bits :
192
+ require_bits .append ('X509' )
193
+
194
+ sql_bits .extend (require_bits )
195
+
196
+ resource_bits = []
197
+ if max_connections :
198
+ resource_bits .append ('MAX_USER_CONNECTIONS {0}' .format (max_connections ))
199
+ if max_queries_per_hour :
200
+ resource_bits .append ('MAX_QUERIES_PER_HOUR {0}' .format (max_queries_per_hour ))
201
+ if max_updates_per_hour :
202
+ resource_bits .append ('MAX_UPDATES_PER_HOUR {0}' .format (max_updates_per_hour ))
203
+ if max_connections_per_hour :
204
+ resource_bits .append ('MAX_CONNECTIONS_PER_HOUR {0}' .format (max_connections_per_hour ))
205
+
206
+ if resource_bits :
207
+ sql_bits .append ('WITH' )
208
+ sql_bits .append (' ' .join (resource_bits ))
209
+
117
210
yield make_execute_mysql_command (
118
211
StringCommand (* sql_bits ),
119
212
user = mysql_user ,
120
213
password = mysql_password ,
121
214
host = mysql_host ,
122
215
port = mysql_port ,
123
216
)
124
- else :
125
- host .noop ('mysql user {0}@{1} exists' .format (user , user_hostname ))
217
+
218
+ current_users [user_host ] = new_or_updated_user_fact
219
+
220
+ if present and is_present :
221
+ current_user = current_users .get (user_host )
222
+
223
+ alter_bits = []
224
+
225
+ if require == 'SSL' :
226
+ if current_user ['ssl_type' ] != 'ANY' :
227
+ alter_bits .append ('REQUIRE SSL' )
228
+
229
+ if require == 'X509' :
230
+ require_bits = []
231
+
232
+ if require_cipher and current_user ['ssl_cipher' ] != require_cipher :
233
+ require_bits .append ('CIPHER "{0}"' .format (require_cipher ))
234
+ if require_issuer and current_user ['x509_issuer' ] != require_issuer :
235
+ require_bits .append ('ISSUER "{0}"' .format (require_issuer ))
236
+ if require_subject and current_user ['x509_subject' ] != require_subject :
237
+ require_bits .append ('SUBJECT "{0}"' .format (require_subject ))
238
+
239
+ if not require_bits :
240
+ if current_user ['ssl_type' ] != 'X509' :
241
+ require_bits .append ('X509' )
242
+
243
+ if require_bits :
244
+ alter_bits .append ('REQUIRE' )
245
+ alter_bits .extend (require_bits )
246
+
247
+ resource_bits = []
248
+ if max_connections and current_user ['max_user_connections' ] != max_connections :
249
+ resource_bits .append ('MAX_USER_CONNECTIONS {0}' .format (max_connections ))
250
+ if max_queries_per_hour and current_user ['max_questions' ] != max_queries_per_hour :
251
+ resource_bits .append ('MAX_QUERIES_PER_HOUR {0}' .format (max_queries_per_hour ))
252
+ if max_updates_per_hour and current_user ['max_updates' ] != max_updates_per_hour :
253
+ resource_bits .append ('MAX_UPDATES_PER_HOUR {0}' .format (max_updates_per_hour ))
254
+ if max_connections_per_hour and current_user ['max_connections' ] != max_connections_per_hour :
255
+ resource_bits .append ('MAX_CONNECTIONS_PER_HOUR {0}' .format (max_connections_per_hour ))
256
+
257
+ if resource_bits :
258
+ alter_bits .append ('WITH' )
259
+ alter_bits .append (' ' .join (resource_bits ))
260
+
261
+ if alter_bits :
262
+ sql_bits = ['ALTER USER "{0}"@"{1}"' .format (user , user_hostname )]
263
+ sql_bits .extend (alter_bits )
264
+ yield make_execute_mysql_command (
265
+ StringCommand (* sql_bits ),
266
+ user = mysql_user ,
267
+ password = mysql_password ,
268
+ host = mysql_host ,
269
+ port = mysql_port ,
270
+ )
271
+ current_user .update (new_or_updated_user_fact )
272
+ else :
273
+ host .noop ('mysql user {0}@{1} exists' .format (user , user_hostname ))
126
274
127
275
# If we're here either the user exists or we just created them; either way
128
276
# now we can check any privileges are set.
@@ -233,13 +381,17 @@ def database(
233
381
)
234
382
235
383
384
+ # TODO: make this behave like a proper state op in v2, by setting present=None as the default
385
+ # and having that mode add/remove privileges to match the provided list. Retain True/False support
386
+ # to ensure certain matches exist or not.
236
387
@operation
237
388
def privileges (
238
389
user , privileges ,
239
390
user_hostname = 'localhost' ,
240
391
database = '*' , table = '*' ,
241
392
present = True ,
242
393
flush = True ,
394
+ with_grant_option = None ,
243
395
# Details for speaking to MySQL via `mysql` CLI
244
396
mysql_user = None , mysql_password = None ,
245
397
mysql_host = None , mysql_port = None ,
@@ -253,15 +405,27 @@ def privileges(
253
405
+ user_hostname: the hostname of the user
254
406
+ database: name of the database to grant privileges to (defaults to all)
255
407
+ table: name of the table to grant privileges to (defaults to all)
256
- + present: whether these privileges should exist (False to ``REVOKE)
408
+ + present: whether these privileges should exist (False to ``REVOKE`` )
257
409
+ flush: whether to flush (and update) the privileges table after any changes
410
+ + with_grant_option: whether to add the with grant option privilege
258
411
+ mysql_*: global module arguments, see above
412
+
413
+ Note:
414
+ This operation will either ensure permissions exist or are removed for a given database
415
+ & table combination. This means when ``present=True`` it won't add/remove any permissions
416
+ that already exist but aren't passed in as ``privileges``.
259
417
'''
260
418
261
419
# Ensure we have a list
262
420
if isinstance (privileges , six .string_types ):
263
421
privileges = [privileges ]
264
422
423
+ if (
424
+ (present and with_grant_option )
425
+ or (present is False and with_grant_option is False )
426
+ ):
427
+ privileges .append ('GRANT OPTION' )
428
+
265
429
if database != '*' :
266
430
database = '`{0}`' .format (database )
267
431
@@ -281,27 +445,20 @@ def privileges(
281
445
mysql_host , mysql_port ,
282
446
)
283
447
284
- has_privileges = False
285
-
448
+ existing_privileges = []
286
449
if database_table in user_grants :
287
450
existing_privileges = [
288
451
'ALL' if privilege == 'ALL PRIVILEGES' else privilege
289
452
for privilege in user_grants [database_table ]['privileges' ]
290
453
]
291
454
292
- has_privileges = (
293
- database_table in user_grants
294
- and all (
295
- privilege in existing_privileges
296
- for privilege in privileges
297
- )
298
- )
299
-
300
455
target = action = None
301
456
302
457
# No privilege and we want it
303
458
if present :
304
- if not has_privileges :
459
+ missing_privileges = [p for p in privileges if p not in existing_privileges ]
460
+ if missing_privileges :
461
+ privileges_to_apply = missing_privileges
305
462
action = 'GRANT'
306
463
target = 'TO'
307
464
else :
@@ -310,7 +467,9 @@ def privileges(
310
467
311
468
# Permission we don't want
312
469
if not present :
313
- if has_privileges :
470
+ unwanted_privileges = [p for p in privileges if p in existing_privileges ]
471
+ if unwanted_privileges :
472
+ privileges_to_apply = unwanted_privileges
314
473
action = 'REVOKE'
315
474
target = 'FROM'
316
475
else :
@@ -323,10 +482,13 @@ def privileges(
323
482
'ON {database}.{table} '
324
483
'{target} "{user}"@"{user_hostname}"'
325
484
).format (
326
- privileges = ', ' .join (privileges ),
327
- action = action , target = target ,
328
- database = database , table = table ,
329
- user = user , user_hostname = user_hostname ,
485
+ privileges = ', ' .join (privileges_to_apply ),
486
+ action = action ,
487
+ target = target ,
488
+ database = database ,
489
+ table = table ,
490
+ user = user ,
491
+ user_hostname = user_hostname ,
330
492
)
331
493
332
494
yield make_execute_mysql_command (
0 commit comments