Skip to content
This repository was archived by the owner on Jan 28, 2021. It is now read-only.

auth: add authentication and authorization interface #496

Merged
merged 12 commits into from
Oct 25, 2018
Prev Previous commit
Next Next commit
auth: add authentication and authorization interface
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
jfontan committed Oct 23, 2018
commit 9361fcdf7fef1204c806cf6ff08d93d404f766a2
53 changes: 53 additions & 0 deletions auth/auth.go
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
Copy link
Collaborator

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.

Copy link
Contributor

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)

Copy link
Collaborator

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 if err != nil.

But alternatively, you can keep just err as return value and use a special error kind ErrNotAuthorized to differentiate from other errors.

Copy link
Contributor

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.

Copy link
Contributor Author

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 returning ErrNotAuthorized so is easier to tell apart.

Copy link
Collaborator

@smola smola Oct 25, 2018

Choose a reason for hiding this comment

The 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 Allowed. My guess is that the one that is not expected to be implementation-depdendent is ErrNoAuthorized.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

}
142 changes: 142 additions & 0 deletions auth/native.go
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)
}
16 changes: 16 additions & 0 deletions auth/none.go
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
}
14 changes: 13 additions & 1 deletion engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sqle // import "gopkg.in/src-d/go-mysql-server.v0"
import (
opentracing "github.com/opentracing/opentracing-go"
"github.com/sirupsen/logrus"
"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/analyzer"
"gopkg.in/src-d/go-mysql-server.v0/sql/expression/function"
Expand All @@ -14,12 +15,15 @@ import (
type Config struct {
// VersionPostfix to display with the `VERSION()` UDF.
VersionPostfix string
// Auth used for authentication and authorization.
Auth auth.Auth
}

// Engine is a SQL engine.
type Engine struct {
Catalog *sql.Catalog
Analyzer *analyzer.Analyzer
Auth auth.Auth
}

// New creates a new Engine with custom configuration. To create an Engine with
Expand All @@ -33,7 +37,15 @@ func New(c *sql.Catalog, a *analyzer.Analyzer, cfg *Config) *Engine {
c.RegisterFunctions(function.Defaults)
c.RegisterFunction("version", sql.FunctionN(function.NewVersion(versionPostfix)))

return &Engine{c, a}
// use auth.None if auth is not specified
var au auth.Auth
if cfg == nil || cfg.Auth == nil {
au = new(auth.None)
} else {
au = cfg.Auth
}

return &Engine{c, a, au}
}

// NewDefault creates a new default Engine.
Expand Down
6 changes: 4 additions & 2 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

opentracing "github.com/opentracing/opentracing-go"
"gopkg.in/src-d/go-mysql-server.v0"
"gopkg.in/src-d/go-mysql-server.v0/auth"

"gopkg.in/src-d/go-vitess.v1/mysql"
)
Expand All @@ -21,7 +22,7 @@ type Config struct {
// Address of the server.
Address string
// Auth of the server.
Auth mysql.AuthServer
Auth auth.Auth
// Tracer to use in the server. By default, a noop tracer will be used if
// no tracer is provided.
Tracer opentracing.Tracer
Expand Down Expand Up @@ -54,7 +55,8 @@ func NewServer(cfg Config, e *sqle.Engine, sb SessionBuilder) (*Server, error) {
}

handler := NewHandler(e, NewSessionManager(sb, tracer, cfg.Address))
l, err := mysql.NewListener(cfg.Protocol, cfg.Address, cfg.Auth, handler, cfg.ConnReadTimeout, cfg.ConnWriteTimeout)
a := cfg.Auth.Mysql()
l, err := mysql.NewListener(cfg.Protocol, cfg.Address, a, handler, cfg.ConnReadTimeout, cfg.ConnWriteTimeout)
if err != nil {
return nil, err
}
Expand Down
6 changes: 6 additions & 0 deletions sql/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
opentracing "github.com/opentracing/opentracing-go"
"github.com/sirupsen/logrus"
"gopkg.in/src-d/go-errors.v1"
"gopkg.in/src-d/go-mysql-server.v0/auth"
"gopkg.in/src-d/go-mysql-server.v0/sql"
)

Expand Down Expand Up @@ -46,6 +47,11 @@ func (ab *Builder) WithParallelism(parallelism int) *Builder {
return ab
}

// WithAuth adds add authorization rule.
func (ab *Builder) WithAuth(a auth.Auth) *Builder {
return ab.AddPostValidationRule(CheckAuthorizationRule, CheckAuthorization(a))
}

// ReadOnly adds a rule that only allows read queries.
func (ab *Builder) ReadOnly() *Builder {
return ab.AddPreAnalyzeRule(EnsureReadOnlyRule, EnsureReadOnly)
Expand Down
31 changes: 31 additions & 0 deletions sql/analyzer/check_auth.go
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll add the possibility to specify the permissions when using NewNativeSingle so it can be used to disable writing instead of read only rule.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
}
}
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy