Skip to content

Commit

Permalink
Merge pull request #83 from atarax/grants_tables
Browse files Browse the repository at this point in the history
Support mysql-grants by table
  • Loading branch information
Duologic authored May 3, 2022
2 parents ca1b76f + 7c4873c commit 3fcd1c9
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 22 deletions.
4 changes: 4 additions & 0 deletions apis/mysql/v1alpha1/grant_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ type GrantParameters struct {
// +optional
UserSelector *xpv1.Selector `json:"userSelector,omitempty"`

// Tables this grant is for.
// +optional
Table *string `json:"table,omitempty" default:"*"`

// Database this grant is for.
// +optional
Database *string `json:"database,omitempty"`
Expand Down
5 changes: 5 additions & 0 deletions apis/mysql/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions examples/mysql/grant_table.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: mysql.sql.crossplane.io/v1alpha1
kind: Grant
metadata:
name: example-grant-table
spec:
forProvider:
privileges:
- DROP
- INSERT
- SELECT
table: example-table
userRef:
name: example-user
databaseRef:
name: example-db
3 changes: 3 additions & 0 deletions package/crds/mysql.sql.crossplane.io_grants.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ spec:
type: string
minItems: 1
type: array
table:
description: Tables this grant is for.
type: string
user:
description: User this grant is for.
type: string
Expand Down
57 changes: 35 additions & 22 deletions pkg/controller/mysql/grant/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const (
)

var (
grantRegex = regexp.MustCompile("^GRANT (.+) ON `(.+)`\\.\\* TO .+")
grantRegex = regexp.MustCompile("^GRANT (.+) ON `(.+)`\\.(.+) TO .+")
)

// Setup adds a controller that reconciles Grant managed resources.
Expand Down Expand Up @@ -141,8 +141,9 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex

username := *cr.Spec.ForProvider.User
dbname := *cr.Spec.ForProvider.Database
table := defaultTable(cr.Spec.ForProvider.Table)

privileges, result, err := c.getPrivileges(ctx, username, dbname)
privileges, result, err := c.getPrivileges(ctx, username, dbname, table)
if err != nil {
return managed.ExternalObservation{}, err
}
Expand All @@ -165,6 +166,13 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex
}, nil
}

func defaultTable(table *string) string {
if !(table == nil) {
return mysql.QuoteIdentifier(*table)
}
return "*"
}

func privilegesEqual(a, b []string) bool {
if len(a) != len(b) {
return false
Expand All @@ -183,15 +191,15 @@ func privilegesEqual(a, b []string) bool {
return true
}

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

func (c *external) getPrivileges(ctx context.Context, username, dbname string) ([]string, *managed.ExternalObservation, error) {
func (c *external) getPrivileges(ctx context.Context, username, dbname string, table string) ([]string, *managed.ExternalObservation, error) {
username, host := mysql.SplitUserHost(username)
query := fmt.Sprintf("SHOW GRANTS FOR %s@%s", mysql.QuoteValue(username), mysql.QuoteValue(host))
rows, err := c.db.Query(ctx, xsql.Query{String: query})
Expand All @@ -206,19 +214,15 @@ func (c *external) getPrivileges(ctx context.Context, username, dbname string) (
if err := rows.Scan(&grant); err != nil {
return nil, nil, errors.Wrap(err, errCurrentGrant)
}
p := parseGrant(grant, dbname)
if p != nil && privileges != nil {
// Found more than one grant for this user/DB pair.
// This probably shouldn't happen because MySQL groups privileges
// for the same user/DB pair in a single grant.
// In any case we want to update and ensure the privileges of our grant.
return nil, &managed.ExternalObservation{
ResourceExists: true,
ResourceUpToDate: false,
}, nil
p := parseGrant(grant, dbname, table)

if p != nil {
// found the grant we were looking for
privileges = p
break
}
privileges = p
}

if err := rows.Err(); err != nil {
var myErr *mysqldriver.MySQLError
if errors.As(err, &myErr) && myErr.Number == errCodeNoSuchGrant {
Expand All @@ -241,10 +245,11 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext

username := *cr.Spec.ForProvider.User
dbname := *cr.Spec.ForProvider.Database
table := defaultTable(cr.Spec.ForProvider.Table)

privileges := strings.Join(cr.Spec.ForProvider.Privileges.ToStringSlice(), ", ")

query := createGrantQuery(privileges, dbname, username)
query := createGrantQuery(privileges, dbname, username, table)
if err := c.db.Exec(ctx, xsql.Query{String: query}); err != nil {
return managed.ExternalCreation{}, errors.Wrap(err, errCreateGrant)
}
Expand All @@ -260,6 +265,7 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext

username := *cr.Spec.ForProvider.User
dbname := *cr.Spec.ForProvider.Database
table := defaultTable(cr.Spec.ForProvider.Table)

privileges := strings.Join(cr.Spec.ForProvider.Privileges.ToStringSlice(), ", ")
username, host := mysql.SplitUserHost(username)
Expand All @@ -269,31 +275,35 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext
// until the privileges are granted again.
// Using a transaction is unfortunately not possible because a GRANT triggers
// an implicit commit: https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html
query := fmt.Sprintf("REVOKE ALL ON %s.* FROM %s@%s",
query := fmt.Sprintf("REVOKE ALL ON %s.%s FROM %s@%s",
mysql.QuoteIdentifier(dbname),
table,
mysql.QuoteValue(username),
mysql.QuoteValue(host),
)
if err := c.db.Exec(ctx, xsql.Query{String: query}); err != nil {
return managed.ExternalUpdate{}, errors.Wrap(err, errRevokeGrant)
}

query = createGrantQuery(privileges, dbname, username)
query = createGrantQuery(privileges, dbname, username, table)
if err := c.db.Exec(ctx, xsql.Query{String: query}); err != nil {
return managed.ExternalUpdate{}, err
}
err := c.db.Exec(ctx, xsql.Query{String: "FLUSH PRIVILEGES"})
return managed.ExternalUpdate{}, errors.Wrap(err, errFlushPriv)
}

func createGrantQuery(privileges, dbname, username string) string {
func createGrantQuery(privileges, dbname, username string, table string) string {
username, host := mysql.SplitUserHost(username)
return fmt.Sprintf("GRANT %s ON %s.* TO %s@%s",
result := fmt.Sprintf("GRANT %s ON %s.%s TO %s@%s",
privileges,
mysql.QuoteIdentifier(dbname),
table,
mysql.QuoteValue(username),
mysql.QuoteValue(host),
)

return result
}

func (c *external) Delete(ctx context.Context, mg resource.Managed) error {
Expand All @@ -304,16 +314,19 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error {

username := *cr.Spec.ForProvider.User
dbname := *cr.Spec.ForProvider.Database
table := defaultTable(cr.Spec.ForProvider.Table)

privileges := strings.Join(cr.Spec.ForProvider.Privileges.ToStringSlice(), ", ")
username, host := mysql.SplitUserHost(username)

query := fmt.Sprintf("REVOKE %s ON %s.* FROM %s@%s",
query := fmt.Sprintf("REVOKE %s ON %s.%s FROM %s@%s",
privileges,
mysql.QuoteIdentifier(dbname),
table,
mysql.QuoteValue(username),
mysql.QuoteValue(host),
)

if err := c.db.Exec(ctx, xsql.Query{String: query}); err != nil {
return errors.Wrap(err, errRevokeGrant)
}
Expand Down
64 changes: 64 additions & 0 deletions pkg/controller/mysql/grant/reconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,70 @@ func TestObserve(t *testing.T) {
},
},
},
want: want{
o: managed.ExternalObservation{
ResourceExists: true,
ResourceUpToDate: true,
},
err: nil,
},
},
"SuccessGrantWithTables": {
reason: "We should see the grants in sync when using a table",
fields: fields{
db: mockDB{
MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) {
return mockRowsToSQLRows(
sqlmock.NewRows([]string{"Grants"}).
AddRow("GRANT CREATE, DROP ON `success-db`.`success-table` TO 'success-user'@%"),
), nil
},
},
},
args: args{
mg: &v1alpha1.Grant{
Spec: v1alpha1.GrantSpec{
ForProvider: v1alpha1.GrantParameters{
Database: pointer.StringPtr("success-db"),
User: pointer.StringPtr("success-user"),
Table: pointer.StringPtr("success-table"),
Privileges: v1alpha1.GrantPrivileges{"DROP", "CREATE"},
},
},
},
},
want: want{
o: managed.ExternalObservation{
ResourceExists: true,
ResourceUpToDate: true,
},
err: nil,
},
},
"SuccessDiffGrantWithTables": {
reason: "We should see the grants out of sync when using a table",
fields: fields{
db: mockDB{
MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) {
return mockRowsToSQLRows(
sqlmock.NewRows([]string{"Grants"}).
AddRow("GRANT CREATE, DROP ON `success-db`.`success-table` TO 'success-user'@%"),
), nil
},
},
},
args: args{
mg: &v1alpha1.Grant{
Spec: v1alpha1.GrantSpec{
ForProvider: v1alpha1.GrantParameters{
Database: pointer.StringPtr("success-db"),
User: pointer.StringPtr("success-user"),
Table: pointer.StringPtr("success-table"),
Privileges: v1alpha1.GrantPrivileges{"INSERT", "CREATE"},
},
},
},
},
want: want{
o: managed.ExternalObservation{
ResourceExists: true,
Expand Down

0 comments on commit 3fcd1c9

Please sign in to comment.