Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 61 additions & 22 deletions pkg/privilege/privileges/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,11 +361,12 @@ type MySQLPrivilege struct {
// This means that DB-records are organized in both a
// slice (p.DB) and a Map (p.DBMap).

user bTree[itemUser]
db bTree[itemDB]
tablesPriv bTree[itemTablesPriv]
columnsPriv bTree[itemColumnsPriv]
defaultRoles bTree[itemDefaultRole]
user bTree[itemUser]
db bTree[itemDB]
tablesPriv bTree[itemTablesPriv]
columnsPriv bTree[itemColumnsPriv]
ColumnsPrivMap map[string][]columnsPrivRecord // Accelerate ColumnsPriv searching
defaultRoles bTree[itemDefaultRole]

globalPriv bTree[itemGlobalPriv]
dynamicPriv bTree[itemDynamicPriv]
Expand Down Expand Up @@ -628,6 +629,7 @@ func (p *MySQLPrivilege) merge(diff *MySQLPrivilege, userList map[string]struct{
}
}
ret.columnsPriv.BTreeG = columnsPriv
ret.buildColumnsPrivMap()

defaultRoles := p.defaultRoles.Clone()
for u := range userList {
Expand Down Expand Up @@ -762,19 +764,27 @@ func compareColumnsPrivRecord(x, y columnsPrivRecord) int {
func compareHost(x, y string) int {
// The more-specific, the smaller it is.
// The pattern '%' means “any host” and is least specific.
if y == `%` {
if x == `%` {
if x == "%" || y == "%" {
if x == "%" && y == "%" {
return 0
}
return -1
if y == "%" {
return -1
}
// x == '%'
return 1
}

// The empty string '' also means “any host” but sorts after '%'.
if y == "" {
if x == "" {
if x == "" || y == "" {
if x == "" && y == "" {
return 0
}
return -1
if y == "" {
return -1
}
// x == ""
return 1
}

// One of them end with `%`.
Expand All @@ -797,11 +807,10 @@ func compareHost(x, y string) int {
}

// For other case, the order is nondeterministic.
switch x < y {
case true:
return -1
case false:
if x > y {
return 1
} else if x < y {
return -1
}
return 0
}
Comment on lines 809 to 816

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Outdated comment: the comparison is now deterministic.

The comment states "the order is nondeterministic" but the implementation performs a deterministic lexicographic comparison. This discrepancy could confuse future maintainers.

📝 Proposed fix
-	// For other case, the order is nondeterministic.
+	// For other cases, use lexicographic ordering.
 	if x > y {
 		return 1
 	} else if x < y {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// For other case, the order is nondeterministic.
switch x < y {
case true:
return -1
case false:
if x > y {
return 1
} else if x < y {
return -1
}
return 0
}
// For other cases, use lexicographic ordering.
if x > y {
return 1
} else if x < y {
return -1
}
return 0
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/privilege/privileges/cache.go` around lines 809 - 816, Update the
outdated comment that reads "the order is nondeterministic" to accurately
reflect the deterministic lexicographic comparison implemented here (using the x
> y / x < y branches returning 1/-1/0); locate the comparison routine that
compares x and y (the function containing the shown return 1 / return -1 /
return 0 logic) and reword or remove the misleading sentence so it states that
the comparison is deterministic and follows lexicographic ordering between x and
y.

Expand Down Expand Up @@ -875,9 +884,23 @@ func (p *MySQLPrivilege) LoadTablesPrivTable(exec sqlexec.SQLExecutor) error {
return nil
}

func (p *MySQLPrivilege) buildColumnsPrivMap() {
columnsPrivMap := make(map[string][]columnsPrivRecord, p.columnsPriv.Len())
columnsPriv := p.columnsPriv.Clone()
columnsPriv.Ascend(func(itm itemColumnsPriv) bool {
columnsPrivMap[itm.username] = slices.Clone(itm.data)
return true
})
p.ColumnsPrivMap = columnsPrivMap
}

// LoadColumnsPrivTable loads the mysql.columns_priv table from database.
func (p *MySQLPrivilege) LoadColumnsPrivTable(exec sqlexec.SQLExecutor) error {
return loadTable(exec, sqlLoadColumnsPrivTable, p.decodeColumnsPrivTableRow(nil))
if err := loadTable(exec, sqlLoadColumnsPrivTable, p.decodeColumnsPrivTableRow(nil)); err != nil {
return err
}
p.buildColumnsPrivMap()
return nil
}

// LoadDefaultRoles loads the mysql.columns_priv table from database.
Expand Down Expand Up @@ -1390,10 +1413,12 @@ func (record *tablesPrivRecord) match(user, host, db, table string) bool {
}

func (record *columnsPrivRecord) match(user, host, db, table, col string) bool {
// `SELECT COUNT(*) ...` requires a column-level SELECT privilege of any column,
// so we add a special case "*" here
return record.baseRecord.match(user, host) &&
strings.EqualFold(record.DB, db) &&
strings.EqualFold(record.TableName, table) &&
strings.EqualFold(record.ColumnName, col)
(strings.EqualFold(record.ColumnName, col) || col == "*" && (record.ColumnPriv&mysql.SelectPriv > 0))
}

// patternMatch matches "%" the same way as ".*" in regular expression, for example,
Expand Down Expand Up @@ -1515,7 +1540,21 @@ func (p *MySQLPrivilege) matchTables(user, host, db, table string) *tablesPrivRe
return nil
}

func (p *MySQLPrivilege) matchColumns(user, host, db, table, column string) *columnsPrivRecord {
// MatchColumns is exported only for test
func (p *MySQLPrivilege) MatchColumns(user, host, db, table, column string) *columnsPrivRecord {
if p.ColumnsPrivMap != nil {
if records, exists := p.ColumnsPrivMap[user]; exists {
for i := range records {
record := &records[i]
if record.match(user, host, db, table, column) {
return record
}
}
return nil
}
return nil
}

item, exists := p.columnsPriv.Get(itemColumnsPriv{username: user})
if exists {
for i := range item.data {
Expand Down Expand Up @@ -1579,6 +1618,7 @@ func (p *MySQLPrivilege) RequestDynamicVerification(activeRoles []*auth.RoleIden
}

// RequestVerification checks whether the user have sufficient privileges to do the operation.
// `column == "*"` means it matches ANY column in the table.
func (p *MySQLPrivilege) RequestVerification(activeRoles []*auth.RoleIdentity, user, host, db, table, column string, priv mysql.PrivilegeType) bool {
if priv == mysql.UsagePriv {
return true
Expand Down Expand Up @@ -1613,17 +1653,16 @@ func (p *MySQLPrivilege) RequestVerification(activeRoles []*auth.RoleIdentity, u
if tableRecord != nil {
tablePriv |= tableRecord.TablePriv
if column != "" {
columnPriv |= tableRecord.ColumnPriv
columnPriv |= tableRecord.TablePriv
}
}
}
if tablePriv&priv > 0 || columnPriv&priv > 0 {
if tablePriv&priv > 0 {
return true
}

columnPriv = 0
for _, r := range roleList {
columnRecord := p.matchColumns(r.Username, r.Hostname, db, table, column)
columnRecord := p.MatchColumns(r.Username, r.Hostname, db, table, column)
if columnRecord != nil {
columnPriv |= columnRecord.ColumnPriv
}
Expand Down
72 changes: 68 additions & 4 deletions pkg/privilege/privileges/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package privileges_test
import (
"context"
"fmt"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -169,6 +170,37 @@ func TestLoadColumnsPrivTable(t *testing.T) {
require.Equal(t, mysql.SelectPriv, columnsPriv[1].ColumnPriv)
}

func TestMatchColumns(t *testing.T) {
store := createStoreAndPrepareDB(t)

tk := testkit.NewTestKit(t, store)
tk.MustExec("use mysql;")
tk.MustExec("delete from columns_priv")
tk.MustExec(`INSERT INTO mysql.columns_priv VALUES ("%", "db", "user", "table", "c1", "2017-01-04 16:33:42.235831", "Insert,Update")`)
tk.MustExec(`INSERT INTO mysql.columns_priv VALUES ("%", "db", "user", "table", "c2", "2017-01-04 16:33:42.235831", "Select")`)

p := privileges.NewMySQLPrivilege()
se := tk.Session()
require.NoError(t, p.LoadColumnsPrivTable(se.GetSQLExecutor()))
col := p.MatchColumns("user", "%", "db", "table", "c1")
require.NotNil(t, col)
col = p.MatchColumns("user", "%", "db", "table", "*")
require.NotNil(t, col)

p = privileges.NewMySQLPrivilege()
tk.MustExec("delete from columns_priv")
tk.MustExec("flush privileges")
tk.MustExec(`INSERT INTO mysql.columns_priv VALUES ("%", "db", "user", "table", "c1", "2017-01-04 16:33:42.235831", "Insert,Update")`)
tk.MustExec(`INSERT INTO mysql.columns_priv VALUES ("%", "db", "user", "table", "c2", "2017-01-04 16:33:42.235831", "References")`)
require.NoError(t, p.LoadColumnsPrivTable(se.GetSQLExecutor()))
col = p.MatchColumns("user", "%", "db", "table", "c1")
require.NotNil(t, col)
col = p.MatchColumns("user", "%", "db", "table", "c2")
require.NotNil(t, col)
col = p.MatchColumns("user", "%", "db", "table", "*")
require.Nil(t, col)
}

func TestLoadDefaultRoleTable(t *testing.T) {
store := createStoreAndPrepareDB(t)

Expand Down Expand Up @@ -395,14 +427,32 @@ func TestFindAllUserEffectiveRoles(t *testing.T) {

func TestSortUserTable(t *testing.T) {
p := privileges.NewMySQLPrivilege()

p.SetUser([]privileges.UserRecord{
privileges.NewUserRecord(`%`, "root"),
privileges.NewUserRecord(`localhost`, "root"),
privileges.NewUserRecord("h1.example.net", "root"),
privileges.NewUserRecord("192.168.%", "root"),
privileges.NewUserRecord("192.168.199.%", "root"),
})
p.SortUserTable()
result := []privileges.UserRecord{
privileges.NewUserRecord("h1.example.net", "root"),
privileges.NewUserRecord(`localhost`, "root"),
privileges.NewUserRecord("192.168.199.%", "root"),
privileges.NewUserRecord("192.168.%", "root"),
privileges.NewUserRecord(`%`, "root"),
}
checkUserRecord(t, p.User(), result)

p.SetUser([]privileges.UserRecord{
privileges.NewUserRecord(`%`, "root"),
privileges.NewUserRecord(`%`, "jeffrey"),
privileges.NewUserRecord("localhost", "root"),
privileges.NewUserRecord("localhost", ""),
})
p.SortUserTable()
result := []privileges.UserRecord{
result = []privileges.UserRecord{
privileges.NewUserRecord("localhost", ""),
privileges.NewUserRecord("localhost", "root"),
privileges.NewUserRecord(`%`, "jeffrey"),
Expand Down Expand Up @@ -453,10 +503,24 @@ func TestGlobalPrivValueRequireStr(t *testing.T) {
}

func checkUserRecord(t *testing.T, x, y []privileges.UserRecord) {
require.Equal(t, len(x), len(y))
var sbX, sbY strings.Builder
for _, u := range x {
sbX.WriteString(u.User)
sbX.WriteString("@")
sbX.WriteString(u.Host)
sbX.WriteString("\n")
}
for _, u := range y {
sbY.WriteString(u.User)
sbY.WriteString("@")
sbY.WriteString(u.Host)
sbY.WriteString("\n")
}

require.Equal(t, len(x), len(y), "%s\n vs %s\n", sbX.String(), sbY.String())
for i := range x {
require.Equal(t, x[i].User, y[i].User)
require.Equal(t, x[i].Host, y[i].Host)
require.Equal(t, x[i].User, y[i].User, "%s\n vs %s\n", sbX.String(), sbY.String())
require.Equal(t, x[i].Host, y[i].Host, "%s\n vs %s\n", sbX.String(), sbY.String())
}
}

Expand Down
Loading