diff --git a/internal/component/database_observability/mysql/collector/health_check.go b/internal/component/database_observability/mysql/collector/health_check.go index 3b9150be605..d32131cb26a 100644 --- a/internal/component/database_observability/mysql/collector/health_check.go +++ b/internal/component/database_observability/mysql/collector/health_check.go @@ -136,6 +136,8 @@ func checkAlloyVersion(ctx context.Context, db *sql.DB) healthCheckResult { } // checkRequiredGrants verifies required privileges are present. +// Requires: PROCESS, REPLICATION CLIENT, SHOW VIEW on *.* +// and SELECT on performance_schema.* func checkRequiredGrants(ctx context.Context, db *sql.DB) healthCheckResult { r := healthCheckResult{name: "RequiredGrantsPresent"} req := map[string]bool{ @@ -160,28 +162,60 @@ func checkRequiredGrants(ctx context.Context, db *sql.DB) healthCheckResult { } up := strings.ToUpper(grantLine) - // Mark individual privileges if present on *.* scope. - for k := range req { - if strings.Contains(up, " ON *.*") && strings.Contains(up, k) { - req[k] = true + if strings.Contains(up, "SELECT") { + if strings.Contains(up, " ON `PERFORMANCE_SCHEMA`.*") || + strings.Contains(up, " ON PERFORMANCE_SCHEMA.*") || + strings.Contains(up, " ON `PERFORMANCE_SCHEMA`.") || + strings.Contains(up, " ON *.*") { + + req["SELECT"] = true + } + } + + if strings.Contains(up, "ALL PRIVILEGES") { + if strings.Contains(up, " ON `PERFORMANCE_SCHEMA`.*") || + strings.Contains(up, " ON PERFORMANCE_SCHEMA.*") || + strings.Contains(up, " ON `PERFORMANCE_SCHEMA`.") || + strings.Contains(up, " ON *.*") { + + req["SELECT"] = true } } + + if strings.Contains(up, "SHOW VIEW") { + req["SHOW VIEW"] = true + } + + if strings.Contains(up, "PROCESS") && strings.Contains(up, " ON *.*") { + req["PROCESS"] = true + } + + if strings.Contains(up, "REPLICATION CLIENT") && strings.Contains(up, " ON *.*") { + req["REPLICATION CLIENT"] = true + } } if err := rows.Err(); err != nil { r.err = fmt.Errorf("iterate SHOW GRANTS: %w", err) return r } - r.result = true - for k, found := range req { - if !found { - r.result = false - if r.value == "" { - r.value = "missing: " + k - } else { - r.value += "," + k - } + r.result = req["PROCESS"] && req["REPLICATION CLIENT"] && req["SELECT"] && req["SHOW VIEW"] + + if !r.result { + var missing []string + if !req["PROCESS"] { + missing = append(missing, "PROCESS") + } + if !req["REPLICATION CLIENT"] { + missing = append(missing, "REPLICATION CLIENT") + } + if !req["SELECT"] { + missing = append(missing, "SELECT on performance_schema.*") + } + if !req["SHOW VIEW"] { + missing = append(missing, "SHOW VIEW") } + r.value = fmt.Sprintf("missing grants: %s", strings.Join(missing, ", ")) } return r diff --git a/internal/component/database_observability/mysql/collector/health_check_test.go b/internal/component/database_observability/mysql/collector/health_check_test.go index 50bdcee3faf..f5e863784f7 100644 --- a/internal/component/database_observability/mysql/collector/health_check_test.go +++ b/internal/component/database_observability/mysql/collector/health_check_test.go @@ -75,9 +75,10 @@ func TestHealthCheck(t *testing.T) { failingCheckName string customSetup func(mock sqlmock.Sqlmock) expectedResult string + expectedValue string }{ { - name: "missing grants", + name: "missing PROCESS and REPLICATION CLIENT grants", failingCheckName: "RequiredGrantsPresent", customSetup: func(mock sqlmock.Sqlmock) { mock.ExpectQuery(`SHOW GRANTS`). @@ -87,6 +88,35 @@ func TestHealthCheck(t *testing.T) { ) }, expectedResult: `result="false"`, + expectedValue: `value="missing grants: PROCESS, REPLICATION CLIENT"`, + }, + { + name: "missing SELECT on performance_schema", + failingCheckName: "RequiredGrantsPresent", + customSetup: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(`SHOW GRANTS`). + WillReturnRows( + sqlmock.NewRows([]string{"Grants"}). + AddRow("GRANT PROCESS, REPLICATION CLIENT, SHOW VIEW ON *.* TO 'user'@'host'"). + AddRow("GRANT SELECT ON cars.* TO 'user'@'host'"), + ) + }, + expectedResult: `result="false"`, + expectedValue: `value="missing grants: SELECT on performance_schema.*"`, + }, + { + name: "missing SELECT and SHOW VIEW grants", + failingCheckName: "RequiredGrantsPresent", + customSetup: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(`SHOW GRANTS`). + WillReturnRows( + sqlmock.NewRows([]string{"Grants"}). + AddRow("GRANT PROCESS, REPLICATION CLIENT ON *.* TO 'user'@'host'"). + AddRow("GRANT SELECT ON cars.* TO 'user'@'host'"), + ) + }, + expectedResult: `result="false"`, + expectedValue: `value="missing grants: SELECT on performance_schema.*, SHOW VIEW"`, }, { name: "no rows in events statements digest", @@ -99,6 +129,7 @@ func TestHealthCheck(t *testing.T) { ) }, expectedResult: `result="false"`, + expectedValue: "", }, } @@ -150,6 +181,9 @@ func TestHealthCheck(t *testing.T) { if strings.Contains(entry.Line, tc.failingCheckName) { require.Equal(t, model.LabelSet{"op": OP_HEALTH_STATUS}, entry.Labels) require.Contains(t, entry.Line, tc.expectedResult) + if tc.expectedValue != "" { + require.Contains(t, entry.Line, tc.expectedValue) + } found = true break }