-
Notifications
You must be signed in to change notification settings - Fork 109
auth: add authentication and authorization interface #496
Changes from 2 commits
3b3b8b0
9361fcd
e678357
de8c11a
20d732a
f4aabc3
277877e
e04a75b
9271159
01ffab1
c874286
8cb84a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package auth | ||
|
||
import ( | ||
"strings" | ||
|
||
"gopkg.in/src-d/go-vitess.v1/mysql" | ||
) | ||
|
||
// Permission holds permissions required by a query or grated to a user. | ||
type Permission int | ||
|
||
const ( | ||
// ReadPerm means that it reads. | ||
ReadPerm Permission = 1 << iota | ||
// WritePerm means that it writes. | ||
WritePerm | ||
) | ||
|
||
var ( | ||
// AllPermissions hold all defined permissions. | ||
AllPermissions = ReadPerm | WritePerm | ||
// DefaultPermissions are the permissions granted to a user if not defined. | ||
DefaultPermissions = ReadPerm | ||
|
||
// PermissionNames is used to translate from human and machine | ||
// representations. | ||
PermissionNames = map[string]Permission{ | ||
"read": ReadPerm, | ||
"write": WritePerm, | ||
} | ||
|
||
// ErrNoPermission is returned when the user lacks needed permissions. | ||
ErrNoPermission = "user does not have permission: %s" | ||
) | ||
|
||
// String returns all the permissions set to on. | ||
func (p Permission) String() string { | ||
var str []string | ||
for k, v := range PermissionNames { | ||
if p&v != 0 { | ||
str = append(str, k) | ||
} | ||
} | ||
|
||
return strings.Join(str, ", ") | ||
} | ||
|
||
// Auth interface provides mysql authentication methods and permission checking | ||
// for users. | ||
type Auth interface { | ||
Mysql() mysql.AuthServer | ||
Allowed(user string, permission Permission) error | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please, add godoc to the interface. Specially a mention to errors returned fo There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
package auth | ||
|
||
import ( | ||
"crypto/sha1" | ||
"encoding/hex" | ||
"encoding/json" | ||
"fmt" | ||
"io/ioutil" | ||
"regexp" | ||
"strings" | ||
|
||
"gopkg.in/src-d/go-vitess.v1/mysql" | ||
) | ||
|
||
var regNative = regexp.MustCompile(`^\*[0-9A-F]{40}$`) | ||
|
||
// ErrUnknownPermission happens when a user permission is not defined. | ||
const ErrUnknownPermission = "error parsing user file, unknown permission %s" | ||
|
||
// nativeUser holds information about credentials and permissions for user. | ||
type nativeUser struct { | ||
Name string | ||
Password string | ||
JSONPermissions []string `json:"Permissions"` | ||
Permissions Permission | ||
} | ||
|
||
// Allowed checks if the user has certain permission. | ||
func (u nativeUser) Allowed(p Permission) error { | ||
if u.Permissions&p == p { | ||
return nil | ||
} | ||
|
||
// permissions needed but not granted to the user | ||
p2 := (^u.Permissions) & p | ||
|
||
return fmt.Errorf(ErrNoPermission, p2) | ||
} | ||
|
||
// NativePassword generates a mysql_native_password string. | ||
func NativePassword(password string) string { | ||
if len(password) == 0 { | ||
return "" | ||
} | ||
|
||
// native = sha1(sha1(password)) | ||
|
||
hash := sha1.New() | ||
hash.Write([]byte(password)) | ||
s1 := hash.Sum(nil) | ||
|
||
hash.Reset() | ||
hash.Write(s1) | ||
s2 := hash.Sum(nil) | ||
|
||
s := strings.ToUpper(hex.EncodeToString(s2)) | ||
|
||
return fmt.Sprintf("*%s", s) | ||
} | ||
|
||
// Native holds mysql_native_password users. | ||
type Native struct { | ||
users map[string]nativeUser | ||
} | ||
|
||
// NewNativeSingle creates a NativeAuth with a single user. | ||
func NewNativeSingle(name, password string) *Native { | ||
users := make(map[string]nativeUser) | ||
users[name] = nativeUser{ | ||
Name: name, | ||
Password: NativePassword(password), | ||
Permissions: AllPermissions, | ||
} | ||
|
||
return &Native{users} | ||
} | ||
|
||
// NewNativeFile creates a NativeAuth and loads users from a JSON file. | ||
func NewNativeFile(file string) (*Native, error) { | ||
var data []nativeUser | ||
|
||
raw, err := ioutil.ReadFile(file) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if err := json.Unmarshal(raw, &data); err != nil { | ||
return nil, err | ||
} | ||
|
||
users := make(map[string]nativeUser) | ||
for _, u := range data { | ||
_, ok := users[u.Name] | ||
if ok { | ||
return nil, fmt.Errorf("duplicate user: %s", u.Name) | ||
} | ||
|
||
if !regNative.MatchString(u.Password) { | ||
u.Password = NativePassword(u.Password) | ||
} | ||
|
||
if len(u.JSONPermissions) == 0 { | ||
u.Permissions = DefaultPermissions | ||
} | ||
|
||
for _, p := range u.JSONPermissions { | ||
perm, ok := PermissionNames[strings.ToLower(p)] | ||
if !ok { | ||
return nil, fmt.Errorf(ErrUnknownPermission, p) | ||
} | ||
|
||
u.Permissions |= perm | ||
} | ||
|
||
users[u.Name] = u | ||
} | ||
|
||
return &Native{users}, nil | ||
} | ||
|
||
// Mysql implements Auth interface. | ||
func (s *Native) Mysql() mysql.AuthServer { | ||
auth := mysql.NewAuthServerStatic() | ||
|
||
for k, v := range s.users { | ||
auth.Entries[k] = []*mysql.AuthServerStaticEntry{ | ||
{MysqlNativePassword: v.Password}, | ||
} | ||
} | ||
|
||
return auth | ||
} | ||
|
||
// Allowed implements Auth interface. | ||
func (s *Native) Allowed(name string, permission Permission) error { | ||
u, ok := s.users[name] | ||
if !ok { | ||
return fmt.Errorf(ErrNoPermission, permission) | ||
} | ||
|
||
return u.Allowed(permission) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package auth | ||
|
||
import "gopkg.in/src-d/go-vitess.v1/mysql" | ||
|
||
// None is a Auth method that always succeeds. | ||
type None struct{} | ||
|
||
// Mysql implements Auth interface. | ||
func (n *None) Mysql() mysql.AuthServer { | ||
return new(mysql.AuthServerNone) | ||
} | ||
|
||
// Mysql implements Auth interface. | ||
func (n *None) Allowed(user string, permission Permission) error { | ||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -55,18 +55,29 @@ func (s *SessionManager) nextPid() uint64 { | |
return s.pid | ||
} | ||
|
||
// NewSession creates a Session for the given connection. | ||
// newSession creates a Session for the given connection. | ||
func (s *SessionManager) newSession(conn *mysql.Conn) sql.Session { | ||
return s.builder(conn, s.addr) | ||
} | ||
|
||
// NewSession creates a Session for the given connection and saves it to | ||
// session pool. | ||
func (s *SessionManager) NewSession(conn *mysql.Conn) { | ||
s.mu.Lock() | ||
s.sessions[conn.ConnectionID] = s.builder(conn, s.addr) | ||
s.sessions[conn.ConnectionID] = s.newSession(conn) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i don't understand this change. Why do we need |
||
s.mu.Unlock() | ||
} | ||
|
||
// NewContext creates a new context for the session at the given conn. | ||
func (s *SessionManager) NewContext(conn *mysql.Conn) *sql.Context { | ||
s.mu.Lock() | ||
sess := s.sessions[conn.ConnectionID] | ||
sess, ok := s.sessions[conn.ConnectionID] | ||
if !ok { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This case can't happen. If it does not exist in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have not mentioned that I also had to change when sessions are created. Before the session was created in |
||
sess = s.newSession(conn) | ||
s.sessions[conn.ConnectionID] = sess | ||
} | ||
s.mu.Unlock() | ||
|
||
context := sql.NewContext( | ||
context.Background(), | ||
sql.WithSession(sess), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe return
(bool, error)
?This would allow to differentiate from not having permissions and failing to check permissions. The former would probably produce a warning audit log (when we have audit logs), the second would probably also produce a regular error log.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(bool, error)
sounds to me like a fuzzy logic:(true, nil)
(false, nil)
(false, err)
and hopefully it's not possible to have:
(true, err)
It's like returning
*bool
it can give you (nil, *true, *false)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kuba-- It's a common pattern in Go, also in our own codebase.
(true, err)
is usually not relevant since the value would not be even checked iferr != nil
.But alternatively, you can keep just
err
as return value and use a special error kindErrNotAuthorized
to differentiate from other errors.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@smola - totally understand, just as I mentioned, personally I don't like this pattern, because have a feeling that it's a boolean logic with extra dimension.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm moving to use
go-errors
and returningErrNotAuthorized
so is easier to tell apart.