Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

*: support certificate based authentication #13955

Merged
merged 5 commits into from
Dec 23, 2019
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
1 change: 1 addition & 0 deletions executor/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,7 @@ func (b *executorBuilder) buildGrant(grant *ast.GrantStmt) Executor {
Level: grant.Level,
Users: grant.Users,
WithGrant: grant.WithGrant,
TLSOptions: grant.TLSOptions,
is: b.is,
}
return e
Expand Down
165 changes: 146 additions & 19 deletions executor/grant.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package executor

import (
"context"
"encoding/json"
"fmt"
"strings"

Expand All @@ -24,8 +25,10 @@ import (
"github.com/pingcap/parser/mysql"
"github.com/pingcap/tidb/domain"
"github.com/pingcap/tidb/infoschema"
"github.com/pingcap/tidb/privilege/privileges"
"github.com/pingcap/tidb/sessionctx"
"github.com/pingcap/tidb/table"
"github.com/pingcap/tidb/util"
"github.com/pingcap/tidb/util/chunk"
"github.com/pingcap/tidb/util/sqlexec"
)
Expand All @@ -46,6 +49,7 @@ type GrantExec struct {
ObjectType ast.ObjectTypeType
Level *ast.GrantLevel
Users []*ast.UserSpec
TLSOptions []*ast.TLSOption

is infoschema.InfoSchema
WithGrant bool
Expand Down Expand Up @@ -86,9 +90,16 @@ func (e *GrantExec) Next(ctx context.Context, req *chunk.Chunk) error {
}

// If there is no privilege entry in corresponding table, insert a new one.
// DB scope: mysql.DB
// Table scope: mysql.Tables_priv
// Column scope: mysql.Columns_priv
// Global scope: mysql.global_priv
// DB scope: mysql.DB
// Table scope: mysql.Tables_priv
// Column scope: mysql.Columns_priv
if e.TLSOptions != nil {
err = checkAndInitGlobalPriv(e.ctx, user.User.Username, user.User.Hostname)
if err != nil {
return err
}
}
switch e.Level.Level {
case ast.GrantLevelDB:
err := checkAndInitDBPriv(e.ctx, dbName, e.is, user.User.Username, user.User.Hostname)
Expand All @@ -113,7 +124,11 @@ func (e *GrantExec) Next(ctx context.Context, req *chunk.Chunk) error {
}
defer func() { e.ctx.GetSessionVars().SetStatusFlag(mysql.ServerStatusInTrans, false) }()
}

// Grant global priv to user.
err = e.grantGlobalPriv(user)
if err != nil {
return err
}
// Grant each priv to the user.
for _, priv := range privs {
if len(priv.Cols) > 0 {
Expand All @@ -124,7 +139,7 @@ func (e *GrantExec) Next(ctx context.Context, req *chunk.Chunk) error {
return err
}
}
err := e.grantPriv(priv, user)
err := e.grantLevelPriv(priv, user)
if err != nil {
return err
}
Expand All @@ -134,6 +149,20 @@ func (e *GrantExec) Next(ctx context.Context, req *chunk.Chunk) error {
return nil
}

// checkAndInitGlobalPriv checks if global scope privilege entry exists in mysql.global_priv.
// If not exists, insert a new one.
func checkAndInitGlobalPriv(ctx sessionctx.Context, user string, host string) error {
ok, err := globalPrivEntryExists(ctx, user, host)
if err != nil {
return err
}
if ok {
return nil
}
// Entry does not exist for user-host-db. Insert a new entry.
return initGlobalPrivEntry(ctx, user, host)
}

// checkAndInitDBPriv checks if DB scope privilege entry exists in mysql.DB.
// If unexists, insert a new one.
func checkAndInitDBPriv(ctx sessionctx.Context, dbName string, is infoschema.InfoSchema, user string, host string) error {
Expand Down Expand Up @@ -190,6 +219,13 @@ func (e *GrantExec) checkAndInitColumnPriv(user string, host string, cols []*ast
return nil
}

// initGlobalPrivEntry inserts a new row into mysql.DB with empty privilege.
func initGlobalPrivEntry(ctx sessionctx.Context, user string, host string) error {
sql := fmt.Sprintf(`INSERT INTO %s.%s (Host, User, PRIV) VALUES ('%s', '%s', '%s')`, mysql.SystemDB, mysql.GlobalPrivTable, host, user, "{}")
_, _, err := ctx.(sqlexec.RestrictedSQLExecutor).ExecRestrictedSQL(sql)
return err
}

// initDBPrivEntry inserts a new row into mysql.DB with empty privilege.
func initDBPrivEntry(ctx sessionctx.Context, user string, host string, db string) error {
sql := fmt.Sprintf(`INSERT INTO %s.%s (Host, User, DB) VALUES ('%s', '%s', '%s')`, mysql.SystemDB, mysql.DBTable, host, user, db)
Expand All @@ -211,25 +247,110 @@ func initColumnPrivEntry(ctx sessionctx.Context, user string, host string, db st
return err
}

// grantPriv grants priv to user in s.Level scope.
func (e *GrantExec) grantPriv(priv *ast.PrivElem, user *ast.UserSpec) error {
// grantGlobalPriv grants priv to user in global scope.
func (e *GrantExec) grantGlobalPriv(user *ast.UserSpec) error {
if len(e.TLSOptions) == 0 {
return nil
}
priv, err := tlsOption2GlobalPriv(e.TLSOptions)
if err != nil {
return errors.Trace(err)
}
sql := fmt.Sprintf(`UPDATE %s.%s SET PRIV = '%s' WHERE User='%s' AND Host='%s'`, mysql.SystemDB, mysql.GlobalPrivTable, priv, user.User.Username, user.User.Hostname)
_, _, err = e.ctx.(sqlexec.RestrictedSQLExecutor).ExecRestrictedSQL(sql)
return err
}

var emptyGP = privileges.GlobalPrivValue{SSLType: privileges.SslTypeNotSpecified}

func tlsOption2GlobalPriv(tlsOptions []*ast.TLSOption) (priv []byte, err error) {
if len(tlsOptions) == 0 {
priv = []byte("{}")
return
}
dupSet := make(map[int]struct{})
for _, opt := range tlsOptions {
if _, dup := dupSet[opt.Type]; dup {
var typeName string
switch opt.Type {
case ast.Cipher:
typeName = "CIPHER"
case ast.Issuer:
typeName = "ISSUER"
case ast.Subject:
typeName = "SUBJECT"
}
err = errors.Errorf("Duplicate require %s clause", typeName)
return
}
dupSet[opt.Type] = struct{}{}
}
gp := privileges.GlobalPrivValue{SSLType: privileges.SslTypeNotSpecified}
for _, tlsOpt := range tlsOptions {
lysu marked this conversation as resolved.
Show resolved Hide resolved
switch tlsOpt.Type {
case ast.TslNone:
gp.SSLType = privileges.SslTypeNone
case ast.Ssl:
gp.SSLType = privileges.SslTypeAny
case ast.X509:
gp.SSLType = privileges.SslTypeX509
case ast.Cipher:
gp.SSLType = privileges.SslTypeSpecified
if len(tlsOpt.Value) > 0 {
if _, ok := util.SupportCipher[tlsOpt.Value]; !ok {
err = errors.Errorf("Unsupported cipher suit: %s", tlsOpt.Value)
return
}
gp.SSLCipher = tlsOpt.Value
}
case ast.Issuer:
err = util.CheckSupportX509NameOneline(tlsOpt.Value)
if err != nil {
return
}
gp.SSLType = privileges.SslTypeSpecified
gp.X509Issuer = tlsOpt.Value
case ast.Subject:
err = util.CheckSupportX509NameOneline(tlsOpt.Value)
if err != nil {
return
}
gp.SSLType = privileges.SslTypeSpecified
gp.X509Subject = tlsOpt.Value
default:
err = errors.Errorf("Unknown ssl type: %#v", tlsOpt.Type)
return
}
}
if gp == emptyGP {
return
}
priv, err = json.Marshal(&gp)
if err != nil {
return
}
return
}

// grantLevelPriv grants priv to user in s.Level scope.
func (e *GrantExec) grantLevelPriv(priv *ast.PrivElem, user *ast.UserSpec) error {
switch e.Level.Level {
case ast.GrantLevelGlobal:
return e.grantGlobalPriv(priv, user)
return e.grantGlobalLevel(priv, user)
case ast.GrantLevelDB:
return e.grantDBPriv(priv, user)
return e.grantDBLevel(priv, user)
case ast.GrantLevelTable:
if len(priv.Cols) == 0 {
return e.grantTablePriv(priv, user)
return e.grantTableLevel(priv, user)
}
return e.grantColumnPriv(priv, user)
return e.grantColumnLevel(priv, user)
default:
return errors.Errorf("Unknown grant level: %#v", e.Level)
}
}

// grantGlobalPriv manipulates mysql.user table.
func (e *GrantExec) grantGlobalPriv(priv *ast.PrivElem, user *ast.UserSpec) error {
// grantGlobalLevel manipulates mysql.user table.
func (e *GrantExec) grantGlobalLevel(priv *ast.PrivElem, user *ast.UserSpec) error {
if priv.Priv == 0 {
return nil
}
Expand All @@ -242,8 +363,8 @@ func (e *GrantExec) grantGlobalPriv(priv *ast.PrivElem, user *ast.UserSpec) erro
return err
}

// grantDBPriv manipulates mysql.db table.
func (e *GrantExec) grantDBPriv(priv *ast.PrivElem, user *ast.UserSpec) error {
// grantDBLevel manipulates mysql.db table.
func (e *GrantExec) grantDBLevel(priv *ast.PrivElem, user *ast.UserSpec) error {
dbName := e.Level.DBName
if len(dbName) == 0 {
dbName = e.ctx.GetSessionVars().CurrentDB
Expand All @@ -257,8 +378,8 @@ func (e *GrantExec) grantDBPriv(priv *ast.PrivElem, user *ast.UserSpec) error {
return err
}

// grantTablePriv manipulates mysql.tables_priv table.
func (e *GrantExec) grantTablePriv(priv *ast.PrivElem, user *ast.UserSpec) error {
// grantTableLevel manipulates mysql.tables_priv table.
func (e *GrantExec) grantTableLevel(priv *ast.PrivElem, user *ast.UserSpec) error {
dbName := e.Level.DBName
if len(dbName) == 0 {
dbName = e.ctx.GetSessionVars().CurrentDB
Expand All @@ -273,8 +394,8 @@ func (e *GrantExec) grantTablePriv(priv *ast.PrivElem, user *ast.UserSpec) error
return err
}

// grantColumnPriv manipulates mysql.tables_priv table.
func (e *GrantExec) grantColumnPriv(priv *ast.PrivElem, user *ast.UserSpec) error {
// grantColumnLevel manipulates mysql.tables_priv table.
func (e *GrantExec) grantColumnLevel(priv *ast.PrivElem, user *ast.UserSpec) error {
dbName, tbl, err := getTargetSchemaAndTable(e.ctx, e.Level.DBName, e.Level.TableName, e.is)
if err != nil {
return err
Expand Down Expand Up @@ -473,6 +594,12 @@ func recordExists(ctx sessionctx.Context, sql string) (bool, error) {
return len(rows) > 0, nil
}

// globalPrivEntryExists checks if there is an entry with key user-host in mysql.global_priv.
func globalPrivEntryExists(ctx sessionctx.Context, name string, host string) (bool, error) {
sql := fmt.Sprintf(`SELECT * FROM %s.%s WHERE User='%s' AND Host='%s';`, mysql.SystemDB, mysql.GlobalPrivTable, name, host)
return recordExists(ctx, sql)
}

// dbUserExists checks if there is an entry with key user-host-db in mysql.DB.
func dbUserExists(ctx sessionctx.Context, name string, host string, db string) (bool, error) {
sql := fmt.Sprintf(`SELECT * FROM %s.%s WHERE User='%s' AND Host='%s' AND DB='%s';`, mysql.SystemDB, mysql.DBTable, name, host, db)
Expand Down
70 changes: 70 additions & 0 deletions executor/grant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,73 @@ func (s *testSuite3) TestGrantUnderANSIQuotes(c *C) {
tk.MustExec(`REVOKE ALL PRIVILEGES ON video_ulimit.* FROM web@'%';`)
tk.MustExec(`DROP USER IF EXISTS 'web'@'%'`)
}

func (s *testSuite3) TestMaintainRequire(c *C) {
tk := testkit.NewTestKit(c, s.store)

// test create with require
tk.MustExec(`CREATE USER 'ssl_auser'@'%' require issuer '/CN=TiDB admin/OU=TiDB/O=PingCAP/L=San Francisco/ST=California/C=US' subject '/CN=tester1/OU=TiDB/O=PingCAP.Inc/L=Haidian/ST=Beijing/C=ZH' cipher 'AES128-GCM-SHA256'`)
tk.MustExec(`CREATE USER 'ssl_buser'@'%' require subject '/CN=tester1/OU=TiDB/O=PingCAP.Inc/L=Haidian/ST=Beijing/C=ZH' cipher 'AES128-GCM-SHA256'`)
tk.MustExec(`CREATE USER 'ssl_cuser'@'%' require cipher 'AES128-GCM-SHA256'`)
tk.MustExec(`CREATE USER 'ssl_duser'@'%'`)
tk.MustExec(`CREATE USER 'ssl_euser'@'%' require none`)
tk.MustExec(`CREATE USER 'ssl_fuser'@'%' require ssl`)
tk.MustExec(`CREATE USER 'ssl_guser'@'%' require x509`)
tk.MustQuery("select * from mysql.global_priv where `user` like 'ssl_%'").Check(testkit.Rows(
"% ssl_auser {\"ssl_type\":3,\"ssl_cipher\":\"AES128-GCM-SHA256\",\"x509_issuer\":\"/CN=TiDB admin/OU=TiDB/O=PingCAP/L=San Francisco/ST=California/C=US\",\"x509_subject\":\"/CN=tester1/OU=TiDB/O=PingCAP.Inc/L=Haidian/ST=Beijing/C=ZH\"}",
"% ssl_buser {\"ssl_type\":3,\"ssl_cipher\":\"AES128-GCM-SHA256\",\"x509_subject\":\"/CN=tester1/OU=TiDB/O=PingCAP.Inc/L=Haidian/ST=Beijing/C=ZH\"}",
"% ssl_cuser {\"ssl_type\":3,\"ssl_cipher\":\"AES128-GCM-SHA256\"}",
"% ssl_duser {}",
"% ssl_euser {}",
"% ssl_fuser {\"ssl_type\":1}",
"% ssl_guser {\"ssl_type\":2}",
))

// test grant with require
tk.MustExec("CREATE USER 'u1'@'%'")
tk.MustExec("GRANT ALL ON *.* TO 'u1'@'%' require issuer '/CN=TiDB admin/OU=TiDB/O=PingCAP/L=San Francisco/ST=California/C=US' and subject '/CN=tester1/OU=TiDB/O=PingCAP.Inc/L=Haidian/ST=Beijing/C=ZH'") // add new require.
tk.MustQuery("select priv from mysql.global_priv where `Host` = '%' and `User` = 'u1'").Check(testkit.Rows("{\"ssl_type\":3,\"x509_issuer\":\"/CN=TiDB admin/OU=TiDB/O=PingCAP/L=San Francisco/ST=California/C=US\",\"x509_subject\":\"/CN=tester1/OU=TiDB/O=PingCAP.Inc/L=Haidian/ST=Beijing/C=ZH\"}"))
tk.MustExec("GRANT ALL ON *.* TO 'u1'@'%' require cipher 'AES128-GCM-SHA256'") // modify always overwrite.
tk.MustQuery("select priv from mysql.global_priv where `Host` = '%' and `User` = 'u1'").Check(testkit.Rows("{\"ssl_type\":3,\"ssl_cipher\":\"AES128-GCM-SHA256\"}"))
tk.MustExec("GRANT select ON *.* TO 'u1'@'%'") // modify without require should not modify old require.
tk.MustQuery("select priv from mysql.global_priv where `Host` = '%' and `User` = 'u1'").Check(testkit.Rows("{\"ssl_type\":3,\"ssl_cipher\":\"AES128-GCM-SHA256\"}"))
tk.MustExec("GRANT ALL ON *.* TO 'u1'@'%' require none") // use require none to clean up require.
tk.MustQuery("select priv from mysql.global_priv where `Host` = '%' and `User` = 'u1'").Check(testkit.Rows("{}"))

// test alter with require
tk.MustExec("CREATE USER 'u2'@'%'")
tk.MustExec("alter user 'u2'@'%' require ssl")
tk.MustQuery("select priv from mysql.global_priv where `Host` = '%' and `User` = 'u2'").Check(testkit.Rows("{\"ssl_type\":1}"))
tk.MustExec("alter user 'u2'@'%' require x509")
tk.MustQuery("select priv from mysql.global_priv where `Host` = '%' and `User` = 'u2'").Check(testkit.Rows("{\"ssl_type\":2}"))
tk.MustExec("alter user 'u2'@'%' require issuer '/CN=TiDB admin/OU=TiDB/O=PingCAP/L=San Francisco/ST=California/C=US' subject '/CN=tester1/OU=TiDB/O=PingCAP.Inc/L=Haidian/ST=Beijing/C=ZH' cipher 'AES128-GCM-SHA256'")
tk.MustQuery("select priv from mysql.global_priv where `Host` = '%' and `User` = 'u2'").Check(testkit.Rows("{\"ssl_type\":3,\"ssl_cipher\":\"AES128-GCM-SHA256\",\"x509_issuer\":\"/CN=TiDB admin/OU=TiDB/O=PingCAP/L=San Francisco/ST=California/C=US\",\"x509_subject\":\"/CN=tester1/OU=TiDB/O=PingCAP.Inc/L=Haidian/ST=Beijing/C=ZH\"}"))
tk.MustExec("alter user 'u2'@'%' require none")
tk.MustQuery("select priv from mysql.global_priv where `Host` = '%' and `User` = 'u2'").Check(testkit.Rows("{}"))

// test show create user
tk.MustExec(`CREATE USER 'u3'@'%' require issuer '/CN=TiDB admin/OU=TiDB/O=PingCAP/L=San Francisco/ST=California/C=US' subject '/CN=tester1/OU=TiDB/O=PingCAP.Inc/L=Haidian/ST=Beijing/C=ZH' cipher 'AES128-GCM-SHA256'`)
tk.MustQuery("show create user 'u3'").Check(testkit.Rows("CREATE USER 'u3'@'%' IDENTIFIED WITH 'mysql_native_password' AS '' REQUIRE CIPHER 'AES128-GCM-SHA256' ISSUER '/CN=TiDB admin/OU=TiDB/O=PingCAP/L=San Francisco/ST=California/C=US' SUBJECT '/CN=tester1/OU=TiDB/O=PingCAP.Inc/L=Haidian/ST=Beijing/C=ZH' PASSWORD EXPIRE DEFAULT ACCOUNT UNLOCK"))

// check issuer/subject/cipher value
_, err := tk.Exec(`CREATE USER 'u4'@'%' require issuer 'CN=TiDB,OU=PingCAP'`)
c.Assert(err, NotNil)
_, err = tk.Exec(`CREATE USER 'u5'@'%' require subject '/CN=TiDB\OU=PingCAP'`)
c.Assert(err, NotNil)
_, err = tk.Exec(`CREATE USER 'u6'@'%' require subject '/CN=TiDB\NC=PingCAP'`)
c.Assert(err, NotNil)
_, err = tk.Exec(`CREATE USER 'u7'@'%' require cipher 'AES128-GCM-SHA1'`)
c.Assert(err, NotNil)
_, err = tk.Exec(`CREATE USER 'u8'@'%' require subject '/CN'`)
c.Assert(err, NotNil)
_, err = tk.Exec(`CREATE USER 'u9'@'%' require cipher 'TLS_AES_256_GCM_SHA384' cipher 'RC4-SHA'`)
c.Assert(err.Error(), Equals, "Duplicate require CIPHER clause")
_, err = tk.Exec(`CREATE USER 'u9'@'%' require issuer 'CN=TiDB,OU=PingCAP' issuer 'CN=TiDB,OU=PingCAP2'`)
c.Assert(err.Error(), Equals, "Duplicate require ISSUER clause")
_, err = tk.Exec(`CREATE USER 'u9'@'%' require subject '/CN=TiDB\OU=PingCAP' subject '/CN=TiDB\OU=PingCAP2'`)
c.Assert(err.Error(), Equals, "Duplicate require SUBJECT clause")
_, err = tk.Exec(`CREATE USER 'u9'@'%' require ssl ssl`)
c.Assert(err, NotNil)
_, err = tk.Exec(`CREATE USER 'u9'@'%' require x509 x509`)
c.Assert(err, NotNil)
}
Loading