Skip to content

Commit 027f743

Browse files
DerekTBrownpetoju
authored andcommitted
Add first class support for privileges
1 parent b8e22b9 commit 027f743

File tree

1 file changed

+143
-64
lines changed

1 file changed

+143
-64
lines changed

mysql/resource_grant.go

Lines changed: 143 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,63 @@ import (
1616
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1717
)
1818

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 {
2044
Database string
2145
Table string
2246
Privileges []string
23-
Roles []string
2447
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
2568
}
2669

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
2976
}
3077

3178
func resourceGrant() *schema.Resource {
@@ -138,21 +185,6 @@ func formatTableName(table string) string {
138185
return fmt.Sprintf("`%s`", table)
139186
}
140187

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-
156188
func supportsRoles(ctx context.Context, meta interface{}) (bool, error) {
157189
currentVersion := getVersionFromMeta(ctx, meta)
158190

@@ -161,50 +193,107 @@ func supportsRoles(ctx context.Context, meta interface{}) (bool, error) {
161193
return hasRoles, nil
162194
}
163195

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+
164275
func CreateGrant(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
165276
db, err := getDatabaseFromMeta(ctx, meta)
166277
if err != nil {
167278
return diag.FromErr(err)
168279
}
169280

170-
hasRoles, err := supportsRoles(ctx, meta)
281+
// Parse the ResourceData
282+
grant, err := parseResource(d)
171283
if err != nil {
172-
return diag.Errorf("failed getting role support: %v", err)
284+
return err
173285
}
174286

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)
202289
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")
204294
}
205-
database := d.Get("database").(string)
206-
table := d.Get("table").(string)
207295

296+
// Check to see if there are existing roles that might be clobbered by this grant
208297
grant, err := showGrant(ctx, db, userOrRole, database, table, grantOption)
209298
if err != nil {
210299
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
543632
return grants, nil
544633
}
545634

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{}
548637

549638
sqlStatement := fmt.Sprintf("SHOW GRANTS FOR %s", user)
550639
log.Printf("[DEBUG] SQL: %s", sqlStatement)
551640
rows, err := db.QueryContext(ctx, sqlStatement)
552641

553642
if isNonExistingGrant(err) {
554-
return []*MySQLGrant{}, nil
643+
return []MySQLGrant{}, nil
555644
}
556645

557646
if err != nil {
@@ -639,16 +728,6 @@ func normalizeUserHost(userHost string) string {
639728
return withoutDblQuotes
640729
}
641730

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-
652731
func removeUselessPerms(grants []string) []string {
653732
ret := []string{}
654733
for _, grant := range grants {

0 commit comments

Comments
 (0)