This repository was archived by the owner on Jan 28, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 110
auth: add authentication and authorization interface #496
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
3b3b8b0
server: create session when needed
jfontan 9361fcd
auth: add authentication and authorization interface
jfontan e678357
auth: use go-errors to better identify causes
jfontan de8c11a
server: do not use extra function to call builder
jfontan 20d732a
integration: fix test server
jfontan f4aabc3
auth: fix both clean and native password
jfontan 277877e
auth: test auth modules
jfontan e04a75b
auth: use go-errors.v1
jfontan 9271159
auth: add documentation to auth.Auth interface
jfontan 01ffab1
auth: specify permissions in NewNativeSingle
jfontan c874286
analyzer: delete readonly rule
jfontan 8cb84a7
integration: fix integration server
jfontan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
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 |
||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
}) | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.