From 79b4e9628d40ad6a2c3c10076d3495b3e7f3a1f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20J=C3=BAno=C5=A1?= Date: Wed, 24 Jan 2024 07:50:54 +0100 Subject: [PATCH] Revert "Fix GRANT OPTION" This reverts commit 1b5b633889842534d7503000c5b5d002750f598b. We need more time to rune this Revert "suggested changes" This reverts commit 309f64c552e1fa5418a627d280ce6545577027bb. Revert "fix mariadb" This reverts commit 2dc2802fd3bec067f864cb1ac7e1a5e946ddefe5. Revert "remove debug logging" This reverts commit 5146c945bfc95b66bb3a9239b515245696c04687. Revert "working" This reverts commit 0c66f79d537447dbdbb784f705f58ded50ef8d3e. Revert "add tests" This reverts commit 8c4cef24389981036ee05e57a0c3914c1c7f8fec. Revert "working" This reverts commit 398f87037e64fd0f9ac8238d81d6b1b5bb1416c8. Revert "working" This reverts commit f913cf863e6eb91f8ed1ee9d9afac8325a5216a9. Revert "Add first class support for privileges" This reverts commit 027f743327b6b534c9582c2f345a9899b57eabde. --- mysql/resource_grant.go | 995 ++++++++++++++--------------------- mysql/resource_grant_test.go | 248 ++------- 2 files changed, 420 insertions(+), 823 deletions(-) diff --git a/mysql/resource_grant.go b/mysql/resource_grant.go index f8a5ce25..f2e918dd 100644 --- a/mysql/resource_grant.go +++ b/mysql/resource_grant.go @@ -7,7 +7,6 @@ import ( "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "log" - "reflect" "regexp" "sort" "strings" @@ -17,232 +16,16 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -type ObjectT string - -var ( - kProcedure ObjectT = "PROCEDURE" - kFunction ObjectT = "FUNCTION" - kTable ObjectT = "TABLE" -) - -type MySQLGrant interface { - GetId() string - SQLGrantStatement() string - SQLRevokeStatement() string - GetUserOrRole() UserOrRole - GrantOption() bool -} - -type MySQLGrantWithDatabase interface { - MySQLGrant - GetDatabase() string -} - -type MySQLGrantWithTable interface { - MySQLGrantWithDatabase - GetTable() string -} - -type MySQLGrantWithPrivileges interface { - MySQLGrant - GetPrivileges() []string -} - -type PrivilegesPartiallyRevocable interface { - SQLPartialRevokePrivilegesStatement(privilegesToRevoke []string) string -} - -type UserOrRole struct { - Name string - Host string -} - -func (u UserOrRole) IDString() string { - if u.Host == "" { - return u.Name - } - return fmt.Sprintf("%s@%s", u.Name, u.Host) -} - -func (u UserOrRole) SQLString() string { - if u.Host == "" { - return fmt.Sprintf("'%s'", u.Name) - } - return fmt.Sprintf("'%s'@'%s'", u.Name, u.Host) -} - -func (u UserOrRole) Equals(other UserOrRole) bool { - if u.Name != other.Name { - return false - } - if (u.Host == "" || u.Host == "%") && (other.Host == "" || other.Host == "%") { - return true - } - return u.Host == other.Host -} - -type TablePrivilegeGrant struct { +type MySQLGrant struct { Database string Table string Privileges []string - Grant bool - UserOrRole UserOrRole - TLSOption string -} - -func (t *TablePrivilegeGrant) GetId() string { - return fmt.Sprintf("%s:%s", t.UserOrRole.IDString(), t.GetDatabase()) -} - -func (t *TablePrivilegeGrant) GetUserOrRole() UserOrRole { - return t.UserOrRole -} - -func (t *TablePrivilegeGrant) GrantOption() bool { - return t.Grant -} - -func (t *TablePrivilegeGrant) GetDatabase() string { - if t.Database == "*" { - return "*" - } else { - return fmt.Sprintf("`%s`", t.Database) - } -} - -func (t *TablePrivilegeGrant) GetTable() string { - if t.Table == "*" || t.Table == "" { - return "*" - } else { - return fmt.Sprintf("`%s`", t.Table) - } -} - -func (t *TablePrivilegeGrant) GetPrivileges() []string { - return t.Privileges -} - -func (t *TablePrivilegeGrant) SQLGrantStatement() string { - stmtSql := fmt.Sprintf("GRANT %s ON %s.%s TO %s", strings.Join(t.Privileges, ", "), t.GetDatabase(), t.GetTable(), t.UserOrRole.SQLString()) - if t.TLSOption != "" && strings.ToLower(t.TLSOption) != "none" { - stmtSql += fmt.Sprintf(" REQUIRE %s", t.TLSOption) - } - if t.Grant { - stmtSql += " WITH GRANT OPTION" - } - return stmtSql -} - -func (t *TablePrivilegeGrant) SQLRevokeStatement() string { - privs := t.Privileges - if t.Grant { - privs = append(privs, "GRANT OPTION") - } - return fmt.Sprintf("REVOKE %s ON %s.%s FROM %s", strings.Join(privs, ", "), t.GetDatabase(), t.GetTable(), t.UserOrRole.SQLString()) -} - -func (t *TablePrivilegeGrant) SQLPartialRevokePrivilegesStatement(privilegesToRevoke []string) string { - if t.Grant { - privilegesToRevoke = append(privilegesToRevoke, "GRANT OPTION") - } - stmt := fmt.Sprintf("REVOKE %s ON %s.%s FROM %s", strings.Join(privilegesToRevoke, ", "), t.GetDatabase(), t.GetTable(), t.UserOrRole.SQLString()) - return stmt -} - -type ProcedurePrivilegeGrant struct { - Database string - ObjectT ObjectT - CallableName string - Privileges []string - Grant bool - UserOrRole UserOrRole - TLSOption string -} - -func (t *ProcedurePrivilegeGrant) GetId() string { - return fmt.Sprintf("%s:%s", t.UserOrRole.IDString(), t.GetDatabase()) -} - -func (t *ProcedurePrivilegeGrant) GetUserOrRole() UserOrRole { - return t.UserOrRole -} - -func (t *ProcedurePrivilegeGrant) GrantOption() bool { - return t.Grant -} - -func (t *ProcedurePrivilegeGrant) GetDatabase() string { - if strings.Compare(t.Database, "*") != 0 && !strings.HasSuffix(t.Database, "`") { - return fmt.Sprintf("`%s`", t.Database) - } - return t.Database -} - -func (t *ProcedurePrivilegeGrant) GetPrivileges() []string { - return t.Privileges -} - -func (t *ProcedurePrivilegeGrant) SQLGrantStatement() string { - stmtSql := fmt.Sprintf("GRANT %s ON %s %s.%s TO %s", strings.Join(t.Privileges, ", "), t.ObjectT, t.GetDatabase(), t.CallableName, t.UserOrRole.SQLString()) - if t.TLSOption != "" && strings.ToLower(t.TLSOption) != "none" { - stmtSql += fmt.Sprintf(" REQUIRE %s", t.TLSOption) - } - if t.Grant { - stmtSql += " WITH GRANT OPTION" - } - return stmtSql -} - -func (t *ProcedurePrivilegeGrant) SQLRevokeStatement() string { - privs := t.Privileges - if t.Grant { - privs = append(privs, "GRANT OPTION") - } - stmt := fmt.Sprintf("REVOKE %s ON %s %s.%s FROM %s", strings.Join(privs, ", "), t.ObjectT, t.GetDatabase(), t.CallableName, t.UserOrRole.SQLString()) - return stmt -} - -func (t *ProcedurePrivilegeGrant) SQLPartialRevokePrivilegesStatement(privilegesToRevoke []string) string { - privs := privilegesToRevoke - if t.Grant { - privs = append(privs, "GRANT OPTION") - } - stmt := fmt.Sprintf("REVOKE %s ON %s %s.%s FROM %s", strings.Join(privs, ", "), t.ObjectT, t.GetDatabase(), t.CallableName, t.UserOrRole.SQLString()) - return stmt -} - -type RoleGrant struct { Roles []string Grant bool - UserOrRole UserOrRole - TLSOption string } -func (t *RoleGrant) GetId() string { - return fmt.Sprintf("%s", t.UserOrRole.IDString()) -} - -func (t *RoleGrant) GetUserOrRole() UserOrRole { - return t.UserOrRole -} - -func (t *RoleGrant) GrantOption() bool { - return t.Grant -} - -func (t *RoleGrant) SQLGrantStatement() string { - stmtSql := fmt.Sprintf("GRANT %s TO %s", strings.Join(t.Roles, ", "), t.UserOrRole.SQLString()) - if t.TLSOption != "" && strings.ToLower(t.TLSOption) != "none" { - stmtSql += fmt.Sprintf(" REQUIRE %s", t.TLSOption) - } - if t.Grant { - stmtSql += " WITH ADMIN OPTION" - } - return stmtSql -} - -func (t *RoleGrant) SQLRevokeStatement() string { - return fmt.Sprintf("REVOKE %s FROM %s", strings.Join(t.Roles, ", "), t.UserOrRole.SQLString()) +func (m MySQLGrant) String() string { + return fmt.Sprintf("{Database=%v,Table=%v,Privileges=%v,Roles=%v,Grant=%v}", m.Database, m.Table, m.Privileges, m.Roles, m.Grant) } func resourceGrant() *schema.Resource { @@ -325,95 +108,57 @@ func resourceGrant() *schema.Resource { } } -func supportsRoles(ctx context.Context, meta interface{}) (bool, error) { - currentVersion := getVersionFromMeta(ctx, meta) +func flattenList(list []interface{}, template string) string { + var result []string + for _, v := range list { + result = append(result, fmt.Sprintf(template, v.(string))) + } - requiredVersion, _ := version.NewVersion("8.0.0") - hasRoles := currentVersion.GreaterThan(requiredVersion) - return hasRoles, nil + return strings.Join(result, ", ") } -var kReProcedureWithoutDatabase = regexp.MustCompile(`(?i)^(function|procedure) ([^.]*)$`) -var kReProcedureWithDatabase = regexp.MustCompile(`(?i)^(function|procedure) ([^.]*)\.([^.]*)$`) +func formatDatabaseName(database string) string { + if strings.Compare(database, "*") != 0 && !strings.HasSuffix(database, "`") { + reProcedure := regexp.MustCompile(`(?i)^(function|procedure) (.*)$`) + if reProcedure.MatchString(database) { + // This is only a hack - user can specify function / procedure as database. + database = reProcedure.ReplaceAllString(database, "$1 `${2}`") + } else { + database = fmt.Sprintf("`%s`", database) + } + } -func parseResourceFromData(d *schema.ResourceData) (MySQLGrant, diag.Diagnostics) { + return database +} - // Step 1: Parse the user/role - var userOrRole UserOrRole - userAttr, userOk := d.GetOk("user") - hostAttr, hostOk := d.GetOk("host") - roleAttr, roleOk := d.GetOk("role") - if userOk && hostOk && userAttr.(string) != "" && hostAttr.(string) != "" { - userOrRole = UserOrRole{ - Name: userAttr.(string), - Host: hostAttr.(string), - } - } else if roleOk && roleAttr.(string) != "" { - userOrRole = UserOrRole{ - Name: roleAttr.(string), +func formatTableName(table string) string { + if table == "" || table == "*" { + return fmt.Sprintf("*") + } + return fmt.Sprintf("`%s`", table) +} + +// Formats user/host or role. Returns the formatted string and whether it is role. And an error in case it's not supported. +func userOrRole(user string, host string, role string, hasRoles bool) (string, bool, error) { + if len(user) > 0 && len(host) > 0 { + return fmt.Sprintf("'%s'@'%s'", user, host), false, nil + } else if len(role) > 0 { + if !hasRoles { + return "", false, fmt.Errorf("roles are only supported on MySQL 8 and above") } + + return fmt.Sprintf("'%s'", role), true, nil } else { - return nil, diag.Errorf("One of user/host or role is required") + return "", false, fmt.Errorf("user with host or a role is required") } +} - // Step 2: Get generic attributes - database := d.Get("database").(string) - tlsOption := d.Get("tls_option").(string) - grantOption := d.Get("grant").(bool) - - // Step 3a: If `roles` is specified, we have a role grant - if attr, ok := d.GetOk("roles"); ok { - roles := setToArray(attr) - return &RoleGrant{ - Roles: roles, - Grant: grantOption, - UserOrRole: userOrRole, - TLSOption: tlsOption, - }, nil - } - - // Step 3b. If the database is a procedure or function, we have a procedure grant - if kReProcedureWithDatabase.MatchString(database) || kReProcedureWithoutDatabase.MatchString(database) { - var callableType ObjectT - var callableName string - if kReProcedureWithDatabase.MatchString(database) { - matches := kReProcedureWithDatabase.FindStringSubmatch(database) - callableType = ObjectT(matches[1]) - database = matches[2] - callableName = matches[3] - } else { - matches := kReProcedureWithoutDatabase.FindStringSubmatch(database) - callableType = ObjectT(matches[1]) - database = matches[2] - callableName = d.Get("table").(string) - } +func supportsRoles(ctx context.Context, meta interface{}) (bool, error) { + currentVersion := getVersionFromMeta(ctx, meta) - privsList := setToArray(d.Get("privileges")) - privileges := normalizePerms(privsList) - - return &ProcedurePrivilegeGrant{ - Database: database, - ObjectT: callableType, - CallableName: callableName, - Privileges: privileges, - Grant: grantOption, - UserOrRole: userOrRole, - TLSOption: tlsOption, - }, nil - } - - // Step 3c. Otherwise, we have a table grant - privsList := setToArray(d.Get("privileges")) - privileges := normalizePerms(privsList) - - return &TablePrivilegeGrant{ - Database: database, - Table: d.Get("table").(string), - Privileges: privileges, - Grant: grantOption, - UserOrRole: userOrRole, - TLSOption: tlsOption, - }, nil + requiredVersion, _ := version.NewVersion("8.0.0") + hasRoles := currentVersion.GreaterThan(requiredVersion) + return hasRoles, nil } func CreateGrant(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { @@ -422,31 +167,88 @@ func CreateGrant(ctx context.Context, d *schema.ResourceData, meta interface{}) return diag.FromErr(err) } - // Parse the ResourceData - grant, diagErr := parseResourceFromData(d) + hasRoles, err := supportsRoles(ctx, meta) if err != nil { - return diagErr + return diag.Errorf("failed getting role support: %v", err) } - // Determine whether the database has support for roles - hasRolesSupport, err := supportsRoles(ctx, meta) - if err != nil { - return diag.Errorf("failed getting role support: %v", err) + var ( + privilegesOrRoles string + grantOn string + ) + + hasPrivs := false + rolesGranted := 0 + if attr, ok := d.GetOk("privileges"); ok { + privilegesOrRoles = flattenList(attr.(*schema.Set).List(), "%s") + hasPrivs = true + } else if attr, ok := d.GetOk("roles"); ok { + if !hasRoles { + return diag.Errorf("Roles are only supported on MySQL 8 and above") + } + listOfRoles := attr.(*schema.Set).List() + rolesGranted = len(listOfRoles) + privilegesOrRoles = flattenList(listOfRoles, "'%s'") + } else { + return diag.Errorf("One of privileges or roles is required") } - if _, ok := grant.(*RoleGrant); ok && !hasRolesSupport { - return diag.Errorf("role grants are not supported by this version of MySQL") + + user := d.Get("user").(string) + host := d.Get("host").(string) + role := d.Get("role").(string) + grantOption := d.Get("grant").(bool) + + userOrRole, isRole, err := userOrRole(user, host, role, hasRoles) + if err != nil { + return diag.Errorf("failed getting whether it's user or a role: %v", err) } + database := d.Get("database").(string) + table := d.Get("table").(string) - // Check to see if there are existing roles that might be clobbered by this grant - conflictingGrant, err := hasConflictingGrants(ctx, db, grant) + grant, err := showGrant(ctx, db, userOrRole, database, table, grantOption) if err != nil { return diag.Errorf("failed showing grants: %v", err) } - if conflictingGrant != nil { - return diag.Errorf("user/role %s already has unmanaged grant %v - import it first", grant.GetUserOrRole(), conflictingGrant) + + if hasPrivs { + if len(grant.Privileges) >= 1 { + if grant.Database == database && grant.Table == table && grant.Grant == grantOption { + return diag.Errorf("user/role %s already has unmanaged grant to %s.%s - import it first", userOrRole, grant.Database, grant.Table) + } + } + } else { + if len(grant.Roles) >= 1 { + // Granting role is just role without DB & table. + if grant.Database == "" && grant.Table == "" && grant.Grant == grantOption { + return diag.Errorf("user/role %s already has unmanaged grant for roles %v - import it first", userOrRole, grant.Roles) + } + } + } + + // DB and table have to be wrapped in backticks in some cases. + databaseWrapped := formatDatabaseName(database) + tableWrapped := formatTableName(table) + if (!isRole || hasPrivs) && rolesGranted == 0 { + grantOn = fmt.Sprintf(" ON %s.%s", databaseWrapped, tableWrapped) + } + + stmtSQL := fmt.Sprintf("GRANT %s%s TO %s", + privilegesOrRoles, + grantOn, + userOrRole) + + // MySQL 8+ doesn't allow REQUIRE on a GRANT statement. + if !hasRoles && d.Get("tls_option").(string) != "" && strings.ToLower(d.Get("tls_option").(string)) != "none" { + stmtSQL += fmt.Sprintf(" REQUIRE %s", d.Get("tls_option").(string)) } - stmtSQL := grant.SQLGrantStatement() + if d.Get("grant").(bool) { + if rolesGranted == 0 { + stmtSQL += " WITH GRANT OPTION" + } else { + stmtSQL += " WITH ADMIN OPTION" + } + } log.Println("Executing statement:", stmtSQL) _, err = db.ExecContext(ctx, stmtSQL) @@ -454,7 +256,13 @@ func CreateGrant(ctx context.Context, d *schema.ResourceData, meta interface{}) return diag.Errorf("Error running SQL (%s): %s", stmtSQL, err) } - d.SetId(grant.GetId()) + id := fmt.Sprintf("%s@%s:%s", user, host, databaseWrapped) + if isRole { + id = fmt.Sprintf("%s:%s", role, databaseWrapped) + } + + d.SetId(id) + return ReadGrant(ctx, d, meta) } @@ -464,23 +272,60 @@ func ReadGrant(ctx context.Context, d *schema.ResourceData, meta interface{}) di return diag.Errorf("failed getting database from Meta: %v", err) } - grant, diagErr := parseResourceFromData(d) - if diagErr != nil { - return diagErr + hasRoles, err := supportsRoles(ctx, meta) + if err != nil { + return diag.Errorf("failed getting role support: %v", err) } + userOrRole, _, err := userOrRole( + d.Get("user").(string), + d.Get("host").(string), + d.Get("role").(string), + hasRoles) + if err != nil { + return diag.Errorf("failed getting user or role: %v", err) + } + database := d.Get("database").(string) + table := d.Get("table").(string) + grantOption := d.Get("grant").(bool) + rolesSet := d.Get("roles").(*schema.Set) + rolesCount := len(rolesSet.List()) + + if rolesCount != 0 { + // For some reason, role can have still database / table, that + // makes no sense. Remove them when reading. + database = "" + table = "" + } + grant, err := showGrant(ctx, db, userOrRole, database, table, grantOption) - allGrants, err := showUserGrants(ctx, db, grant.GetUserOrRole()) if err != nil { - return diag.Errorf("ReadGrant - getting all grants failed: %v", err) + return diag.Errorf("error reading grant for %s: %v", userOrRole, err) } - if len(allGrants) == 0 { - log.Printf("[WARN] GRANT not found for %s - removing from state", grant.GetUserOrRole()) + if len(grant.Privileges) == 0 && len(grant.Roles) == 0 { + log.Printf("[WARN] GRANT not found for %s (%s) - removing from state", userOrRole, err) d.SetId("") return nil } - setDataFromGrant(grant, d) + var privileges []string + var roles []string + + if grant.Database == database && grant.Table == table { + privileges = makePrivs(setToArray(d.Get("privileges")), grant.Privileges) + } + // Granting role is just role without DB & table. + if grant.Database == "" && grant.Table == "" { + roles = grant.Roles + } + + if grant.Grant { + grantOption = true + } + + d.Set("privileges", privileges) + d.Set("roles", roles) + d.Set("grant", grantOption) return nil } @@ -491,17 +336,27 @@ func UpdateGrant(ctx context.Context, d *schema.ResourceData, meta interface{}) return diag.FromErr(err) } + hasRoles, err := supportsRoles(ctx, meta) + + if err != nil { + return diag.Errorf("failed getting role support: %v", err) + } + + userOrRole, _, err := userOrRole( + d.Get("user").(string), + d.Get("host").(string), + d.Get("role").(string), + hasRoles) + if err != nil { return diag.Errorf("failed getting user or role: %v", err) } - if d.HasChange("privileges") { - grant, diagErr := parseResourceFromData(d) - if diagErr != nil { - return diagErr - } + database := d.Get("database").(string) + table := d.Get("table").(string) - err = updatePrivileges(ctx, db, d, grant) + if d.HasChange("privileges") { + err = updatePrivileges(ctx, d, db, userOrRole, database, table) if err != nil { return diag.Errorf("failed updating privileges: %v", err) } @@ -510,27 +365,22 @@ func UpdateGrant(ctx context.Context, d *schema.ResourceData, meta interface{}) return nil } -func updatePrivileges(ctx context.Context, db *sql.DB, d *schema.ResourceData, grant MySQLGrant) error { +func updatePrivileges(ctx context.Context, d *schema.ResourceData, db *sql.DB, user string, database string, table string) error { oldPrivsIf, newPrivsIf := d.GetChange("privileges") oldPrivs := oldPrivsIf.(*schema.Set) newPrivs := newPrivsIf.(*schema.Set) grantIfs := newPrivs.Difference(oldPrivs).List() revokeIfs := oldPrivs.Difference(newPrivs).List() - // Normalize the privileges to revoke - privsToRevoke := []string{} - for _, revokeIf := range revokeIfs { - privsToRevoke = append(privsToRevoke, revokeIf.(string)) - } - privsToRevoke = normalizePerms(privsToRevoke) + if len(revokeIfs) > 0 { + revokes := make([]string, len(revokeIfs)) - // Do a partial revoke of anything that has been removed - if len(privsToRevoke) > 0 { - partialRevoker, ok := grant.(PrivilegesPartiallyRevocable) - if !ok { - return fmt.Errorf("grant does not support partial privilege revokes") + for i, v := range revokeIfs { + revokes[i] = v.(string) } - sqlCommand := partialRevoker.SQLPartialRevokePrivilegesStatement(privsToRevoke) + + sqlCommand := fmt.Sprintf("REVOKE %s ON %s.%s FROM %s", strings.Join(revokes, ","), formatDatabaseName(database), formatTableName(table), user) + log.Printf("[DEBUG] SQL: %s", sqlCommand) if _, err := db.ExecContext(ctx, sqlCommand); err != nil { @@ -538,9 +388,15 @@ func updatePrivileges(ctx context.Context, db *sql.DB, d *schema.ResourceData, g } } - // Do a full grant if anything has been added if len(grantIfs) > 0 { - sqlCommand := grant.SQLGrantStatement() + grants := make([]string, len(grantIfs)) + + for i, v := range grantIfs { + grants[i] = v.(string) + } + + sqlCommand := fmt.Sprintf("GRANT %s ON %s.%s TO %s", strings.Join(grants, ","), formatDatabaseName(database), formatTableName(table), user) + log.Printf("[DEBUG] SQL: %s", sqlCommand) if _, err := db.ExecContext(ctx, sqlCommand); err != nil { @@ -557,17 +413,46 @@ func DeleteGrant(ctx context.Context, d *schema.ResourceData, meta interface{}) return diag.FromErr(err) } - grant, diagErr := parseResourceFromData(d) + database := formatDatabaseName(d.Get("database").(string)) + table := formatTableName(d.Get("table").(string)) + + hasRoles, err := supportsRoles(ctx, meta) if err != nil { - return diagErr + return diag.Errorf("failed getting role support: %v", err) + } + + userOrRole, _, err := userOrRole( + d.Get("user").(string), + d.Get("host").(string), + d.Get("role").(string), + hasRoles) + if err != nil { + return diag.Errorf("failed getting user or role: %v", err) + } + + roles := d.Get("roles").(*schema.Set) + privileges := d.Get("privileges").(*schema.Set) + grantOption := d.Get("grant").(bool) + + whatToRevoke := fmt.Sprintf("ALL ON %s.%s", database, table) + if len(roles.List()) > 0 { + whatToRevoke = flattenList(roles.List(), "'%s'") + } else if len(privileges.List()) > 0 { + privilegeList := flattenList(privileges.List(), "%s") + if grantOption { + // For privilege grant (SELECT or so), we have to revoke GRANT OPTION + // for role grant, ADMIN OPTION is revoked when role is revoked. + privilegeList = fmt.Sprintf("%v, GRANT OPTION", privilegeList) + } + whatToRevoke = fmt.Sprintf("%s ON %s.%s", privilegeList, database, table) } - sqlStatement := grant.SQLRevokeStatement() + sqlStatement := fmt.Sprintf("REVOKE %s FROM %s", whatToRevoke, userOrRole) log.Printf("[DEBUG] SQL: %s", sqlStatement) _, err = db.ExecContext(ctx, sqlStatement) if err != nil { if !isNonExistingGrant(err) { - return diag.Errorf("error revoking %s: %s", sqlStatement, err) + return diag.Errorf("error revoking ALL (%s): %s", sqlStatement, err) } } @@ -600,311 +485,168 @@ func ImportGrant(ctx context.Context, d *schema.ResourceData, meta interface{}) table := userHostDatabaseTable[3] grantOption := len(userHostDatabaseTable) == 5 - userOrRole := UserOrRole{ - Name: user, - Host: host, - } - db, err := getDatabaseFromMeta(ctx, meta) if err != nil { return nil, err } - grants, err := showUserGrants(ctx, db, userOrRole) + grant, err := showGrant(ctx, db, fmt.Sprintf("'%s'@'%s'", user, host), database, table, grantOption) + if err != nil { return nil, err } - for _, grant := range grants { - // Grant options must match - if grant.GrantOption() != grantOption { - continue - } - // If we have a database grant, we need to match the database name - if dbGrant, ok := grant.(MySQLGrantWithDatabase); (!ok && database != "") || (ok && dbGrant.GetDatabase() != database) { - continue - } + results := []*schema.ResourceData{restoreGrant(user, host, &grant)} - // If we have a table grant, we need to match the table name - if tableGrant, ok := grant.(MySQLGrantWithTable); (!ok && table != "") || (ok && tableGrant.GetTable() != table) { - continue - } - - // We have a match! - res := resourceGrant().Data(nil) - setDataFromGrant(grant, res) - return []*schema.ResourceData{res}, nil - } - - // No match found - results := []*schema.ResourceData{} return results, nil } -// setDataFromGrant copies the values from MySQLGrant to the schema.ResourceData -func setDataFromGrant(grant MySQLGrant, d *schema.ResourceData) *schema.ResourceData { - - if tableGrant, ok := grant.(*TablePrivilegeGrant); ok { - d.Set("database", tableGrant.Database) - d.Set("grant", grant.GrantOption()) - d.Set("tls_option", tableGrant.TLSOption) +func restoreGrant(user string, host string, grant *MySQLGrant) *schema.ResourceData { + d := resourceGrant().Data(nil) - } else if procedureGrant, ok := grant.(*ProcedurePrivilegeGrant); ok { - d.Set("database", fmt.Sprintf("%s %s.%s", procedureGrant.ObjectT, procedureGrant.Database, procedureGrant.CallableName)) - d.Set("table", "*") - d.Set("grant", grant.GrantOption()) - d.Set("tls_option", procedureGrant.TLSOption) + database := grant.Database + id := fmt.Sprintf("%s@%s:%s", user, host, formatDatabaseName(database)) + d.SetId(id) - } else if roleGrant, ok := grant.(*RoleGrant); ok { - d.Set("grant", grant.GrantOption()) - d.Set("roles", roleGrant.Roles) - d.Set("tls_option", roleGrant.TLSOption) - } else { - panic("Unknown grant type") - } - - // Only set privileges if there is a delta in the normalized privileges - if grantWithPriv, hasPriv := grant.(MySQLGrantWithPrivileges); hasPriv { - currentPriv, ok := d.GetOk("privileges") - if !ok { - d.Set("privileges", grantWithPriv.GetPrivileges()) - } else { - currentPrivs := setToArray(currentPriv.(*schema.Set)) - currentPrivs = normalizePerms(currentPrivs) - if !reflect.DeepEqual(currentPrivs, grantWithPriv.GetPrivileges()) { - d.Set("privileges", grantWithPriv.GetPrivileges()) - } - } - } - - // This is a bit of a hack, since we don't have a way to distingush between users and roles - // from the grant itself. We can only infer it from the schema. - userOrRole := grant.GetUserOrRole() - if d.Get("role") != "" { - d.Set("role", userOrRole.Name) - } else { - d.Set("user", userOrRole.Name) - d.Set("host", userOrRole.Host) - } + d.Set("user", user) + d.Set("host", host) + d.Set("database", database) + d.Set("table", grant.Table) + d.Set("grant", grant.Grant) + d.Set("tls_option", "NONE") + d.Set("privileges", grant.Privileges) return d } -func hasConflictingGrants(ctx context.Context, db *sql.DB, desiredGrant MySQLGrant) (MySQLGrant, error) { - allGrants, err := showUserGrants(ctx, db, desiredGrant.GetUserOrRole()) +func showGrant(ctx context.Context, db *sql.DB, user, database, table string, grantOption bool) (MySQLGrant, error) { + allGrants, err := showUserGrants(ctx, db, user) if err != nil { - return nil, fmt.Errorf("showGrant - getting all grants failed: %w", err) - } - for _, dbGrant := range allGrants { - if desiredGrant.GrantOption() != dbGrant.GrantOption() { - continue - } - if reflect.TypeOf(desiredGrant) != reflect.TypeOf(dbGrant) { - continue - } - if grantWithDatabase, ok := desiredGrant.(MySQLGrantWithDatabase); ok { - if grantWithDatabase.GetDatabase() != dbGrant.(MySQLGrantWithDatabase).GetDatabase() { - continue - } - } - if grantWithTable, ok := desiredGrant.(MySQLGrantWithTable); ok { - if grantWithTable.GetTable() != dbGrant.(MySQLGrantWithTable).GetTable() { - continue + return MySQLGrant{}, fmt.Errorf("showGrant - getting all grants failed: %w", err) + } + grants := MySQLGrant{ + Database: database, + Table: table, + Grant: grantOption, + } + for _, grant := range allGrants { + // We must normalize database as it may contain something like PROCEDURE `asd` or the same without backticks. + // TODO: write tests or consider some other way to handle permissions to PROCEDURE/FUNCTION + if grant.Grant == grantOption { + if normalizeDatabase(grant.Database) == normalizeDatabase(database) && grant.Table == table { + grants.Privileges = append(grants.Privileges, grant.Privileges...) } + // Roles don't depend on database / table settings. + grants.Roles = append(grants.Roles, grant.Roles...) } - return dbGrant, nil } - return nil, nil + return grants, nil } -var ( - kUserOrRoleRegex = regexp.MustCompile("['`]?([^'`]+)['`]?(?:@['`]?([^'`]+)['`]?)?") -) +func showUserGrants(ctx context.Context, db *sql.DB, user string) ([]*MySQLGrant, error) { + grants := []*MySQLGrant{} -func parseUserOrRoleFromRow(userOrRoleStr string) (*UserOrRole, error) { - userHostMatches := kUserOrRoleRegex.FindStringSubmatch(userOrRoleStr) - if len(userHostMatches) == 3 { - return &UserOrRole{ - Name: userHostMatches[1], - Host: userHostMatches[2], - }, nil - } else if len(userHostMatches) == 2 { - return &UserOrRole{ - Name: userHostMatches[1], - Host: "%", - }, nil - } else { - return nil, fmt.Errorf("failed to parse user or role portion of grant statement: %s", userOrRoleStr) - } -} - -var ( - kDatabaseAndObjectRegex = regexp.MustCompile("['`]?([^'`]+)['`]?\\.['`]?([^'`]+)['`]?") -) + sqlStatement := fmt.Sprintf("SHOW GRANTS FOR %s", user) + log.Printf("[DEBUG] SQL: %s", sqlStatement) + rows, err := db.QueryContext(ctx, sqlStatement) -func parseDatabaseQualifiedObject(objectRef string) (string, string, error) { - if matches := kDatabaseAndObjectRegex.FindStringSubmatch(objectRef); len(matches) == 3 { - return matches[1], matches[2], nil + if isNonExistingGrant(err) { + return []*MySQLGrant{}, nil } - return "", "", fmt.Errorf("failed to parse database and table portion of grant statement: %s", objectRef) -} - -var ( - kRequireRegex = regexp.MustCompile(`.*REQUIRE\s+(.*)`) - - kGrantRegex = regexp.MustCompile(`\bGRANT OPTION\b|\bADMIN OPTION\b`) - - procedureGrantRegex = regexp.MustCompile(`GRANT\s+(.+)\s+ON\s+(FUNCTION|PROCEDURE)\s+(.+)\s+TO\s+(.+)`) - tableGrantRegex = regexp.MustCompile(`GRANT\s+(.+)\s+ON\s+(.+)\s+TO\s+(.+)`) - roleGrantRegex = regexp.MustCompile(`GRANT\s+(.+)\s+TO\s+(.+)`) -) - -func parseGrantFromRow(grantStr string) (MySQLGrant, error) { - // Ignore REVOKE.* - if strings.HasPrefix(grantStr, "REVOKE") { - log.Printf("[WARN] Partial revokes are not fully supported and lead to unexpected behavior. Consult documentation https://dev.mysql.com/doc/refman/8.0/en/partial-revokes.html on how to disable them for safe and reliable terraform. Relevant partial revoke: %s\n", grantStr) - return nil, nil + if err != nil { + return nil, fmt.Errorf("showUserGrants - getting grants failed: %w", err) } - // Parse Require Statement - tlsOption := "" - if requireMatches := kRequireRegex.FindStringSubmatch(grantStr); len(requireMatches) == 2 { - tlsOption = requireMatches[1] - } + defer rows.Close() + re := regexp.MustCompile(`^GRANT (.+) ON (.+?)\.(.+?) TO ([^ ]+)`) - if procedureMatches := procedureGrantRegex.FindStringSubmatch(grantStr); len(procedureMatches) == 5 { - privsStr := procedureMatches[1] - privileges := extractPermTypes(privsStr) - privileges = normalizePerms(privileges) + // Ex: GRANT `app_read`@`%`,`app_write`@`%` TO `rw_user1`@`localhost + reRole := regexp.MustCompile(`^GRANT (.+) TO`) + reGrant := regexp.MustCompile(`\bGRANT OPTION\b|\bADMIN OPTION\b`) - // After normalizePerms, we may have empty privileges. If so, skip this grant. - if len(privileges) == 0 { - return nil, nil - } + for rows.Next() { + var rawGrant string - userOrRole, err := parseUserOrRoleFromRow(procedureMatches[4]) + err := rows.Scan(&rawGrant) if err != nil { - return nil, err + return nil, fmt.Errorf("showUserGrants - reading row failed: %w", err) } - database, callable, err := parseDatabaseQualifiedObject(procedureMatches[3]) - if err != nil { - return nil, err + if strings.HasPrefix(rawGrant, "REVOKE") { + log.Printf("[WARN] Partial revokes are not fully supported and lead to unexpected behavior. Consult documentation https://dev.mysql.com/doc/refman/8.0/en/partial-revokes.html on how to disable them for safe and reliable terraform. Relevant partial revoke: %s\n", rawGrant) + continue } - grant := &ProcedurePrivilegeGrant{ - Database: database, - ObjectT: ObjectT(procedureMatches[2]), - CallableName: callable, - Privileges: privileges, - Grant: kGrantRegex.MatchString(grantStr), - UserOrRole: *userOrRole, - TLSOption: tlsOption, - } - log.Printf("[DEBUG] Got: %s, parsed grant is %s: %v", grantStr, reflect.TypeOf(grant), grant) - return grant, nil - } else if tableMatches := tableGrantRegex.FindStringSubmatch(grantStr); len(tableMatches) == 4 { - privsStr := tableMatches[1] - privileges := extractPermTypes(privsStr) - privileges = normalizePerms(privileges) - - // After normalizePerms, we may have empty privileges. If so, skip this grant. - if len(privileges) == 0 { - return nil, nil - } + if m := re.FindStringSubmatch(rawGrant); len(m) == 5 { + privsStr := m[1] + privList := extractPermTypes(privsStr) + privileges := make([]string, len(privList)) - userOrRole, err := parseUserOrRoleFromRow(tableMatches[3]) - if err != nil { - return nil, err - } + for i, priv := range privList { + privileges[i] = strings.TrimSpace(priv) + } + grantUserHost := m[4] + if normalizeUserHost(grantUserHost) != normalizeUserHost(user) { + // Percona returns also grants for % if we requested IP. + // Skip them as we don't want terraform to consider it. + log.Printf("[DEBUG] Skipping grant with host %v while we want %v", grantUserHost, user) + continue + } - database, table, err := parseDatabaseQualifiedObject(tableMatches[2]) - if err != nil { - return nil, err - } + grant := &MySQLGrant{ + Database: strings.Trim(m[2], "`\""), + Table: strings.Trim(m[3], "`\""), + Privileges: privileges, + Grant: reGrant.MatchString(rawGrant), + } - grant := &TablePrivilegeGrant{ - Database: database, - Table: table, - Privileges: privileges, - Grant: kGrantRegex.MatchString(grantStr), - UserOrRole: *userOrRole, - TLSOption: tlsOption, - } - log.Printf("[DEBUG] Got: %s, parsed grant is %s: %v", grantStr, reflect.TypeOf(grant), grant) - return grant, nil - } else if roleMatches := roleGrantRegex.FindStringSubmatch(grantStr); len(roleMatches) == 3 { - rolesStart := strings.Split(roleMatches[1], ",") - roles := make([]string, len(rolesStart)) - - for i, role := range rolesStart { - roles[i] = strings.Trim(role, "`@%\" ") - } + if len(privileges) > 0 { + grants = append(grants, grant) + } - userOrRole, err := parseUserOrRoleFromRow(roleMatches[2]) - if err != nil { - return nil, err - } + } else if m := reRole.FindStringSubmatch(rawGrant); len(m) == 2 { + roleStr := m[1] + rolesStart := strings.Split(roleStr, ",") + roles := make([]string, len(rolesStart)) - grant := &RoleGrant{ - Roles: roles, - Grant: kGrantRegex.MatchString(grantStr), - UserOrRole: *userOrRole, - TLSOption: tlsOption, - } - log.Printf("[DEBUG] Got: %s, parsed grant is %s: %v", grantStr, reflect.TypeOf(grant), grant) - return grant, nil + for i, role := range rolesStart { + roles[i] = strings.Trim(role, "`@%\" ") + } - } else { - return nil, fmt.Errorf("failed to parse object portion of grant statement: %s", grantStr) - } -} + grant := &MySQLGrant{ + Roles: roles, + Grant: reGrant.MatchString(rawGrant), + } -func showUserGrants(ctx context.Context, db *sql.DB, userOrRole UserOrRole) ([]MySQLGrant, error) { - grants := []MySQLGrant{} + grants = append(grants, grant) + } else { + return nil, fmt.Errorf("failed to parse grant statement: %s", rawGrant) + } + } - sqlStatement := fmt.Sprintf("SHOW GRANTS FOR %s", userOrRole.SQLString()) - log.Printf("[DEBUG] SQL: %s", sqlStatement) - rows, err := db.QueryContext(ctx, sqlStatement) + log.Printf("[DEBUG] Parsed grants are: %v", grants) + return grants, nil +} - if isNonExistingGrant(err) { - return []MySQLGrant{}, nil +func normalizeUserHost(userHost string) string { + if !strings.Contains(userHost, "@") { + userHost = fmt.Sprint(userHost, "@%") } + withoutQuotes := strings.ReplaceAll(userHost, "'", "") + withoutBackticks := strings.ReplaceAll(withoutQuotes, "`", "") + withoutDblQuotes := strings.ReplaceAll(withoutBackticks, "\"", "") + return withoutDblQuotes +} - if err != nil { - return nil, fmt.Errorf("showUserGrants - getting grants failed: %w", err) +func normalizeDatabase(database string) string { + reProcedure := regexp.MustCompile("(?i)^(function|procedure) `(.*)$") + if reProcedure.MatchString(database) { + // This is only a hack - user can specify function / procedure as database. + database = reProcedure.ReplaceAllString(database, "$1 ${2}") } - defer rows.Close() - for rows.Next() { - var rawGrant string - - err := rows.Scan(&rawGrant) - if err != nil { - return nil, fmt.Errorf("showUserGrants - reading row failed: %w", err) - } - - parsedGrant, err := parseGrantFromRow(rawGrant) - if err != nil { - return nil, err - } - if parsedGrant == nil { - continue - } - - // Filter out any grants that don't match the provided user - // Percona returns also grants for % if we requested IP. - // Skip them as we don't want terraform to consider it. - if !parsedGrant.GetUserOrRole().Equals(userOrRole) { - log.Printf("[DEBUG] Skipping grant for %s as it doesn't match %s", parsedGrant.GetUserOrRole().SQLString(), userOrRole.SQLString()) - continue - } - grants = append(grants, parsedGrant) - - } - log.Printf("[DEBUG] Parsed grants are: %s", grants) - return grants, nil + return database } func removeUselessPerms(grants []string) []string { @@ -948,15 +690,14 @@ func extractPermTypes(g string) []string { } } grants = append(grants, string(currentWord)) - return grants + return removeUselessPerms(grants) } func normalizeColumnOrder(perm string) string { - re := regexp.MustCompile(`^([^(]*)\((.*)\)$`) + re := regexp.MustCompile("^([^(]*)\\((.*)\\)$") // We may get inputs like // SELECT(b,a,c) -> SELECT(a,b,c) // DELETE -> DELETE - // SELECT (a,b,c) -> SELECT(a,b,c) // if it's without parentheses, return it right away. // Else split what is inside, sort it, concat together and return the result. m := re.FindStringSubmatch(perm) @@ -969,17 +710,16 @@ func normalizeColumnOrder(perm string) string { parts[i] = strings.Trim(parts[i], "` ") } sort.Strings(parts) - precursor := strings.Trim(m[1], " ") partsTogether := strings.Join(parts, ", ") - return fmt.Sprintf("%s(%s)", precursor, partsTogether) + return fmt.Sprintf("%s(%s)", m[1], partsTogether) } func normalizePerms(perms []string) []string { + // Spaces and backticks are optional, let's ignore them. + re := regexp.MustCompile("[ `]") ret := []string{} for _, perm := range perms { - // Remove leading and trailing backticks and spaces - permNorm := strings.Trim(perm, "` ") - + permNorm := re.ReplaceAllString(perm, "") permUcase := strings.ToUpper(permNorm) if permUcase == "ALL" || permUcase == "ALLPRIVILEGES" { permUcase = "ALL PRIVILEGES" @@ -988,11 +728,44 @@ func normalizePerms(perms []string) []string { ret = append(ret, permSortedColumns) } + return ret +} - // Remove useless perms - ret = removeUselessPerms(ret) +func makePrivs(have, want []string) []string { + // This is tricky to prevent diffs that cannot be suppressed easily. + // Example: + // Have select(`c1`, `c2`), insert (c3,c2) + // Want select(c2,c1), insert(c3,c2) + // So we want to normalize both and then go from "want" to "have" to + // We'll have map want->wantnorm = havenorm->have - return ret + // Also, we need to return all mapped values of "want". + + // After normalize, same indices have the same values, prepare maps. + haveNorm := normalizePerms(have) + haveNormToHave := map[string]string{} + for i := range haveNorm { + haveNormToHave[haveNorm[i]] = have[i] + } + + wantNorm := normalizePerms(want) + wantNormToWant := map[string]string{} + for i := range wantNorm { + wantNormToWant[want[i]] = wantNorm[i] + } + + retSet := []string{} + for _, w := range want { + suspect := haveNormToHave[wantNormToWant[w]] + if suspect == "" { + // Nothing found in what we have. + retSet = append(retSet, w) + } else { + retSet = append(retSet, suspect) + } + } + + return retSet } func setToArray(s interface{}) []string { diff --git a/mysql/resource_grant_test.go b/mysql/resource_grant_test.go index 56aecb22..83c3d642 100644 --- a/mysql/resource_grant_test.go +++ b/mysql/resource_grant_test.go @@ -23,7 +23,7 @@ func TestAccGrant(t *testing.T) { { Config: testAccGrantConfigBasic(dbName), Check: resource.ComposeTestCheckFunc( - testAccPrivilege("mysql_grant.test", "SELECT", true, false), + testAccPrivilege("mysql_grant.test", "SELECT", true), resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), @@ -33,7 +33,7 @@ func TestAccGrant(t *testing.T) { { Config: testAccGrantConfigBasic(dbName), Check: resource.ComposeTestCheckFunc( - testAccPrivilege("mysql_grant.test", "SELECT", true, false), + testAccPrivilege("mysql_grant.test", "SELECT", true), resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), @@ -53,22 +53,19 @@ func TestAccGrantWithGrantOption(t *testing.T) { { Config: testAccGrantConfigBasic(dbName), Check: resource.ComposeTestCheckFunc( - testAccPrivilege("mysql_grant.test", "SELECT", true, false), - resource.TestCheckResourceAttr("mysql_grant.test", "grant", "false"), + testAccPrivilege("mysql_grant.test", "SELECT", true), ), }, { Config: testAccGrantConfigBasicWithGrant(dbName), Check: resource.ComposeTestCheckFunc( - testAccPrivilege("mysql_grant.test", "SELECT", true, true), - resource.TestCheckResourceAttr("mysql_grant.test", "grant", "true"), + testAccPrivilege("mysql_grant.test", "SELECT", true), ), }, { Config: testAccGrantConfigBasic(dbName), Check: resource.ComposeTestCheckFunc( - testAccPrivilege("mysql_grant.test", "SELECT", true, false), - resource.TestCheckResourceAttr("mysql_grant.test", "grant", "false"), + testAccPrivilege("mysql_grant.test", "SELECT", true), ), }, }, @@ -85,7 +82,7 @@ func TestAccBroken(t *testing.T) { { Config: testAccGrantConfigBasic(dbName), Check: resource.ComposeTestCheckFunc( - testAccPrivilege("mysql_grant.test", "SELECT", true, false), + testAccPrivilege("mysql_grant.test", "SELECT", true), resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), @@ -96,7 +93,7 @@ func TestAccBroken(t *testing.T) { Config: testAccGrantConfigBroken(dbName), ExpectError: regexp.MustCompile("already has"), Check: resource.ComposeTestCheckFunc( - testAccPrivilege("mysql_grant.test", "SELECT", true, false), + testAccPrivilege("mysql_grant.test", "SELECT", true), resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), @@ -120,7 +117,7 @@ func TestAccDifferentHosts(t *testing.T) { { Config: testAccGrantConfigExtraHost(dbName, false), Check: resource.ComposeTestCheckFunc( - testAccPrivilege("mysql_grant.test_all", "SELECT", true, false), + testAccPrivilege("mysql_grant.test_all", "SELECT", true), resource.TestCheckResourceAttr("mysql_grant.test_all", "user", fmt.Sprintf("jdoe-%s", dbName)), resource.TestCheckResourceAttr("mysql_grant.test_all", "host", "%"), resource.TestCheckResourceAttr("mysql_grant.test_all", "table", "*"), @@ -129,7 +126,7 @@ func TestAccDifferentHosts(t *testing.T) { { Config: testAccGrantConfigExtraHost(dbName, true), Check: resource.ComposeTestCheckFunc( - testAccPrivilege("mysql_grant.test", "SELECT", true, false), + testAccPrivilege("mysql_grant.test", "SELECT", true), resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), resource.TestCheckResourceAttr("mysql_grant.test", "host", "10.1.2.3"), resource.TestCheckResourceAttr("mysql_grant.test", "table", "*"), @@ -159,7 +156,7 @@ func TestAccGrantComplex(t *testing.T) { { Config: testAccGrantConfigWithPrivs(dbName, `"SELECT (c1, c2)"`), Check: resource.ComposeTestCheckFunc( - testAccPrivilege("mysql_grant.test", "SELECT (c1,c2)", true, false), + testAccPrivilege("mysql_grant.test", "SELECT (c1,c2)", true), resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), @@ -169,10 +166,10 @@ func TestAccGrantComplex(t *testing.T) { { Config: testAccGrantConfigWithPrivs(dbName, `"DROP", "SELECT (c1)", "INSERT(c3, c4)", "REFERENCES(c5)"`), Check: resource.ComposeTestCheckFunc( - testAccPrivilege("mysql_grant.test", "INSERT (c3,c4)", true, false), - testAccPrivilege("mysql_grant.test", "SELECT (c1)", true, false), - testAccPrivilege("mysql_grant.test", "SELECT (c1,c2)", false, false), - testAccPrivilege("mysql_grant.test", "REFERENCES (c5)", true, false), + testAccPrivilege("mysql_grant.test", "INSERT (c3,c4)", true), + testAccPrivilege("mysql_grant.test", "SELECT (c1)", true), + testAccPrivilege("mysql_grant.test", "SELECT (c1,c2)", false), + testAccPrivilege("mysql_grant.test", "REFERENCES (c5)", true), resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), @@ -182,7 +179,7 @@ func TestAccGrantComplex(t *testing.T) { { Config: testAccGrantConfigWithPrivs(dbName, `"DROP", "SELECT (c1)", "INSERT(c4, c3, c2)"`), Check: resource.ComposeTestCheckFunc( - testAccPrivilege("mysql_grant.test", "REFERENCES (c5)", false, false), + testAccPrivilege("mysql_grant.test", "REFERENCES (c5)", false), resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), @@ -192,7 +189,7 @@ func TestAccGrantComplex(t *testing.T) { { Config: testAccGrantConfigWithPrivs(dbName, `"ALL PRIVILEGES"`), Check: resource.ComposeTestCheckFunc( - testAccPrivilege("mysql_grant.test", "ALL", true, false), + testAccPrivilege("mysql_grant.test", "ALL", true), resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), @@ -202,7 +199,7 @@ func TestAccGrantComplex(t *testing.T) { { Config: testAccGrantConfigWithPrivs(dbName, `"ALL"`), Check: resource.ComposeTestCheckFunc( - testAccPrivilege("mysql_grant.test", "ALL", true, false), + testAccPrivilege("mysql_grant.test", "ALL", true), resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), @@ -212,11 +209,11 @@ func TestAccGrantComplex(t *testing.T) { { Config: testAccGrantConfigWithPrivs(dbName, `"DROP", "SELECT (c1, c2)", "INSERT(c5)", "REFERENCES(c1)"`), Check: resource.ComposeTestCheckFunc( - testAccPrivilege("mysql_grant.test", "ALL", false, false), - testAccPrivilege("mysql_grant.test", "DROP", true, false), - testAccPrivilege("mysql_grant.test", "SELECT(c1,c2)", true, false), - testAccPrivilege("mysql_grant.test", "INSERT(c5)", true, false), - testAccPrivilege("mysql_grant.test", "REFERENCES(c1)", true, false), + testAccPrivilege("mysql_grant.test", "ALL", false), + testAccPrivilege("mysql_grant.test", "DROP", true), + testAccPrivilege("mysql_grant.test", "SELECT(c1,c2)", true), + testAccPrivilege("mysql_grant.test", "INSERT(c5)", true), + testAccPrivilege("mysql_grant.test", "REFERENCES(c1)", true), resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), @@ -249,9 +246,9 @@ func TestAccGrantComplexMySQL8(t *testing.T) { { Config: testAccGrantConfigWithDynamicMySQL8(dbName), Check: resource.ComposeTestCheckFunc( - testAccPrivilege("mysql_grant.test", "SHOW DATABASES", true, false), - testAccPrivilege("mysql_grant.test", "CONNECTION_ADMIN", true, false), - testAccPrivilege("mysql_grant.test", "SELECT", true, false), + testAccPrivilege("mysql_grant.test", "SHOW DATABASES", true), + testAccPrivilege("mysql_grant.test", "CONNECTION_ADMIN", true), + testAccPrivilege("mysql_grant.test", "SELECT", true), ), }, }, @@ -280,7 +277,6 @@ func TestAccGrant_role(t *testing.T) { Config: testAccGrantConfigRoleWithGrantOption(dbName, roleName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("mysql_grant.test", "role", roleName), - resource.TestCheckResourceAttr("mysql_grant.test", "grant", "true"), ), }, { @@ -350,7 +346,7 @@ func prepareTable(dbname string) resource.TestCheckFunc { } // Test privilege - one can condition it exists or that it doesn't exist. -func testAccPrivilege(rn string, privilege string, expectExists bool, expectGrant bool) resource.TestCheckFunc { +func testAccPrivilege(rn string, privilege string, expectExists bool) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[rn] if !ok { @@ -369,17 +365,12 @@ func testAccPrivilege(rn string, privilege string, expectExists bool, expectGran id := strings.Split(rs.Primary.ID, ":") - var userOrRole UserOrRole + var userOrRole string if strings.Contains(id[0], "@") { parts := strings.Split(id[0], "@") - userOrRole = UserOrRole{ - Name: parts[0], - Host: parts[1], - } + userOrRole = fmt.Sprintf("'%s'@'%s'", parts[0], parts[1]) } else { - userOrRole = UserOrRole{ - Name: id[0], - } + userOrRole = fmt.Sprintf("'%s'", id[0]) } grants, err := showUserGrants(context.Background(), db, userOrRole) @@ -389,35 +380,27 @@ func testAccPrivilege(rn string, privilege string, expectExists bool, expectGran privilegeNorm := normalizePerms([]string{privilege})[0] - var expectedGrant MySQLGrant + haveGrant := false Outer: for _, grant := range grants { - grantWithPrivs, ok := grant.(MySQLGrantWithPrivileges) - if !ok { - continue - } - for _, priv := range grantWithPrivs.GetPrivileges() { - log.Printf("[DEBUG] Checking grant %s against %s", priv, privilegeNorm) + privs := normalizePerms(grant.Privileges) + for _, priv := range privs { if priv == privilegeNorm { - expectedGrant = grant + haveGrant = true break Outer } } } - if expectExists != (expectedGrant != nil) { - if expectedGrant != nil { + if expectExists != haveGrant { + if haveGrant { return fmt.Errorf("grant %s found but it was not requested for %s", privilege, userOrRole) } else { - return fmt.Errorf("grant %s not found for %s", privilegeNorm, userOrRole) + return fmt.Errorf("grant %s not found for %s", privilege, userOrRole) } } - if expectedGrant != nil && expectedGrant.GrantOption() != expectGrant { - return fmt.Errorf("grant %s found but had incorrect grant option", privilege) - } - // We match expectations. return nil } @@ -759,162 +742,3 @@ func testAccGrantConfigComplexRoleGrants(user string) string { privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "RELOAD", "PROCESS", "REFERENCES", "INDEX", "ALTER", "SHOW DATABASES", "CREATE TEMPORARY TABLES", "LOCK TABLES", "EXECUTE", "REPLICATION SLAVE", "REPLICATION CLIENT", "CREATE VIEW", "SHOW VIEW", "CREATE ROUTINE", "ALTER ROUTINE", "CREATE USER", "EVENT", "TRIGGER"] }`, user) } - -func prepareProcedure(dbname string, procedureName string) resource.TestCheckFunc { - return func(s *terraform.State) error { - ctx := context.Background() - db, err := connectToMySQL(ctx, testAccProvider.Meta().(*MySQLConfiguration)) - if err != nil { - return err - } - - // Switch to the specified database - _, err = db.ExecContext(ctx, fmt.Sprintf("USE `%s`", dbname)) - if err != nil { - return fmt.Errorf("Error selecting database %s: %s", dbname, err) - } - - // Check if the procedure exists - var exists int - checkExistenceSQL := fmt.Sprintf(` -SELECT COUNT(*) -FROM information_schema.ROUTINES -WHERE ROUTINE_SCHEMA = ? AND ROUTINE_NAME = ? AND ROUTINE_TYPE = 'PROCEDURE' -`) - err = db.QueryRowContext(ctx, checkExistenceSQL, dbname, procedureName).Scan(&exists) - if err != nil { - return fmt.Errorf("Error checking existence of procedure %s: %s", procedureName, err) - } - - if exists > 0 { - return nil - } - - // Create the procedure - createProcedureSQL := fmt.Sprintf(` - CREATE PROCEDURE %s() - BEGIN - SELECT 1; - END - `, procedureName) - if _, err := db.Exec(createProcedureSQL); err != nil { - return fmt.Errorf("error reading grant: %s", err) - } - return nil - } -} - -func TestAccGrantOnProcedure(t *testing.T) { - procedureName := "test_procedure" - dbName := fmt.Sprintf("tf-test-%d", rand.Intn(100)) - userName := fmt.Sprintf("jdoe-%s", dbName) - hostName := "%" - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheckSkipTiDB(t); testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccGrantCheckDestroy, - Steps: []resource.TestStep{ - { - // Create table first - Config: testAccGrantConfigNoGrant(dbName), - Check: resource.ComposeTestCheckFunc( - prepareTable(dbName), - ), - }, - { - // Create a procedure - Config: testAccGrantConfigNoGrant(dbName), - Check: resource.ComposeTestCheckFunc( - prepareProcedure(dbName, procedureName), - ), - }, - { - Config: testAccGrantConfigProcedure(procedureName, dbName, hostName), - Check: resource.ComposeTestCheckFunc( - testAccCheckProcedureGrant("mysql_grant.test_procedure", userName, hostName, procedureName, true), - resource.TestCheckResourceAttr("mysql_grant.test_procedure", "user", userName), - resource.TestCheckResourceAttr("mysql_grant.test_procedure", "host", hostName), - resource.TestCheckResourceAttr("mysql_grant.test_procedure", "database", fmt.Sprintf("PROCEDURE %s.%s", dbName, procedureName)), - resource.TestCheckResourceAttr("mysql_grant.test_procedure", "table", "*"), // Ensure table attribute is empty for procedures - ), - }, - }, - }) -} - -func testAccGrantConfigProcedure(procedureName string, dbName string, hostName string) string { - return fmt.Sprintf(` -resource "mysql_database" "test" { - name = "%s" -} - -resource "mysql_user" "test" { - user = "jdoe-%s" - host = "example.com" -} - -resource "mysql_user" "test_global" { - user = "jdoe-%s" - host = "%%" -} - -resource "mysql_grant" "test_procedure" { - user = "jdoe-%s" - host = "%s" - privileges = ["EXECUTE"] - database = "PROCEDURE %s.%s" -} -`, dbName, dbName, dbName, dbName, hostName, dbName, procedureName) -} - -func testAccCheckProcedureGrant(resourceName, userName, hostName, procedureName string, expected bool) resource.TestCheckFunc { - return func(s *terraform.State) error { - // Obtain the database connection from the Terraform provider - ctx := context.Background() - db, err := connectToMySQL(ctx, testAccProvider.Meta().(*MySQLConfiguration)) - if err != nil { - return err - } - - // Query to show grants for the specific user - query := fmt.Sprintf("SHOW GRANTS FOR '%s'@'%s'", userName, hostName) - - // Use db.Query to execute the query - rows, err := db.Query(query) - if err != nil { - return err - } - defer rows.Close() - - // Variable to track if the required privilege is found - found := false - - // Iterate through the results - for rows.Next() { - var grant string - if err := rows.Scan(&grant); err != nil { - return err - } - - // Check if the grant string contains the necessary privilege - // Adjust the following line according to the exact format of your GRANT statement - if strings.Contains(grant, fmt.Sprintf("`%s`", procedureName)) && strings.Contains(grant, "EXECUTE") { - found = true - break - } - } - - // Check if there was an error during iteration - if err := rows.Err(); err != nil { - return err - } - - // Compare the result with the expected outcome - if found != expected { - return fmt.Errorf("Grant for procedure %s does not match expected state: %v", procedureName, expected) - } - - return nil - } -}