@@ -16,16 +16,63 @@ import (
16
16
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
17
17
)
18
18
19
- type MySQLGrant struct {
19
+ type MySQLGrant interface {
20
+ }
21
+
22
+ type UserOrRole interface {
23
+ SQLString () string
24
+ }
25
+
26
+ type User struct {
27
+ User string
28
+ Host string
29
+ }
30
+
31
+ func (u User ) SQLString () string {
32
+ return fmt .Sprintf ("'%s'@'%s'" , u .User , u .Host )
33
+ }
34
+
35
+ type Role struct {
36
+ Role string
37
+ }
38
+
39
+ func (r Role ) SQLString () string {
40
+ return fmt .Sprintf ("'%s'" , r .Role )
41
+ }
42
+
43
+ type TablePrivilegeGrant struct {
20
44
Database string
21
45
Table string
22
46
Privileges []string
23
- Roles []string
24
47
Grant bool
48
+ UserOrRole UserOrRole
49
+ TLSOption string
50
+ }
51
+
52
+ type ObjectT string
53
+
54
+ var (
55
+ kProcedure ObjectT = "PROCEDURE"
56
+ kFunction ObjectT = "FUNCTION"
57
+ kTable ObjectT = "TABLE"
58
+ )
59
+
60
+ type ProcedurePrivilegeGrant struct {
61
+ Database string
62
+ ObjectT CallableType
63
+ CallableName string
64
+ Privileges []string
65
+ Grant bool
66
+ UserOrRole UserOrRole
67
+ TLSOption string
25
68
}
26
69
27
- func (m MySQLGrant ) String () string {
28
- return fmt .Sprintf ("{Database=%v,Table=%v,Privileges=%v,Roles=%v,Grant=%v}" , m .Database , m .Table , m .Privileges , m .Roles , m .Grant )
70
+ type RoleGrant struct {
71
+ Database string
72
+ Roles []string
73
+ Grant bool
74
+ UserOrRole UserOrRole
75
+ TLSOption string
29
76
}
30
77
31
78
func resourceGrant () * schema.Resource {
@@ -138,21 +185,6 @@ func formatTableName(table string) string {
138
185
return fmt .Sprintf ("`%s`" , table )
139
186
}
140
187
141
- // Formats user/host or role. Returns the formatted string and whether it is role. And an error in case it's not supported.
142
- func userOrRole (user string , host string , role string , hasRoles bool ) (string , bool , error ) {
143
- if len (user ) > 0 && len (host ) > 0 {
144
- return fmt .Sprintf ("'%s'@'%s'" , user , host ), false , nil
145
- } else if len (role ) > 0 {
146
- if ! hasRoles {
147
- return "" , false , fmt .Errorf ("roles are only supported on MySQL 8 and above" )
148
- }
149
-
150
- return fmt .Sprintf ("'%s'" , role ), true , nil
151
- } else {
152
- return "" , false , fmt .Errorf ("user with host or a role is required" )
153
- }
154
- }
155
-
156
188
func supportsRoles (ctx context.Context , meta interface {}) (bool , error ) {
157
189
currentVersion := getVersionFromMeta (ctx , meta )
158
190
@@ -161,50 +193,107 @@ func supportsRoles(ctx context.Context, meta interface{}) (bool, error) {
161
193
return hasRoles , nil
162
194
}
163
195
196
+ var kReProcedureWithoutDatabase = regexp .MustCompile (`(?i)^(function|procedure) ([^\.]*)$` )
197
+ var kReProcedureWithDatabase = regexp .MustCompile (`(?i)^(function|procedure) ([^\.]*)\.([^\.]*)$` )
198
+
199
+ func parseResource (d * schema.ResourceData ) (MySQLGrant , diag.Diagnostics ) {
200
+
201
+ // Step 1: Parse the user/role
202
+ var userOrRole UserOrRole
203
+ userAttr , userOk := d .GetOk ("user" )
204
+ hostAttr , hostOk := d .GetOk ("host" )
205
+ roleAttr , roleOk := d .GetOk ("role" )
206
+ if userOk && hostOk && userAttr .(string ) != "" && hostAttr .(string ) != "" {
207
+ userOrRole = User {
208
+ User : userAttr .(string ),
209
+ Host : hostAttr .(string ),
210
+ }
211
+ } else if roleOk && roleAttr .(string ) != "" {
212
+ userOrRole = Role {
213
+ Role : roleAttr .(string ),
214
+ }
215
+ } else {
216
+ return nil , diag .Errorf ("One of user/host or role is required" )
217
+ }
218
+
219
+ // Step 2: Get generic attributes
220
+ database := d .Get ("database" ).(string )
221
+ tlsOption := d .Get ("tls_option" ).(string )
222
+ grantOption := d .Get ("grant" ).(bool )
223
+
224
+ // Step 3a: If `roles` is specified, we have a role grant
225
+ if attr , ok := d .GetOk ("roles" ); ok {
226
+ roles := flattenList (attr .(* schema.Set ).List (), "'%s'" )
227
+ return RoleGrant {
228
+ Database : database ,
229
+ Roles : roles ,
230
+ Grant : grantOption ,
231
+ UserOrRole : userOrRole ,
232
+ TLSOption : tlsOption ,
233
+ }, nil
234
+ }
235
+
236
+ // Step 3b. If the database is a procedure or function, we have a procedure grant
237
+ if kReProcedureWithDatabase .MatchString (database ) || kReProcedureWithoutDatabase .MatchString (database ) {
238
+ var callableType CallableT
239
+ var callableName string
240
+ if kReProcedureWithDatabase .MatchString (database ) {
241
+ matches := kReProcedureWithDatabase .FindStringSubmatch (database )
242
+ callableType = CallableT (matches [1 ])
243
+ database = matches [2 ]
244
+ callableName = matches [3 ]
245
+ } else {
246
+ matches := kReProcedureWithoutDatabase .FindStringSubmatch (database )
247
+ callableType = CallableT (matches [1 ])
248
+ database = matches [2 ]
249
+ callableName = d .Get ("table" ).(string )
250
+ }
251
+ privileges := setToArray (d .Get ("privileges" ))
252
+ return ProcedurePrivilegeGrant {
253
+ Database : database ,
254
+ CallableT : callableType ,
255
+ CallableName : callableName ,
256
+ Privileges : privileges ,
257
+ Grant : grantOption ,
258
+ UserOrRole : userOrRole ,
259
+ TLSOption : tlsOption ,
260
+ }, nil
261
+ }
262
+
263
+ // Step 3c. Otherwise, we have a table grant
264
+ privileges := setToArray (d .Get ("privileges" ))
265
+ return TablePrivilegeGrant {
266
+ Database : database ,
267
+ Table : d .Get ("table" ).(string ),
268
+ Privileges : privileges ,
269
+ Grant : grantOption ,
270
+ UserOrRole : userOrRole ,
271
+ TLSOption : tlsOption ,
272
+ }, nil
273
+ }
274
+
164
275
func CreateGrant (ctx context.Context , d * schema.ResourceData , meta interface {}) diag.Diagnostics {
165
276
db , err := getDatabaseFromMeta (ctx , meta )
166
277
if err != nil {
167
278
return diag .FromErr (err )
168
279
}
169
280
170
- hasRoles , err := supportsRoles (ctx , meta )
281
+ // Parse the ResourceData
282
+ grant , err := parseResource (d )
171
283
if err != nil {
172
- return diag . Errorf ( "failed getting role support: %v" , err )
284
+ return err
173
285
}
174
286
175
- var (
176
- privilegesOrRoles string
177
- grantOn string
178
- )
179
-
180
- hasPrivs := false
181
- rolesGranted := 0
182
- if attr , ok := d .GetOk ("privileges" ); ok {
183
- privilegesOrRoles = flattenList (attr .(* schema.Set ).List (), "%s" )
184
- hasPrivs = true
185
- } else if attr , ok := d .GetOk ("roles" ); ok {
186
- if ! hasRoles {
187
- return diag .Errorf ("Roles are only supported on MySQL 8 and above" )
188
- }
189
- listOfRoles := attr .(* schema.Set ).List ()
190
- rolesGranted = len (listOfRoles )
191
- privilegesOrRoles = flattenList (listOfRoles , "'%s'" )
192
- } else {
193
- return diag .Errorf ("One of privileges or roles is required" )
194
- }
195
-
196
- user := d .Get ("user" ).(string )
197
- host := d .Get ("host" ).(string )
198
- role := d .Get ("role" ).(string )
199
- grantOption := d .Get ("grant" ).(bool )
200
-
201
- userOrRole , isRole , err := userOrRole (user , host , role , hasRoles )
287
+ // Determine whether the database has support for roles
288
+ hasRolesSupport , err := supportsRoles (ctx , meta )
202
289
if err != nil {
203
- return diag .Errorf ("failed getting whether it's user or a role: %v" , err )
290
+ return diag .Errorf ("failed getting role support: %v" , err )
291
+ }
292
+ if _ , ok := grant .(RoleGrant ); ok && ! hasRolesSupport {
293
+ return diag .Errorf ("role grants are not supported by this version of MySQL" )
204
294
}
205
- database := d .Get ("database" ).(string )
206
- table := d .Get ("table" ).(string )
207
295
296
+ // Check to see if there are existing roles that might be clobbered by this grant
208
297
grant , err := showGrant (ctx , db , userOrRole , database , table , grantOption )
209
298
if err != nil {
210
299
return diag .Errorf ("failed showing grants: %v" , err )
@@ -543,15 +632,15 @@ func showGrant(ctx context.Context, db *sql.DB, user, database, table string, gr
543
632
return grants , nil
544
633
}
545
634
546
- func showUserGrants (ctx context.Context , db * sql.DB , user string ) ([]* MySQLGrant , error ) {
547
- grants := []* MySQLGrant {}
635
+ func showUserGrants (ctx context.Context , db * sql.DB , user string ) ([]MySQLGrant , error ) {
636
+ grants := []MySQLGrant {}
548
637
549
638
sqlStatement := fmt .Sprintf ("SHOW GRANTS FOR %s" , user )
550
639
log .Printf ("[DEBUG] SQL: %s" , sqlStatement )
551
640
rows , err := db .QueryContext (ctx , sqlStatement )
552
641
553
642
if isNonExistingGrant (err ) {
554
- return []* MySQLGrant {}, nil
643
+ return []MySQLGrant {}, nil
555
644
}
556
645
557
646
if err != nil {
@@ -639,16 +728,6 @@ func normalizeUserHost(userHost string) string {
639
728
return withoutDblQuotes
640
729
}
641
730
642
- func normalizeDatabase (database string ) string {
643
- reProcedure := regexp .MustCompile ("(?i)^(function|procedure) `(.*)$" )
644
- if reProcedure .MatchString (database ) {
645
- // This is only a hack - user can specify function / procedure as database.
646
- database = reProcedure .ReplaceAllString (database , "$1 ${2}" )
647
- }
648
-
649
- return database
650
- }
651
-
652
731
func removeUselessPerms (grants []string ) []string {
653
732
ret := []string {}
654
733
for _ , grant := range grants {
0 commit comments