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
Merged
9 changes: 2 additions & 7 deletions _example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import (
"time"

"gopkg.in/src-d/go-mysql-server.v0"
"gopkg.in/src-d/go-mysql-server.v0/auth"
"gopkg.in/src-d/go-mysql-server.v0/mem"
"gopkg.in/src-d/go-mysql-server.v0/server"
"gopkg.in/src-d/go-mysql-server.v0/sql"
"gopkg.in/src-d/go-vitess.v1/mysql"
)

// Example of how to implement a MySQL server based on a Engine:
Expand All @@ -28,15 +28,10 @@ func main() {
engine.AddDatabase(createTestDatabase())
engine.AddDatabase(sql.NewInformationSchemaDB())

auth := mysql.NewAuthServerStatic()
auth.Entries["user"] = []*mysql.AuthServerStaticEntry{{
Password: "pass",
}}

config := server.Config{
Protocol: "tcp",
Address: "localhost:3306",
Auth: auth,
Auth: auth.NewNativeSingle("user", "pass", auth.AllPermissions),
}

s, err := server.NewDefaultServer(config, engine)
Expand Down
61 changes: 61 additions & 0 deletions auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package auth

import (
"strings"

"gopkg.in/src-d/go-errors.v1"
"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 to machine
// representations.
PermissionNames = map[string]Permission{
"read": ReadPerm,
"write": WritePerm,
}

// ErrNotAuthorized is returned when the user is not allowed to use a
// permission.
ErrNotAuthorized = errors.NewKind("not authorized")
// ErrNoPermission is returned when the user lacks needed permissions.
ErrNoPermission = errors.NewKind("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 returns a configured authentication method used by server.Server.
Mysql() mysql.AuthServer
// Allowed checks user's permissions with needed permission. If the user
// does not have enough permissions it returns ErrNotAuthorized.
// Otherwise is an error using the authentication method.
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

}
172 changes: 172 additions & 0 deletions auth/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package auth_test

import (
"context"
dsql "database/sql"
"fmt"
"io/ioutil"
"os"
"testing"

"github.com/stretchr/testify/require"
sqle "gopkg.in/src-d/go-mysql-server.v0"
"gopkg.in/src-d/go-mysql-server.v0/auth"
"gopkg.in/src-d/go-mysql-server.v0/mem"
"gopkg.in/src-d/go-mysql-server.v0/server"
"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/index/pilosa"
)

const port = 3336

func authEngine(au auth.Auth) (string, *sqle.Engine, error) {
db := mem.NewDatabase("test")
catalog := sql.NewCatalog()
catalog.AddDatabase(db)

tblName := "test"

table := mem.NewTable(tblName, sql.Schema{
{Name: "id", Type: sql.Text, Nullable: false, Source: tblName},
{Name: "name", Type: sql.Text, Nullable: false, Source: tblName},
})

db.AddTable(tblName, table)

tmpDir, err := ioutil.TempDir(os.TempDir(), "pilosa-test")
if err != nil {
return "", nil, err
}

err = os.MkdirAll(tmpDir, 0644)
if err != nil {
return "", nil, err
}

catalog.RegisterIndexDriver(pilosa.NewDriver(tmpDir))

a := analyzer.NewBuilder(catalog).WithAuth(au).Build()
config := &sqle.Config{Auth: au}

return tmpDir, sqle.New(catalog, a, config), nil
}

func authServer(a auth.Auth) (string, *server.Server, error) {
tmpDir, engine, err := authEngine(a)
if err != nil {
os.RemoveAll(tmpDir)
return "", nil, err
}

config := server.Config{
Protocol: "tcp",
Address: fmt.Sprintf("localhost:%d", port),
Auth: a,
}

s, err := server.NewDefaultServer(config, engine)
if err != nil {
os.RemoveAll(tmpDir)
return "", nil, err
}

go s.Start()

return tmpDir, s, nil
}

func connString(user, password string) string {
return fmt.Sprintf("%s:%s@tcp(127.0.0.1:%d)/test", user, password, port)
}

type authenticationTests []struct {
user string
password string
success bool
}

func testAuthentication(
t *testing.T,
a auth.Auth,
tests authenticationTests,
) {
t.Helper()
req := require.New(t)

tmpDir, s, err := authServer(a)
req.NoError(err)
defer os.RemoveAll(tmpDir)

for _, c := range tests {
t.Run(fmt.Sprintf("%s-%s", c.user, c.password), func(t *testing.T) {
req := require.New(t)

db, err := dsql.Open("mysql", connString(c.user, c.password))
req.NoError(err)
_, err = db.Query("SELECT 1")

if c.success {
req.NoError(err)
} else {
req.Error(err)
req.Contains(err.Error(), "Access denied")
}

err = db.Close()
req.NoError(err)
})
}

err = s.Close()
req.NoError(err)
}

var queries = map[string]string{
"select": "select * from test",
"create_index": "create index t on test using pilosa (name) with (async = false)",
"drop_index": "drop index t on test",
"insert": "insert into test (id, name) values ('id', 'name')",
"lock": "lock tables test read",
"unlock": "unlock tables",
}

type authorizationTests []struct {
user string
query string
success bool
}

func testAuthorization(
t *testing.T,
a auth.Auth,
tests authorizationTests,
) {
t.Helper()
req := require.New(t)

tmpDir, e, err := authEngine(a)
req.NoError(err)
defer os.RemoveAll(tmpDir)

for i, c := range tests {
t.Run(fmt.Sprintf("%s-%s", c.user, c.query), func(t *testing.T) {
req := require.New(t)

session := sql.NewSession("localhost", c.user, uint32(i))
ctx := sql.NewContext(context.TODO(),
sql.WithSession(session),
sql.WithPid(uint64(i)))

_, _, err := e.Query(ctx, c.query)

if c.success {
req.NoError(err)
return
}

req.Error(err)
req.True(auth.ErrNotAuthorized.Is(err))
})
}
}
Loading
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