Skip to content

Commit

Permalink
Support MySQL WITH GRANT OPTION (cherry-picks #132) (#148)
Browse files Browse the repository at this point in the history
* Support mysql grant option by updating grantRegex and parseGrants function

Signed-off-by: Alejandro Recalde <[email protected]>
Signed-off-by: Duologic <[email protected]>

* When GRANT OPTION is present append WITH GRANT OPTION to query

Signed-off-by: Alejandro Recalde <[email protected]>
Signed-off-by: Duologic <[email protected]>

* fix: tests

Signed-off-by: Duologic <[email protected]>

---------

Signed-off-by: Alejandro Recalde <[email protected]>
Signed-off-by: Duologic <[email protected]>
Co-authored-by: Alejandro Recalde <[email protected]>
Co-authored-by: Duologic <[email protected]>
  • Loading branch information
3 people authored Aug 24, 2023
1 parent eb43750 commit c1ebf45
Show file tree
Hide file tree
Showing 2 changed files with 184 additions and 17 deletions.
44 changes: 29 additions & 15 deletions pkg/controller/mysql/grant/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const (
)

var (
grantRegex = regexp.MustCompile(`^GRANT (.+) ON (.+)\.(.+) TO .+`)
grantRegex = regexp.MustCompile(`^GRANT (.+) ON (\S+)\.(\S+) TO \S+@\S+?(\sWITH GRANT OPTION)?$`)
)

// Setup adds a controller that reconciles Grant managed resources.
Expand Down Expand Up @@ -172,9 +172,16 @@ func defaultIdentifier(identifier *string) string {

func parseGrant(grant, dbname string, table string) (privileges []string) {
matches := grantRegex.FindStringSubmatch(grant)
if len(matches) == 4 && matches[2] == dbname && matches[3] == table {
return strings.Split(matches[1], ", ")
if len(matches) == 5 && matches[2] == dbname && matches[3] == table {
privileges := strings.Split(matches[1], ", ")

if matches[4] != "" {
privileges = append(privileges, "GRANT OPTION")
}

return privileges
}

return nil
}

Expand Down Expand Up @@ -252,13 +259,13 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext
table := defaultIdentifier(cr.Spec.ForProvider.Table)

privileges := strings.Join(cr.Spec.ForProvider.Privileges.ToStringSlice(), ", ")
grantOption := hasGrantOption(cr)
binlog := cr.Spec.ForProvider.BinLog
query := createGrantQuery(privileges, dbname, username, table)
query := createGrantQuery(privileges, dbname, username, table, grantOption)

if err := mysql.ExecWithBinlogAndFlush(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errCreateGrant}, mysql.ExecOptions{Binlog: binlog}); err != nil {
return managed.ExternalCreation{}, err
}

return managed.ExternalCreation{}, nil
}

Expand All @@ -278,6 +285,7 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext
observed := cr.Status.AtProvider.Privileges
desired := cr.Spec.ForProvider.Privileges.ToStringSlice()
toGrant, toRevoke := diffPermissions(desired, observed)
grantOption := hasGrantOption(cr)

if len(toRevoke) > 0 {
sort.Strings(toRevoke)
Expand All @@ -300,14 +308,7 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext

if len(toGrant) > 0 {
sort.Strings(toGrant)
query := fmt.Sprintf("GRANT %s ON %s.%s TO %s@%s",
strings.Join(toGrant, ", "),
dbname,
table,
mysql.QuoteValue(username),
mysql.QuoteValue(host),
)

query := createGrantQuery(strings.Join(toGrant, ", "), dbname, username, table, grantOption)
if err := mysql.ExecWithBinlogAndFlush(ctx, c.db,
mysql.ExecQuery{
Query: query, ErrorValue: errCreateGrant,
Expand All @@ -316,11 +317,20 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext
return managed.ExternalUpdate{}, err
}
}

return managed.ExternalUpdate{}, nil
}

func createGrantQuery(privileges, dbname, username string, table string) string {
// hasGrantOption returns true if the privileges has a grant option item
func hasGrantOption(cr *v1alpha1.Grant) bool {
for _, p := range cr.Spec.ForProvider.Privileges {
if string(p) == "GRANT OPTION" {
return true
}
}
return false
}

func createGrantQuery(privileges, dbname, username string, table string, grantOption bool) string {
username, host := mysql.SplitUserHost(username)
result := fmt.Sprintf("GRANT %s ON %s.%s TO %s@%s",
privileges,
Expand All @@ -330,6 +340,10 @@ func createGrantQuery(privileges, dbname, username string, table string) string
mysql.QuoteValue(host),
)

if grantOption {
result = fmt.Sprintf("%s WITH GRANT OPTION", result)
}

return result
}

Expand Down
157 changes: 155 additions & 2 deletions pkg/controller/mysql/grant/reconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,79 @@ func TestObserve(t *testing.T) {
observedPrivileges: []string{allPrivileges},
},
},
"SuccessGrantOptionNoDatabase": {
reason: "We should return no error if we can successfully show our grants",
fields: fields{
db: mockDB{
MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) {
return mockRowsToSQLRows(
sqlmock.NewRows(
[]string{"Grants"},
).AddRow("GRANT INSERT, SELECT ON *.* TO 'success-user'@% WITH GRANT OPTION"),
), nil
},
},
},
args: args{
mg: &v1alpha1.Grant{
Spec: v1alpha1.GrantSpec{
ForProvider: v1alpha1.GrantParameters{
User: pointer.StringPtr("success-user"),
Privileges: v1alpha1.GrantPrivileges{"INSERT", "SELECT", "GRANT OPTION"},
},
},
},
},
want: want{
o: managed.ExternalObservation{
ResourceExists: true,
ResourceUpToDate: true,
},
err: nil,
observedPrivileges: []string{
"GRANT OPTION",
"INSERT",
"SELECT",
},
},
},
"SuccessGrantOptionWithDatabase": {
reason: "We should return no error if we can successfully show our grants",
fields: fields{
db: mockDB{
MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) {
return mockRowsToSQLRows(
sqlmock.NewRows(
[]string{"Grants"},
).AddRow("GRANT INSERT, SELECT ON `success-db`.* TO 'success-user'@% WITH GRANT OPTION"),
), nil
},
},
},
args: args{
mg: &v1alpha1.Grant{
Spec: v1alpha1.GrantSpec{
ForProvider: v1alpha1.GrantParameters{
Database: pointer.StringPtr("success-db"),
User: pointer.StringPtr("success-user"),
Privileges: v1alpha1.GrantPrivileges{"INSERT", "SELECT", "GRANT OPTION"},
},
},
},
},
want: want{
o: managed.ExternalObservation{
ResourceExists: true,
ResourceUpToDate: true,
},
err: nil,
observedPrivileges: []string{
"GRANT OPTION",
"INSERT",
"SELECT",
},
},
},
"SuccessDiffGrants": {
reason: "We should return no error if different grants exist for the provided database",
fields: fields{
Expand Down Expand Up @@ -635,8 +708,59 @@ func TestCreate(t *testing.T) {
mg: &v1alpha1.Grant{
Spec: v1alpha1.GrantSpec{
ForProvider: v1alpha1.GrantParameters{
Database: pointer.StringPtr("test-example"),
User: pointer.StringPtr("test-example"),
Database: pointer.StringPtr("test-example"),
User: pointer.StringPtr("test-example"),
Privileges: v1alpha1.GrantPrivileges{"INSERT", "SELECT"},
},
},
},
},
want: want{
err: nil,
},
},
"SuccessNoDatabase": {
reason: "No error should be returned when we successfully create a grant with no database",
fields: fields{
db: &mockDB{
MockExec: func(ctx context.Context, q xsql.Query) error { return nil },
},
},
args: args{
mg: &v1alpha1.Grant{
Spec: v1alpha1.GrantSpec{
ForProvider: v1alpha1.GrantParameters{
User: pointer.StringPtr("test-example"),
Privileges: v1alpha1.GrantPrivileges{"INSERT", "SELECT"},
},
},
},
},
want: want{
err: nil,
},
},
"SuccessGrantOption": {
reason: "No error should be returned when we successfully create a grant with grant option",
fields: fields{
db: &mockDB{
MockExec: func(ctx context.Context, q xsql.Query) error {
if strings.HasPrefix(q.String, "GRANT") &&
!strings.HasSuffix(q.String, "WITH GRANT OPTION") {
return errBoom
}

return nil
},
},
},
args: args{
mg: &v1alpha1.Grant{
Spec: v1alpha1.GrantSpec{
ForProvider: v1alpha1.GrantParameters{
Database: pointer.StringPtr("test-example"),
User: pointer.StringPtr("test-example"),
Privileges: v1alpha1.GrantPrivileges{"GRANT OPTION", "ALL"},
},
},
},
Expand Down Expand Up @@ -857,6 +981,35 @@ func TestUpdate(t *testing.T) {
c: managed.ExternalUpdate{},
},
},
"SuccessGrantOption": {
reason: "No error should be returned when we successfully create a grant with grant option",
fields: fields{
db: &mockDB{
MockExec: func(ctx context.Context, q xsql.Query) error {
if strings.HasPrefix(q.String, "GRANT") &&
!strings.HasSuffix(q.String, "WITH GRANT OPTION") {
return errBoom
}

return nil
},
},
},
args: args{
mg: &v1alpha1.Grant{
Spec: v1alpha1.GrantSpec{
ForProvider: v1alpha1.GrantParameters{
Database: pointer.StringPtr("test-example"),
User: pointer.StringPtr("test-example"),
Privileges: v1alpha1.GrantPrivileges{"GRANT OPTION", "ALL"},
},
},
},
},
want: want{
err: nil,
},
},
}

for name, tc := range cases {
Expand Down

0 comments on commit c1ebf45

Please sign in to comment.