Merge pull request #474 from nats-io/auth_cleanup

Authorization cleanup
This commit is contained in:
Derek Collison
2017-04-20 19:00:05 -07:00
committed by GitHub
17 changed files with 152 additions and 301 deletions

View File

@@ -1,45 +0,0 @@
// Copyright 2016 Apcera Inc. All rights reserved.
package auth
import (
"github.com/nats-io/gnatsd/server"
"golang.org/x/crypto/bcrypt"
)
// Plain authentication is a basic username and password
type MultiUser struct {
users map[string]*server.User
}
// Create a new multi-user
func NewMultiUser(users []*server.User) *MultiUser {
m := &MultiUser{users: make(map[string]*server.User)}
for _, u := range users {
m.users[u.Username] = u
}
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()
user, ok := m.users[opts.Username]
if !ok {
return false
}
pass := user.Password
// 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
}
c.RegisterUser(user)
return true
}

View File

@@ -1,40 +0,0 @@
// Copyright 2014-2015 Apcera Inc. All rights reserved.
package auth
import (
"strings"
"github.com/nats-io/gnatsd/server"
"golang.org/x/crypto/bcrypt"
)
const bcryptPrefix = "$2a$"
func isBcrypt(password string) bool {
return strings.HasPrefix(password, bcryptPrefix)
}
// Plain authentication is a basic username and password
type Plain struct {
Username string
Password string
}
// Check authenticates the client using a username and password
func (p *Plain) Check(c server.ClientAuth) bool {
opts := c.GetOpts()
if p.Username != opts.Username {
return false
}
// Check to see if the password is a bcrypt hash
if isBcrypt(p.Password) {
if err := bcrypt.CompareHashAndPassword([]byte(p.Password), []byte(opts.Password)); err != nil {
return false
}
} else if p.Password != opts.Password {
return false
}
return true
}

View File

@@ -1,26 +0,0 @@
package auth
import (
"github.com/nats-io/gnatsd/server"
"golang.org/x/crypto/bcrypt"
)
// Token holds a string token used for authentication
type Token struct {
Token string
}
// Check authenticates a client from a token
func (p *Token) Check(c server.ClientAuth) bool {
opts := c.GetOpts()
// Check to see if the token is a bcrypt hash
if isBcrypt(p.Token) {
if err := bcrypt.CompareHashAndPassword([]byte(p.Token), []byte(opts.Authorization)); err != nil {
return false
}
} else if p.Token != opts.Authorization {
return false
}
return true
}

32
main.go
View File

@@ -9,7 +9,6 @@ import (
"net/url"
"os"
"github.com/nats-io/gnatsd/auth"
"github.com/nats-io/gnatsd/logger"
"github.com/nats-io/gnatsd/server"
)
@@ -178,9 +177,6 @@ func main() {
// Create the server with appropriate options.
s := server.New(&opts)
// Configure the authentication mechanism
configureAuth(s, &opts)
// Configure the logger based on the flags
configureLogger(s, &opts)
@@ -188,34 +184,6 @@ func main() {
s.Start()
}
func configureAuth(s *server.Server, opts *server.Options) {
// Client
// 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,
}
s.SetClientAuthMethod(auth)
} else if opts.Authorization != "" {
auth := &auth.Token{
Token: opts.Authorization,
}
s.SetClientAuthMethod(auth)
}
// Routes
if opts.Cluster.Username != "" {
auth := &auth.Plain{
Username: opts.Cluster.Username,
Password: opts.Cluster.Password,
}
s.SetRouteAuthMethod(auth)
}
}
func configureLogger(s *server.Server, opts *server.Options) {
var log server.Logger

View File

@@ -1,23 +1,113 @@
// Copyright 2012-2014 Apcera Inc. All rights reserved.
// Copyright 2012-2017 Apcera Inc. All rights reserved.
package server
import (
"crypto/tls"
"strings"
"golang.org/x/crypto/bcrypt"
)
// Auth is an interface for implementing authentication
type Auth interface {
// Check if a client is authorized to connect
Check(c ClientAuth) bool
// User is for multiple accounts/users.
type User struct {
Username string `json:"user"`
Password string `json:"password"`
Permissions *Permissions `json:"permissions"`
}
// ClientAuth is an interface for client authentication
type ClientAuth interface {
// Get options associated with a client
GetOpts() *clientOpts
// If TLS is enabled, TLS ConnectionState, nil otherwise
GetTLSConnectionState() *tls.ConnectionState
// Optionally map a user after auth.
RegisterUser(*User)
// Permissions are the allowed subjects on a per
// publish or subscribe basis.
type Permissions struct {
Publish []string `json:"publish"`
Subscribe []string `json:"subscribe"`
}
// configureAuthorization will do any setup needed for authorization.
// Lock is assumed held.
func (s *Server) configureAuthorization() {
if s.opts == nil {
return
}
// Check for multiple users first
// This just checks and sets up the user map if we have multiple users.
if s.opts.Users != nil {
s.users = make(map[string]*User)
for _, u := range s.opts.Users {
s.users[u.Username] = u
}
s.info.AuthRequired = true
} else if s.opts.Username != "" || s.opts.Authorization != "" {
s.info.AuthRequired = true
}
}
// checkAuthorization will check authorization based on client type and
// return boolean indicating if client is authorized.
func (s *Server) checkAuthorization(c *client) bool {
switch c.typ {
case CLIENT:
return s.isClientAuthorized(c)
case ROUTER:
return s.isRouterAuthorized(c)
default:
return false
}
}
// isClientAuthorized will check the client against the proper authorization method and data.
// This could be token or username/password based.
func (s *Server) isClientAuthorized(c *client) bool {
// Check multiple users first, then token, then single user/pass.
if s.users != nil {
user, ok := s.users[c.opts.Username]
if !ok {
return false
}
ok = comparePasswords(user.Password, c.opts.Password)
// If we are authorized, register the user which will properly setup any permissions
// for pub/sub authorizations.
if ok {
c.RegisterUser(user)
}
return ok
} else if s.opts.Authorization != "" {
return comparePasswords(s.opts.Authorization, c.opts.Authorization)
} else if s.opts.Username != "" {
if s.opts.Username != c.opts.Username {
return false
}
return comparePasswords(s.opts.Password, c.opts.Password)
}
return true
}
// checkRouterAuth checks optional router authorization which can be nil or username/password.
func (s *Server) isRouterAuthorized(c *client) bool {
if s.opts.Cluster.Username != c.opts.Username {
return false
}
return comparePasswords(s.opts.Cluster.Password, c.opts.Password)
}
// Support for bcrypt stored passwords and tokens.
const bcryptPrefix = "$2a$"
// isBcrypt checks whether the given password or token is bcrypted.
func isBcrypt(password string) bool {
return strings.HasPrefix(password, bcryptPrefix)
}
func comparePasswords(serverPassword, clientPassword string) bool {
// Check to see if the server password is a bcrypt hash
if isBcrypt(serverPassword) {
if err := bcrypt.CompareHashAndPassword([]byte(serverPassword), []byte(clientPassword)); err != nil {
return false
}
} else if serverPassword != clientPassword {
return false
}
return true
}

View File

@@ -461,7 +461,7 @@ func (c *client) processConnect(arg []byte) error {
}
// Check for Auth
if ok := srv.checkAuth(c); !ok {
if ok := srv.checkAuthorization(c); !ok {
c.authViolation()
return ErrAuthorization
}

View File

@@ -30,12 +30,6 @@ type serverInfo struct {
MaxPayload int64 `json:"max_payload"`
}
type mockAuth struct{}
func (m *mockAuth) Check(c ClientAuth) bool {
return true
}
func createClientAsync(ch chan *client, s *Server, cli net.Conn) {
go func() {
c := s.createClient(cli)
@@ -56,9 +50,6 @@ func rawSetup(serverOptions Options) (*Server, *client, *bufio.Reader, string) {
cli, srv := net.Pipe()
cr := bufio.NewReaderSize(cli, maxBufSize)
s := New(&serverOptions)
if serverOptions.Authorization != "" {
s.SetClientAuthMethod(&mockAuth{})
}
ch := make(chan *client)
createClientAsync(ch, s, srv)
@@ -601,8 +592,7 @@ func TestAuthorizationTimeout(t *testing.T) {
serverOptions := defaultServerOptions
serverOptions.Authorization = "my_token"
serverOptions.AuthTimeout = 1
s, _, cr, _ := rawSetup(serverOptions)
s.SetClientAuthMethod(&mockAuth{})
_, _, cr, _ := rawSetup(serverOptions)
time.Sleep(secondsToDuration(serverOptions.AuthTimeout))
l, err := cr.ReadString('\n')
@@ -682,7 +672,6 @@ func TestTLSCloseClientConnection(t *testing.T) {
if err != nil {
t.Fatalf("Error processing config file: %v", err)
}
opts.Authorization = ""
opts.TLSTimeout = 100
s := RunServer(opts)
defer s.Shutdown()
@@ -704,7 +693,7 @@ func TestTLSCloseClientConnection(t *testing.T) {
t.Fatalf("Unexpected error during handshake: %v", err)
}
br = bufio.NewReaderSize(tlsConn, 100)
connectOp := []byte("CONNECT {\"verbose\":false,\"pedantic\":false,\"tls_required\":true}\r\n")
connectOp := []byte("CONNECT {\"user\":\"derek\",\"pass\":\"foo\",\"verbose\":false,\"pedantic\":false,\"tls_required\":true}\r\n")
if _, err := tlsConn.Write(connectOp); err != nil {
t.Fatalf("Unexpected error writing CONNECT: %v", err)
}

View File

@@ -16,15 +16,7 @@ cluster {
authorization {
user: ruser
# bcrypt version of 'bar'
password: $2a$11$lnaSz3ya7RQ3QK9T9pBPyen1WRLz4QGLu6mI3kC701NUWcBO0bml6
timeout: 2
password: $2a$10$LoRPzN3GtF2pNX5QgCBBHeUr6/zVN./RVGOu5U8SpHyg2sfzvfXji
timeout: 5
}
# Routes are actively solicited and connected to from this server.
# Other servers can connect to us if they supply the correct credentials
# in their routes definitions from above.
routes = [
nats-route://ruser:bar@127.0.0.1:7246
]
}

View File

@@ -16,8 +16,8 @@ cluster {
authorization {
user: ruser
# bcrypt version of 'bar'
password: $2a$11$lnaSz3ya7RQ3QK9T9pBPyen1WRLz4QGLu6mI3kC701NUWcBO0bml6
timeout: 2
password: $2a$10$LoRPzN3GtF2pNX5QgCBBHeUr6/zVN./RVGOu5U8SpHyg2sfzvfXji
timeout: 5
}
# Routes are actively solicited and connected to from this server.

View File

@@ -11,6 +11,6 @@ tls {
authorization {
user: derek
password: buckley
password: foo
timeout: 1
}

View File

@@ -17,20 +17,6 @@ import (
"github.com/nats-io/gnatsd/conf"
)
// For multiple accounts/users.
type User struct {
Username string `json:"user"`
Password string `json:"password"`
Permissions *Permissions `json:"permissions"`
}
// Authorization are the allowed subjects on a per
// publish or subscribe basis.
type Permissions struct {
Publish []string `json:"publish"`
Subscribe []string `json:"subscribe"`
}
// Options for clusters.
type ClusterOpts struct {
Host string `json:"addr"`

View File

@@ -92,7 +92,7 @@ func TestTLSConfigFile(t *testing.T) {
Host: "localhost",
Port: 4443,
Username: "derek",
Password: "buckley",
Password: "foo",
AuthTimeout: 1.0,
TLSTimeout: 2.0,
}

View File

@@ -82,7 +82,7 @@ func TestServerRoutesWithClients(t *testing.T) {
defer srvB.Shutdown()
// Wait for route to form.
time.Sleep(250 * time.Millisecond)
checkClusterFormed(t, srvA, srvB)
nc2, err := nats.Connect(urlB)
if err != nil {
@@ -106,11 +106,11 @@ func TestServerRoutesWithAuthAndBCrypt(t *testing.T) {
srvB := RunServer(optsB)
defer srvB.Shutdown()
urlA := fmt.Sprintf("nats://%s:%d/", optsA.Host, optsA.Port)
urlB := fmt.Sprintf("nats://%s:%d/", optsB.Host, optsB.Port)
// Wait for route to form.
time.Sleep(250 * time.Millisecond)
checkClusterFormed(t, srvA, srvB)
urlA := fmt.Sprintf("nats://%s:%s@%s:%d/", optsA.Username, optsA.Password, optsA.Host, optsA.Port)
urlB := fmt.Sprintf("nats://%s:%s@%s:%d/", optsB.Username, optsB.Password, optsB.Host, optsB.Port)
nc1, err := nats.Connect(urlA)
if err != nil {
@@ -120,7 +120,10 @@ func TestServerRoutesWithAuthAndBCrypt(t *testing.T) {
// Test that we are connected.
ch := make(chan bool)
sub, _ := nc1.Subscribe("foo", func(m *nats.Msg) { ch <- true })
sub, err := nc1.Subscribe("foo", func(m *nats.Msg) { ch <- true })
if err != nil {
t.Fatalf("Error creating subscription: %v\n", err)
}
nc1.Flush()
defer sub.Unsubscribe()
@@ -130,6 +133,7 @@ func TestServerRoutesWithAuthAndBCrypt(t *testing.T) {
}
defer nc2.Close()
nc2.Publish("foo", []byte("Hello"))
nc2.Flush()
// Wait for message
select {

View File

@@ -54,8 +54,6 @@ type Server struct {
infoJSON []byte
sl *Sublist
opts *Options
cAuth Auth
rAuth Auth
trace bool
debug bool
running bool
@@ -63,6 +61,7 @@ type Server struct {
clients map[uint64]*client
routes map[uint64]*client
remotes map[string]*client
users map[string]*User
totalClients uint64
done chan bool
start time.Time
@@ -137,30 +136,16 @@ func New(opts *Options) *Server {
// Used to kick out all of the route
// connect Go routines.
s.rcQuit = make(chan bool)
// Used to setup Authorization.
s.configureAuthorization()
s.generateServerInfoJSON()
s.handleSignals()
return s
}
// SetClientAuthMethod sets the authentication method for clients.
func (s *Server) SetClientAuthMethod(authMethod Auth) {
s.mu.Lock()
defer s.mu.Unlock()
s.info.AuthRequired = true
s.cAuth = authMethod
s.generateServerInfoJSON()
}
// SetRouteAuthMethod sets the authentication method for routes.
func (s *Server) SetRouteAuthMethod(authMethod Auth) {
s.mu.Lock()
defer s.mu.Unlock()
s.rAuth = authMethod
}
func (s *Server) generateServerInfoJSON() {
// Generate the info json
b, err := json.Marshal(s.info)
@@ -734,32 +719,6 @@ func tlsCipher(cs uint16) string {
return fmt.Sprintf("Unknown [%x]", cs)
}
func (s *Server) checkClientAuth(c *client) bool {
if s.cAuth == nil {
return true
}
return s.cAuth.Check(c)
}
func (s *Server) checkRouterAuth(c *client) bool {
if s.rAuth == nil {
return true
}
return s.rAuth.Check(c)
}
// Check auth and return boolean indicating if client is ok
func (s *Server) checkAuth(c *client) bool {
switch c.typ {
case CLIENT:
return s.checkClientAuth(c)
case ROUTER:
return s.checkRouterAuth(c)
default:
return false
}
}
// Remove a client or route from our internal accounting.
func (s *Server) removeClient(c *client) {
var rID string

View File

@@ -1,4 +1,4 @@
// Copyright 2012-2016 Apcera Inc. All rights reserved.
// Copyright 2012-2017 Apcera Inc. All rights reserved.
package test
@@ -9,7 +9,6 @@ import (
"testing"
"time"
"github.com/nats-io/gnatsd/auth"
"github.com/nats-io/gnatsd/server"
)
@@ -46,7 +45,7 @@ func runAuthServerWithToken() *server.Server {
opts := DefaultTestOptions
opts.Port = AUTH_PORT
opts.Authorization = AUTH_TOKEN
return RunServerWithAuth(&opts, &auth.Token{Token: AUTH_TOKEN})
return RunServer(&opts)
}
func TestNoAuthClient(t *testing.T) {
@@ -112,9 +111,7 @@ func runAuthServerWithUserPass() *server.Server {
opts.Port = AUTH_PORT
opts.Username = AUTH_USER
opts.Password = AUTH_PASS
auth := &auth.Plain{Username: AUTH_USER, Password: AUTH_PASS}
return RunServerWithAuth(&opts, auth)
return RunServer(&opts)
}
func TestNoUserOrPasswordClient(t *testing.T) {
@@ -170,9 +167,7 @@ func runAuthServerWithBcryptUserPass() *server.Server {
opts.Port = AUTH_PORT
opts.Username = AUTH_USER
opts.Password = BCRYPT_AUTH_HASH
auth := &auth.Plain{Username: AUTH_USER, Password: BCRYPT_AUTH_HASH}
return RunServerWithAuth(&opts, auth)
return RunServer(&opts)
}
func TestBadBcryptPassword(t *testing.T) {
@@ -206,7 +201,7 @@ func runAuthServerWithBcryptToken() *server.Server {
opts := DefaultTestOptions
opts.Port = AUTH_PORT
opts.Authorization = BCRYPT_AUTH_TOKEN_HASH
return RunServerWithAuth(&opts, &auth.Token{Token: BCRYPT_AUTH_TOKEN_HASH})
return RunServer(&opts)
}
func TestBadBcryptToken(t *testing.T) {

View File

@@ -15,7 +15,6 @@ import (
"strings"
"time"
"github.com/nats-io/gnatsd/auth"
"github.com/nats-io/gnatsd/server"
)
@@ -48,7 +47,22 @@ func RunDefaultServer() *server.Server {
// RunServer starts a new Go routine based server
func RunServer(opts *server.Options) *server.Server {
return RunServerWithAuth(opts, nil)
if opts == nil {
opts = &DefaultTestOptions
}
s := server.New(opts)
if s == nil {
panic("No NATS Server object returned.")
}
// Run server in Go routine.
go s.Start()
// Wait for accept loop(s) to be started
if !s.ReadyForConnections(10 * time.Second) {
panic("Unable to start NATS Server in Go Routine")
}
return s
}
// LoadConfig loads a configuration from a filename
@@ -64,46 +78,10 @@ func LoadConfig(configFile string) (opts *server.Options) {
// RunServerWithConfig starts a new Go routine based server with a configuration file.
func RunServerWithConfig(configFile string) (srv *server.Server, opts *server.Options) {
opts = LoadConfig(configFile)
// Check for auth
var a server.Auth
if opts.Authorization != "" {
a = &auth.Token{Token: opts.Authorization}
}
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)
srv = RunServer(opts)
return
}
// RunServerWithAuth starts a new Go routine based server with auth
func RunServerWithAuth(opts *server.Options, auth server.Auth) *server.Server {
if opts == nil {
opts = &DefaultTestOptions
}
s := server.New(opts)
if s == nil {
panic("No NATS Server object returned.")
}
if auth != nil {
s.SetClientAuthMethod(auth)
}
// Run server in Go routine.
go s.Start()
// Wait for accept loop(s) to be started
if !s.ReadyForConnections(10 * time.Second) {
panic("Unable to start NATS Server in Go Routine")
}
return s
}
func stackFatalf(t tLogger, f string, args ...interface{}) {
lines := make([]string, 0, 32)
msg := fmt.Sprintf(f, args...)

View File

@@ -201,7 +201,8 @@ func TestTLSStressConnect(t *testing.T) {
opts.NoSigs, opts.NoLog = true, true
// For this test, remove the authorization
opts.Authorization = ""
opts.Username = ""
opts.Password = ""
// Increase ssl timeout
opts.TLSTimeout = 2.0