From 35f1f3518181f5350dd136e78f07efc172180640 Mon Sep 17 00:00:00 2001 From: Benjamin Gentil Date: Fri, 24 Jul 2026 07:54:40 +0200 Subject: [PATCH] fix(mysql): replace SHOW GRANTS string comparison with privilege table query (#1221) SHOW GRANTS output format varies between MySQL and MariaDB, particularly the ordering of privileges in the comma-separated list. This caused false negatives (and reconciliation loop) where privileges existed but string comparison failed. --- internal/datastore/mysql.go | 40 ++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/internal/datastore/mysql.go b/internal/datastore/mysql.go index 1eb8ec7..0a95034 100644 --- a/internal/datastore/mysql.go +++ b/internal/datastore/mysql.go @@ -30,7 +30,6 @@ const ( // quoteMySQLIdentifier; the password literal must be fed escapeMySQLString. mysqlFetchUserStatement = "SELECT User FROM mysql.user WHERE User= ? LIMIT 1" mysqlFetchDBStatement = "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME=? LIMIT 1" - mysqlShowGrantsStatement = "SHOW GRANTS FOR %s@`%%`" mysqlCreateDBStatement = "CREATE DATABASE IF NOT EXISTS %s" mysqlCreateUserStatement = "CREATE USER %s@`%%` IDENTIFIED BY '%s'" mysqlUpdateUserStatement = "ALTER USER %s@`%%` IDENTIFIED BY '%s'" @@ -38,6 +37,18 @@ const ( mysqlDropDBStatement = "DROP DATABASE IF EXISTS %s" mysqlDropUserStatement = "DROP USER IF EXISTS %s" mysqlRevokePrivilegesStatement = "REVOKE ALL PRIVILEGES ON %s.* FROM %s" + mysqlCheckGrantsStatement = ` + SELECT 1 + FROM mysql.db + WHERE user = ? AND db = ? AND host = '%' + AND Select_priv = 'Y' + AND Insert_priv = 'Y' + AND Update_priv = 'Y' + AND Delete_priv = 'Y' + AND Create_priv = 'Y' + AND Alter_priv = 'Y' + AND Index_priv = 'Y' + ` ) type MySQLConnection struct { @@ -266,31 +277,18 @@ func (c *MySQLConnection) DBExists(ctx context.Context, dbName string) (bool, er return ok, nil } -func (c *MySQLConnection) GrantPrivilegesExists(_ context.Context, user, dbName string) (bool, error) { - statementShowGrantsStatement := fmt.Sprintf(mysqlShowGrantsStatement, quoteMySQLIdentifier(user)) - rows, err := c.db.Query(statementShowGrantsStatement) //nolint:sqlclosecheck - if err != nil { - return false, errors.NewGrantPrivilegesError(err) - } +func (c *MySQLConnection) GrantPrivilegesExists(ctx context.Context, user, dbName string) (bool, error) { + var exists int - if err = rows.Err(); err != nil { - return false, errors.NewGrantPrivilegesError(err) - } - - expected := fmt.Sprintf(mysqlGrantPrivilegesStatement, quoteMySQLIdentifier(dbName), quoteMySQLIdentifier(user)) - var grant string - - for rows.Next() { - if err = rows.Scan(&grant); err != nil { - return false, errors.NewGrantPrivilegesError(err) + if err := c.db.QueryRowContext(ctx, mysqlCheckGrantsStatement, user, dbName).Scan(&exists); err != nil { + if err == sql.ErrNoRows { + return false, nil } - if grant == expected { - return true, nil - } + return false, errors.NewCheckGrantExistsError(err) } - return false, nil + return true, nil } func (c *MySQLConnection) DeleteUser(ctx context.Context, user string) error {