-
Notifications
You must be signed in to change notification settings - Fork 110
auth: add authentication and authorization interface #496
Changes from 1 commit
3b3b8b0
9361fcd
e678357
de8c11a
20d732a
f4aabc3
277877e
e04a75b
9271159
01ffab1
c874286
8cb84a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
The interface has two methods: * Mysql: returns a vitess server auth * Allow: checks if a user has permissions needed Currently two auth methods are implemented: * None: allows all logins and permissions * Native: uses mysql_native_password method to authenticate. Native method can read users from a json file. This contains the user credentials and permissions for the user. The credentials can be in plain or in mysql native format. It can also contain a list of all permissions granted to the user. If not specified it uses default permissions that for now is "read". [ { "name": "root", "password": "*9E128DA0C64A6FCCCDCFBDD0FC0A2C967C6DB36F", "permissions": ["read", "write"] }, { "name": "javi", "password": "Passw0rd!" } ] To enforce permissions a new validation rule can be added with the builder: ab := analyzer.NewBuilder(catalog).WithAuth(userAuth) Signed-off-by: Javi Fontan <jfontan@gmail.com>
- Loading branch information
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 |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package analyzer | ||
|
||
import ( | ||
"gopkg.in/src-d/go-mysql-server.v0/auth" | ||
"gopkg.in/src-d/go-mysql-server.v0/sql" | ||
"gopkg.in/src-d/go-mysql-server.v0/sql/plan" | ||
) | ||
|
||
// CheckAuthorizationRule is a rule that validates that a user can execute | ||
// the query. | ||
const CheckAuthorizationRule = "check_authorization" | ||
|
||
// CheckAuthorization creates an authorization check with the given Auth. | ||
func CheckAuthorization(au auth.Auth) RuleFunc { | ||
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. Could you delete the old rule that we used to make possible read-only queries? 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'll add the possibility to specify the permissions when using 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 |
||
return func(ctx *sql.Context, a *Analyzer, node sql.Node) (sql.Node, error) { | ||
var perm auth.Permission | ||
switch node.(type) { | ||
case *plan.InsertInto, *plan.DropIndex, *plan.CreateIndex, *plan.UnlockTables, *plan.LockTables: | ||
perm = auth.ReadPerm | auth.WritePerm | ||
default: | ||
perm = auth.ReadPerm | ||
} | ||
|
||
err := au.Allowed(ctx.User(), perm) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return node, nil | ||
} | ||
} |
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.