From 46a9e6f0bcd5874dacf4922e5b116c1276ab662c Mon Sep 17 00:00:00 2001 From: Derek Collison Date: Fri, 13 May 2016 12:27:57 -0700 Subject: [PATCH] First pass at multi-user support --- auth/multiuser.go | 41 +++++++++++++++++++++++ conf/lex.go | 2 +- conf/lex_test.go | 38 ++++++++++++++++++++++ main.go | 6 +++- server/configs/multiple_users.conf | 11 +++++++ server/opts.go | 52 ++++++++++++++++++++++++------ server/opts_test.go | 8 +++++ test/configs/multi_user.conf | 10 ++++++ test/test.go | 3 ++ 9 files changed, 160 insertions(+), 11 deletions(-) create mode 100644 auth/multiuser.go create mode 100644 server/configs/multiple_users.conf create mode 100644 test/configs/multi_user.conf diff --git a/auth/multiuser.go b/auth/multiuser.go new file mode 100644 index 00000000..0adb4142 --- /dev/null +++ b/auth/multiuser.go @@ -0,0 +1,41 @@ +// Copyright 2016 Apcera Inc. All rights reserved. + +package auth + +import ( + "golang.org/x/crypto/bcrypt" + + "github.com/nats-io/gnatsd/server" +) + +// Plain authentication is a basic username and password +type MultiUser struct { + users map[string]string +} + +// Create a new multi-user +func NewMultiUser(users []server.User) *MultiUser { + m := &MultiUser{users: make(map[string]string)} + for _, u := range users { + m.users[u.Username] = u.Password + } + return m +} + +// Check authenticates the client using a username and password against a list of multiple users. +func (m *MultiUser) Check(c server.ClientAuth) bool { + opts := c.GetOpts() + pass, ok := m.users[opts.Username] + if !ok { + return false + } + // Check to see if the password is a bcrypt hash + if isBcrypt(pass) { + if err := bcrypt.CompareHashAndPassword([]byte(pass), []byte(opts.Password)); err != nil { + return false + } + } else if pass != opts.Password { + return false + } + return true +} diff --git a/conf/lex.go b/conf/lex.go index 77199f70..a03a4319 100644 --- a/conf/lex.go +++ b/conf/lex.go @@ -610,7 +610,7 @@ func lexString(lx *lexer) stateFn { return lexStringEscape // Termination of non-quoted strings case isNL(r) || r == eof || r == optValTerm || - r == arrayValTerm || r == arrayEnd || + r == arrayValTerm || r == arrayEnd || r == mapEnd || isWhitespace(r): lx.backup() diff --git a/conf/lex_test.go b/conf/lex_test.go index 521af4b2..82200c38 100644 --- a/conf/lex_test.go +++ b/conf/lex_test.go @@ -698,3 +698,41 @@ func TestUnquotedIPAddr(t *testing.T) { lx = lex("listen = [localhost:4222, localhost:4333]") expect(t, lx, expectedItems) } + +var arrayOfMaps = ` +authorization { + users = [ + {user: alice, password: foo} + {user: bob, password: bar} + ] + timeout: 0.5 +} +` + +func TestArrayOfMaps(t *testing.T) { + expectedItems := []item{ + {itemKey, "authorization", 2}, + {itemMapStart, "", 2}, + {itemKey, "users", 3}, + {itemArrayStart, "", 3}, + {itemMapStart, "", 4}, + {itemKey, "user", 4}, + {itemString, "alice", 4}, + {itemKey, "password", 4}, + {itemString, "foo", 4}, + {itemMapEnd, "", 4}, + {itemMapStart, "", 5}, + {itemKey, "user", 5}, + {itemString, "bob", 5}, + {itemKey, "password", 5}, + {itemString, "bar", 5}, + {itemMapEnd, "", 5}, + {itemArrayEnd, "", 6}, + {itemKey, "timeout", 7}, + {itemFloat, "0.5", 7}, + {itemMapEnd, "", 8}, + {itemEOF, "", 9}, + } + lx := lex(arrayOfMaps) + expect(t, lx, expectedItems) +} diff --git a/main.go b/main.go index 96937a66..1f52453c 100644 --- a/main.go +++ b/main.go @@ -184,7 +184,11 @@ func main() { func configureAuth(s *server.Server, opts *server.Options) { // Client - if opts.Username != "" { + // Check for multiple users first + if opts.Users != nil { + auth := auth.NewMultiUser(opts.Users) + s.SetClientAuthMethod(auth) + } else if opts.Username != "" { auth := &auth.Plain{ Username: opts.Username, Password: opts.Password, diff --git a/server/configs/multiple_users.conf b/server/configs/multiple_users.conf new file mode 100644 index 00000000..2214ba0b --- /dev/null +++ b/server/configs/multiple_users.conf @@ -0,0 +1,11 @@ +# Copyright 2016 Apcera Inc. All rights reserved. + +listen: 127.0.0.1:4222 + +authorization { + users = [ + {user: alice, password: foo} + {user: bob, password: bar} + ] + timeout: 0.5 +} diff --git a/server/opts.go b/server/opts.go index 44f92337..30d6aa5d 100644 --- a/server/opts.go +++ b/server/opts.go @@ -1,10 +1,11 @@ -// Copyright 2012-2015 Apcera Inc. All rights reserved. +// Copyright 2012-2016 Apcera Inc. All rights reserved. package server import ( "crypto/tls" "crypto/x509" + "encoding/json" "fmt" "io/ioutil" "net" @@ -17,6 +18,12 @@ import ( "github.com/nats-io/gnatsd/conf" ) +// For multiple accounts/users. +type User struct { + Username string `json:"user"` + Password string `json:"password"` +} + // Options block for gnatsd server. type Options struct { Host string `json:"addr"` @@ -27,7 +34,8 @@ type Options struct { NoSigs bool `json:"-"` Logtime bool `json:"-"` MaxConn int `json:"max_connections"` - Username string `json:"user,omitempty"` + Users []User `json:"-"` + Username string `json:"-"` Password string `json:"-"` Authorization string `json:"-"` PingInterval time.Duration `json:"ping_interval"` @@ -64,8 +72,11 @@ type Options struct { } type authorization struct { - user string - pass string + // Singles + user string + pass string + // Multiple Users + users []User timeout float64 } @@ -140,10 +151,20 @@ func ProcessConfigFile(configFile string) (*Options, error) { opts.Logtime = v.(bool) case "authorization": am := v.(map[string]interface{}) - auth := parseAuthorization(am) + auth, err := parseAuthorization(am) + if err != nil { + return nil, err + } opts.Username = auth.user opts.Password = auth.pass opts.AuthTimeout = auth.timeout + // Check for multiple users defined + if auth.users != nil { + if auth.user != "" { + return nil, fmt.Errorf("Can not have a single user/pass and a users array") + } + opts.Users = auth.users + } case "http": hp, err := parseListen(v) if err != nil { @@ -244,7 +265,13 @@ func parseCluster(cm map[string]interface{}, opts *Options) error { opts.ClusterHost = mv.(string) case "authorization": am := mv.(map[string]interface{}) - auth := parseAuthorization(am) + auth, err := parseAuthorization(am) + if err != nil { + return err + } + if auth.users != nil { + return fmt.Errorf("Cluster authorization does not allow multiple users") + } opts.ClusterUsername = auth.user opts.ClusterPassword = auth.pass opts.ClusterAuthTimeout = auth.timeout @@ -280,8 +307,8 @@ func parseCluster(cm map[string]interface{}, opts *Options) error { } // Helper function to parse Authorization configs. -func parseAuthorization(am map[string]interface{}) authorization { - auth := authorization{} +func parseAuthorization(am map[string]interface{}) (*authorization, error) { + auth := &authorization{} for mk, mv := range am { switch strings.ToLower(mk) { case "user", "username": @@ -297,9 +324,16 @@ func parseAuthorization(am map[string]interface{}) authorization { at = mv.(float64) } auth.timeout = at + case "users": + b, _ := json.Marshal(mv) + users := []User{} + if err := json.Unmarshal(b, &users); err != nil { + return nil, fmt.Errorf("Could not parse user array properly, %v", err) + } + auth.users = users } } - return auth + return auth, nil } // PrintTLSHelpAndDie prints TLS usage and exits. diff --git a/server/opts_test.go b/server/opts_test.go index e410a15a..e83c279e 100644 --- a/server/opts_test.go +++ b/server/opts_test.go @@ -389,3 +389,11 @@ func TestListenPortWithColonConfig(t *testing.T) { t.Fatalf("Received incorrect port %v, expected %v\n", opts.Port, port) } } + +func TestMultipleUsersConfig(t *testing.T) { + opts, err := ProcessConfigFile("./configs/multiple_users.conf") + if err != nil { + t.Fatalf("Received an error reading config file: %v\n", err) + } + processOptions(opts) +} diff --git a/test/configs/multi_user.conf b/test/configs/multi_user.conf new file mode 100644 index 00000000..ebfb5964 --- /dev/null +++ b/test/configs/multi_user.conf @@ -0,0 +1,10 @@ +# Copyright 2016 Apcera Inc. All rights reserved. + +listen: 127.0.0.1:4233 + +authorization { + users = [ + {user: alice, password: foo} + {user: bob, password: bar} + ] +} diff --git a/test/test.go b/test/test.go index a0d7d62d..3d11fb45 100644 --- a/test/test.go +++ b/test/test.go @@ -72,6 +72,9 @@ func RunServerWithConfig(configFile string) (srv *server.Server, opts *server.Op if opts.Username != "" { a = &auth.Plain{Username: opts.Username, Password: opts.Password} } + if opts.Users != nil { + a = auth.NewMultiUser(opts.Users) + } srv = RunServerWithAuth(opts, a) return }