Skip to content

Commit

Permalink
*: support certificate based authentication (#13955)
Browse files Browse the repository at this point in the history
  • Loading branch information
lysu authored Dec 23, 2019
1 parent f0282d3 commit cd07c45
Show file tree
Hide file tree
Showing 18 changed files with 1,016 additions and 66 deletions.
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 {
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

0 comments on commit cd07c45

Please sign in to comment.