Skip to content

checker: support HeatWave grants in precheck#12752

Open
GMHDBJD wants to merge 1 commit into
pingcap:masterfrom
GMHDBJD:tiflow-heatwave-precheck-20260702165019
Open

checker: support HeatWave grants in precheck#12752
GMHDBJD wants to merge 1 commit into
pingcap:masterfrom
GMHDBJD:tiflow-heatwave-precheck-20260702165019

Conversation

@GMHDBJD

@GMHDBJD GMHDBJD commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: ref #12751

This PR fixes DM precheck compatibility with Oracle HeatWave/MySQL 8 style SHOW GRANTS output.

Problems addressed:

  • HeatWave may return role grants with WITH ADMIN OPTION, which TiDB parser cannot parse as GrantRoleStmt yet.
  • HeatWave may return partial revoke statements in SHOW GRANTS; these are not direct grants and should not fail privilege verification.
  • HeatWave grants FLUSH_TABLES as a dynamic privilege. It should satisfy the FTWRL dump privilege requirement currently modeled as RELOAD in DM precheck.

What is changed and how it works?

  • Add a DM checker-local showGrants helper to tolerate role grants with WITH ADMIN OPTION while still discovering active roles via SHOW GRANTS ... USING ....
  • Ignore role grants and revoke statements during direct privilege verification.
  • Treat global extended privilege FLUSH_TABLES as satisfying the dump privilege checker’s FTWRL requirement.
  • Add unit coverage for HeatWave-style grants, role grants with WITH ADMIN OPTION, and FLUSH_TABLES dynamic privilege handling.

Check List

Tests:

  • Unit test
  • Manual test with real HeatWave source

Manual validation:

  • go test ./dm/pkg/checker ./dm/checker
  • Built dm-master, dm-worker, and dmctl from this branch.
  • Ran DM against Oracle HeatWave on AWS MySQL 9.7 with precheck enabled and without ignore-checking-items.
  • check-task passed with only the MySQL version warning.
  • start-task succeeded.
  • Full load plus incremental insert/update/delete synced correctly to TiDB.

Related issue: #12751

Signed-off-by: gmhdbjd <gmhdbjd@gmail.com>
@ti-chi-bot

ti-chi-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Adding the "do-not-merge/release-note-label-needed" label because no release-note block was detected, please follow our release note process to remove it.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@ti-chi-bot ti-chi-bot Bot added do-not-merge/needs-triage-completed do-not-merge/release-note-label-needed Indicates that a PR should not merge because it's missing one of the release note labels. area/dm Issues or PRs related to DM. labels Jul 2, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign lance6716 for approval. For more information see the Code Review Process.
Please ensure that each of them provides their approval before proceeding.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@ti-chi-bot ti-chi-bot Bot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Jul 2, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request replaces dbutil.ShowGrants with a local showGrants implementation to better handle MySQL 8.0 and HeatWave role grants, specifically ignoring or trimming the WITH ADMIN OPTION suffix. It also adds support for ignoring REVOKE statements and mapping the FLUSH_TABLES privilege to RELOAD. The reviewer feedback highlights two important issues: first, trimAdminOption can potentially panic or slice incorrectly when handling non-ASCII characters due to index mismatches after calling strings.ToUpper; second, failing the entire showGrants execution when a single grant statement fails to parse during role discovery is too fragile and should instead log a warning and continue.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +547 to +554
func trimAdminOption(grant string) string {
upperGrant := strings.ToUpper(grant)
idx := strings.LastIndex(upperGrant, " WITH ADMIN OPTION")
if idx < 0 {
return grant
}
return strings.TrimSpace(grant[:idx])
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using strings.ToUpper to find the index of a substring and then slicing the original string grant using that index can lead to panics or incorrect slicing if the string contains non-ASCII characters (e.g., in usernames or role names). This is because strings.ToUpper can change the byte length of certain Unicode characters (such as Turkish 'i' or other special characters), causing the byte indices to shift.\n\nSince WITH ADMIN OPTION is expected to be a suffix, we can safely perform a case-insensitive suffix check and slice using the known ASCII suffix length.

func trimAdminOption(grant string) string {\n\ttrimmed := strings.TrimSpace(grant)\n\tconst suffix = \" WITH ADMIN OPTION\"\n\tif len(trimmed) >= len(suffix) && strings.EqualFold(trimmed[len(trimmed)-len(suffix):], suffix) {\n\t\treturn strings.TrimSpace(trimmed[:len(trimmed)-len(suffix)])\n\t}\n\treturn grant\n}

Comment on lines +508 to +511
node, err := p.ParseOneStmt(grantForParse, "", "")
if err != nil {
return nil, errors.New(err.Error())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Failing the entire showGrants function (and thus the entire precheck) if a single grant statement fails to parse during role discovery is too restrictive and fragile. If a user has a custom or unsupported grant syntax that is ignored during privilege verification (via shouldIgnoreGrant), it will still cause a hard failure here.\n\nSince this loop is only used for best-effort role discovery, we should log a warning and continue instead of returning an error.

\t\tnode, err := p.ParseOneStmt(grantForParse, \"\", \"\")\n\t\tif err != nil {\n\t\t\tlog.L().Warn(\"failed to parse grant statement during role discovery\", zap.String(\"grant\", grant), zap.Error(err))\n\t\t\tcontinue\n\t\t}

@ti-chi-bot

ti-chi-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

@GMHDBJD: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
pull-dm-integration-test 8e83b34 link true /test pull-dm-integration-test

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

return readGrants(builder.String())
}

func shouldIgnoreGrant(grant string) bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe called containsWithAdminOption

Comment on lines +310 to +312
if shouldIgnoreGrant(grant) {
continue
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as we have consider the WITH ADMIN OPTION inside showGrants, we can remove this check?

case ast.GrantLevelGlobal:
for _, privElem := range grantStmt.Privs {
if privElem.Priv == mysql.ExtendedPriv {
if strings.EqualFold(privElem.Name, "FLUSH_TABLES") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this HW specific? maybe add a comment

@D3Hunter D3Hunter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

  • Total findings: 4
  • Inline comments: 4
  • Summary-only findings (no inline anchor): 0
Findings (highest risk first)

⚠️ [Major] (2)

  1. Partial revokes can be reported as sufficient privileges (dm/pkg/checker/privilege.go:320)
  2. Version-specific MariaDB grants can fail before MariaDB parsing is enabled (dm/pkg/checker/privilege.go:501)

🟡 [Minor] (1)

  1. Grant predicate name hides the role-admin case (dm/pkg/checker/privilege.go:505 and dm/pkg/checker/privilege.go:534)

ℹ️ [Info] (1)

  1. FLUSH_TABLES-to-RELOAD mapping needs rationale (dm/pkg/checker/privilege.go:336)

if !ok {
switch node.(type) {
case *ast.GrantProxyStmt, *ast.GrantRoleStmt:
case *ast.GrantProxyStmt, *ast.GrantRoleStmt, *ast.RevokeStmt:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [Major] Partial revokes can be reported as sufficient privileges

Why
The new *ast.RevokeStmt branch treats parsed partial-revoke rows from SHOW GRANTS as non-operative metadata, but MySQL partial revokes subtract privileges from earlier global grants for a specific schema or table. The verifier only removes privileges from lackPrivs; it never applies a revoke back to the required table or schema set.

Scope
dm/pkg/checker/privilege.go:320

Risk if unchanged
A source account with a global required privilege plus REVOKE ... ON checked_db.* can pass DM's dump privilege precheck even though the account is denied on the checked schema or table. DM may then start after a checker success and fail later during dump/load/replication access, and an unsupported partial-revoke case becomes a silent compatibility pass.

Evidence
VerifyPrivileges now continues on *ast.RevokeStmt, while the global grant branch deletes required privileges from lackPrivs. The concrete state transition is: pending SelectPriv for app.t -> GRANT SELECT ON *.* clears it -> REVOKE SELECT ON app.* is skipped -> final result incorrectly has no missing SELECT. The parser AST exposes RevokeStmt.Privs and RevokeStmt.Level, and the added test at dm/pkg/checker/privilege_test.go:219 uses REVOKE ... ON sys.* without a checked sys table, so it does not cover an overlapping revoke.

Change request
Please model RevokeStmt for required privileges, or fail the precheck conservatively when a partial revoke overlaps the checked schema/table; add a unit test for global SELECT plus REVOKE SELECT ON checked_db.* where the checker must fail.

// For MySQL 8.0, collect granted roles and read grants using those roles.
// HeatWave SHOW GRANTS may append `WITH ADMIN OPTION` to role grants, which
// TiDB parser cannot parse yet. Strip the suffix for role discovery only.
var roles []string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [Major] Version-specific MariaDB grants can fail before MariaDB parsing is enabled

Why
The new showGrants helper pre-parses every returned grant only to discover roles, but it does so with a plain TiDB parser before the existing MariaDB parser mode in VerifyPrivileges can run.

Scope
dm/pkg/checker/privilege.go:501

Risk if unchanged
MariaDB sources can get a checker error for grant syntax that the existing verifier is explicitly prepared to parse, changing the precheck result from a privilege decision into an early parse failure.

Evidence
VerifyPrivileges enables p.SetMariaDB(true) when the stored version contains MariaDB, with a comment calling out BINLOG MONITOR and other MariaDB things. The new showGrants has no version parameter and returns immediately on any parse error before verifyPrivilegesWithResult receives the grant list and version.

Change request
Please either pass the DB version into showGrants and use the same parser mode, or avoid failing role discovery on non-role grant parse errors; add a case for a MariaDB-specific grant row to keep this path covered.

p := parser.New()
for _, grant := range grants {
grantForParse := grant
if shouldIgnoreGrant(grant) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [Minor] Grant predicate name hides the role-admin case

Why
shouldIgnoreGrant reads like a general privilege-check decision, but the implementation only detects role grants with WITH ADMIN OPTION; in showGrants the same predicate is used to trim and parse the role instead of ignoring it.

Scope
dm/pkg/checker/privilege.go:505 and dm/pkg/checker/privilege.go:534

Risk if unchanged
Future changes can reuse the predicate as a broad ignore gate and accidentally skip grant forms that still need parsing, or miss that role discovery depends on this exact HeatWave role-grant shape.

Evidence
The predicate returns true only for GRANT ... TO ... WITH ADMIN OPTION statements without ON. VerifyPrivileges continues on it, while showGrants treats the same true value as trimAdminOption(grant) before parsing roles.

Change request
Prefer a more specific name, for example isRoleGrantWithAdminOption, and use a separate shouldSkipGrantForPrivilegeCheck wrapper if the verification path needs the ignore wording.

switch grantStmt.Level.Level {
case ast.GrantLevelGlobal:
for _, privElem := range grantStmt.Privs {
if privElem.Priv == mysql.ExtendedPriv {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ [Info] FLUSH_TABLES-to-RELOAD mapping needs rationale

Why
The new dynamic-privilege branch maps FLUSH_TABLES to mysql.ReloadPriv, which is a non-obvious cross-vendor privilege equivalence.

Scope
dm/pkg/checker/privilege.go:336

Risk if unchanged
Maintainers may treat other FLUSH_* dynamic privileges the same way, or remove the special case, because the intent is not captured near the enforcement point.

Evidence
The code deletes mysql.ReloadPriv only for mysql.ExtendedPriv named FLUSH_TABLES, while the adjacent comments still describe only AllPriv; the added tests separately assert FLUSH_STATUS does not satisfy RELOAD.

Change request
Please add a short why-comment for this branch explaining why FLUSH_TABLES is accepted as satisfying RELOAD and why sibling FLUSH_* extended privileges are not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/dm Issues or PRs related to DM. do-not-merge/needs-triage-completed do-not-merge/release-note-label-needed Indicates that a PR should not merge because it's missing one of the release note labels. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants