From 9ba55f0f2162e3c01b13cf6d856209f44e79af7f Mon Sep 17 00:00:00 2001 From: Tyler Treat Date: Mon, 12 Jun 2017 11:52:11 -0500 Subject: [PATCH] Add support for reloading single-user and token authentication --- server/auth.go | 2 + server/configs/reload/reload.conf | 7 + .../reload/single_user_authentication.conf | 6 + .../configs/reload/token_authentication.conf | 5 + .../configs/single_user_authentication.conf | 6 + server/configs/token_authentication.conf | 5 + server/reload.go | 138 +++++- server/reload_test.go | 435 ++++++++++++++++++ 8 files changed, 596 insertions(+), 8 deletions(-) create mode 100644 server/configs/reload/single_user_authentication.conf create mode 100644 server/configs/reload/token_authentication.conf create mode 100644 server/configs/single_user_authentication.conf create mode 100644 server/configs/token_authentication.conf diff --git a/server/auth.go b/server/auth.go index d18f85b7..e7ee5f0b 100644 --- a/server/auth.go +++ b/server/auth.go @@ -72,6 +72,8 @@ func (s *Server) configureAuthorization() { s.info.AuthRequired = true } else if opts.Username != "" || opts.Authorization != "" { s.info.AuthRequired = true + } else { + s.info.AuthRequired = false } } diff --git a/server/configs/reload/reload.conf b/server/configs/reload/reload.conf index 5324ffd3..74ed9db8 100644 --- a/server/configs/reload/reload.conf +++ b/server/configs/reload/reload.conf @@ -11,3 +11,10 @@ tls { ca_file: "../test/configs/certs/ca.pem" verify: true } + +# Enable authorization on reload +authorization { + user: tyler + password: T0pS3cr3t + timeout: 2 +} diff --git a/server/configs/reload/single_user_authentication.conf b/server/configs/reload/single_user_authentication.conf new file mode 100644 index 00000000..26efcee4 --- /dev/null +++ b/server/configs/reload/single_user_authentication.conf @@ -0,0 +1,6 @@ +listen: localhost:4443 + +authorization { + user: derek + password: passw0rd +} diff --git a/server/configs/reload/token_authentication.conf b/server/configs/reload/token_authentication.conf new file mode 100644 index 00000000..add902fc --- /dev/null +++ b/server/configs/reload/token_authentication.conf @@ -0,0 +1,5 @@ +listen: localhost:4443 + +authorization { + token: passw0rd +} diff --git a/server/configs/single_user_authentication.conf b/server/configs/single_user_authentication.conf new file mode 100644 index 00000000..3c36e3bc --- /dev/null +++ b/server/configs/single_user_authentication.conf @@ -0,0 +1,6 @@ +listen: localhost:4443 + +authorization { + user: tyler + password: T0pS3cr3t +} diff --git a/server/configs/token_authentication.conf b/server/configs/token_authentication.conf new file mode 100644 index 00000000..6af54616 --- /dev/null +++ b/server/configs/token_authentication.conf @@ -0,0 +1,5 @@ +listen: localhost:4443 + +authorization { + token: T0pS3cr3t +} diff --git a/server/reload.go b/server/reload.go index f65adee3..2aab2856 100644 --- a/server/reload.go +++ b/server/reload.go @@ -18,27 +18,46 @@ var FlagSnapshot *Options type option interface { // Apply the server option. Apply(server *Server) + + // IsLoggingChange indicates if this option requires reloading the logger. + IsLoggingChange() bool + + // IsAuthChange indicates if this option requires reloading authorization. + IsAuthChange() bool +} + +// loggingOption is a base struct that provides default option behaviors. +type loggingOption struct{} + +func (l loggingOption) IsLoggingChange() bool { + return true +} + +func (l loggingOption) IsAuthChange() bool { + return false } // traceOption implements the option interface for the `trace` setting. type traceOption struct { + loggingOption newValue bool } -// Apply the tracing change by reconfiguring the server's logger. +// Apply is a no-op because authorization will be reloaded after options are +// applied func (t *traceOption) Apply(server *Server) { - server.ConfigureLogger() server.Noticef("Reloaded: trace = %v", t.newValue) } // debugOption implements the option interface for the `debug` setting. type debugOption struct { + loggingOption newValue bool } -// Apply the debug change by reconfiguring the server's logger. +// Apply is a no-op because authorization will be reloaded after options are +// applied func (d *debugOption) Apply(server *Server) { - server.ConfigureLogger() server.Noticef("Reloaded: debug = %v", d.newValue) } @@ -49,6 +68,7 @@ type tlsOption struct { // Apply the tls change. func (t *tlsOption) Apply(server *Server) { + server.mu.Lock() tlsRequired := t.newValue != nil server.info.TLSRequired = tlsRequired message := "disabled" @@ -57,24 +77,80 @@ func (t *tlsOption) Apply(server *Server) { message = "enabled" } server.generateServerInfoJSON() + server.mu.Unlock() server.Noticef("Reloaded: tls = %s", message) } +func (t *tlsOption) IsLoggingChange() bool { + return false +} + +func (t *tlsOption) IsAuthChange() bool { + return false +} + +// authOption is a base struct that provides default option behaviors. +type authOption struct{} + +func (o authOption) IsLoggingChange() bool { + return false +} + +func (o authOption) IsAuthChange() bool { + return true +} + +// usernameOption implements the option interface for the `username` setting. +type usernameOption struct { + authOption +} + +// Apply is a no-op because authorization will be reloaded after options are +// applied. +func (u *usernameOption) Apply(server *Server) { + server.Noticef("Reloaded: username") +} + +// passwordOption implements the option interface for the `password` setting. +type passwordOption struct { + authOption +} + +// Apply is a no-op because authorization will be reloaded after options are +// applied. +func (p *passwordOption) Apply(server *Server) { + server.Noticef("Reloaded: password") +} + +// authorizationOption implements the option interface for the `token` +// authorization setting. +type authorizationOption struct { + authOption +} + +// Apply is a no-op because authorization will be reloaded after options are +// applied. +func (a *authorizationOption) Apply(server *Server) { + server.Noticef("Reloaded: token") +} + // Reload reads the current configuration file and applies any supported // changes. This returns an error if the server was not started with a config // file or an option which doesn't support hot-swapping was changed. func (s *Server) Reload() error { s.mu.Lock() - defer s.mu.Unlock() - if s.configFile == "" { + s.mu.Unlock() return errors.New("Can only reload config when a file is provided using -c or --config") } newOpts, err := ProcessConfigFile(s.configFile) if err != nil { + s.mu.Unlock() // TODO: Dump previous good config to a .bak file? return fmt.Errorf("Config reload failed: %s", err) } + s.mu.Unlock() + // Apply flags over config file settings. newOpts = MergeOptions(newOpts, FlagSnapshot) processOptions(newOpts) @@ -115,14 +191,23 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) { } switch strings.ToLower(field.Name) { case "trace": - diffOpts = append(diffOpts, &traceOption{newValue.(bool)}) + diffOpts = append(diffOpts, &traceOption{newValue: newValue.(bool)}) case "debug": - diffOpts = append(diffOpts, &debugOption{newValue.(bool)}) + diffOpts = append(diffOpts, &debugOption{newValue: newValue.(bool)}) case "tlsconfig": diffOpts = append(diffOpts, &tlsOption{newValue.(*tls.Config)}) case "tlstimeout": // TLSTimeout change is picked up when Options is swapped. continue + case "username": + diffOpts = append(diffOpts, &usernameOption{}) + case "password": + diffOpts = append(diffOpts, &passwordOption{}) + case "authorization": + diffOpts = append(diffOpts, &authorizationOption{}) + case "authtimeout": + // AuthTimeout change is picked up when Options is swapped. + continue default: // Bail out if attempting to reload any unsupported options. return nil, fmt.Errorf("Config reload not supported for %s", field.Name) @@ -133,9 +218,46 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) { } func (s *Server) applyOptions(opts []option) { + var ( + reloadLogging = false + reloadAuth = false + ) for _, opt := range opts { opt.Apply(s) + if opt.IsLoggingChange() { + reloadLogging = true + } + if opt.IsAuthChange() { + reloadAuth = true + } + } + + if reloadLogging { + s.ConfigureLogger() + } + if reloadAuth { + s.reloadAuthorization() } s.Noticef("Reloaded server configuration") } + +// reloadAuthorization reconfigures the server authorization settings and +// disconnects any clients who are no longer authorized. +func (s *Server) reloadAuthorization() { + s.mu.Lock() + s.configureAuthorization() + s.generateServerInfoJSON() + clients := make(map[uint64]*client, len(s.clients)) + for i, client := range s.clients { + clients[i] = client + } + s.mu.Unlock() + + // Disconnect any unauthorized clients. + for _, client := range clients { + if !s.isClientAuthorized(client) { + client.authViolation() + } + } +} diff --git a/server/reload_test.go b/server/reload_test.go index 1c7af78c..5e4d04c2 100644 --- a/server/reload_test.go +++ b/server/reload_test.go @@ -215,6 +215,15 @@ func TestConfigReload(t *testing.T) { if !server.info.TLSVerify { t.Fatal("Expected TLSVerify to be true") } + if updated.Username != "tyler" { + t.Fatalf("Username is incorrect.\nexpected: tyler\ngot: %s", updated.Username) + } + if updated.Password != "T0pS3cr3t" { + t.Fatalf("Password is incorrect.\nexpected: T0pS3cr3t\ngot: %s", updated.Password) + } + if !server.info.AuthRequired { + t.Fatal("Expected AuthRequired to be true") + } } // Ensure Reload supports TLS config changes. Test this by starting a server @@ -401,3 +410,429 @@ func TestConfigReloadDisableTLS(t *testing.T) { } nc.Close() } + +// Ensure Reload supports single user authentication config changes. Test this +// by starting a server with authentication enabled, connect to it to verify, +// reload config using a different username/password, ensure reconnect fails, +// then ensure reconnect succeeds when using the correct credentials. +func TestConfigReloadRotateUserAuthentication(t *testing.T) { + dir, err := os.Getwd() + if err != nil { + t.Fatalf("Error getting working directory: %v", err) + } + config := filepath.Join(dir, "tmp.conf") + + if err := os.Symlink("./configs/single_user_authentication.conf", config); err != nil { + t.Fatalf("Error creating symlink: %v", err) + } + defer os.Remove(config) + + opts, err := ProcessConfigFile(config) + if err != nil { + t.Fatalf("Error processing config file: %v", err) + } + + server := RunServer(opts) + defer server.Shutdown() + + // Ensure we can connect as a sanity check. + addr := fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port) + nc, err := nats.Connect(addr, nats.UserInfo("tyler", "T0pS3cr3t")) + if err != nil { + t.Fatalf("Error creating client: %v", err) + } + disconnected := make(chan struct{}) + asyncErr := make(chan error) + nc.SetErrorHandler(func(nc *nats.Conn, sub *nats.Subscription, err error) { + asyncErr <- err + }) + nc.SetDisconnectHandler(func(*nats.Conn) { + disconnected <- struct{}{} + }) + + // Change user credentials. + if err := os.Remove(config); err != nil { + t.Fatalf("Error deleting symlink: %v", err) + } + if err := os.Symlink("./configs/reload/single_user_authentication.conf", config); err != nil { + t.Fatalf("Error creating symlink: %v", err) + } + if err := server.Reload(); err != nil { + t.Fatalf("Error reloading config: %v", err) + } + + // Ensure connecting fails. + if _, err := nats.Connect(addr, nats.UserInfo("tyler", "T0pS3cr3t")); err == nil { + t.Fatal("Expected connect to fail") + } + + // Ensure connecting succeeds when using new credentials. + conn, err := nats.Connect(addr, nats.UserInfo("derek", "passw0rd")) + if err != nil { + t.Fatalf("Error creating client: %v", err) + } + conn.Close() + + // Ensure the previous connection received an authorization error. + select { + case err := <-asyncErr: + if err != nats.ErrAuthorization { + t.Fatalf("Expected ErrAuthorization, got %v", err) + } + case <-time.After(2 * time.Second): + t.Fatal("Expected authorization error") + } + + // Ensure the previous connection was disconnected. + select { + case <-disconnected: + case <-time.After(2 * time.Second): + t.Fatal("Expected connection to be disconnected") + } +} + +// Ensure Reload supports enabling single user authentication. Test this by +// starting a server with authentication disabled, connect to it to verify, +// reload config using with a username/password, ensure reconnect fails, then +// ensure reconnect succeeds when using the correct credentials. +func TestConfigReloadEnableUserAuthentication(t *testing.T) { + dir, err := os.Getwd() + if err != nil { + t.Fatalf("Error getting working directory: %v", err) + } + config := filepath.Join(dir, "tmp.conf") + + if err := os.Symlink("./configs/basic.conf", config); err != nil { + t.Fatalf("Error creating symlink: %v", err) + } + defer os.Remove(config) + + opts, err := ProcessConfigFile(config) + if err != nil { + t.Fatalf("Error processing config file: %v", err) + } + + server := RunServer(opts) + defer server.Shutdown() + + // Ensure we can connect as a sanity check. + addr := fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port) + nc, err := nats.Connect(addr) + if err != nil { + t.Fatalf("Error creating client: %v", err) + } + disconnected := make(chan struct{}) + asyncErr := make(chan error) + nc.SetErrorHandler(func(nc *nats.Conn, sub *nats.Subscription, err error) { + asyncErr <- err + }) + nc.SetDisconnectHandler(func(*nats.Conn) { + disconnected <- struct{}{} + }) + + // Enable authentication. + if err := os.Remove(config); err != nil { + t.Fatalf("Error deleting symlink: %v", err) + } + if err := os.Symlink("./configs/single_user_authentication.conf", config); err != nil { + t.Fatalf("Error creating symlink: %v", err) + } + if err := server.Reload(); err != nil { + t.Fatalf("Error reloading config: %v", err) + } + + // Ensure connecting fails. + if _, err := nats.Connect(addr); err == nil { + t.Fatal("Expected connect to fail") + } + + // Ensure connecting succeeds when using new credentials. + conn, err := nats.Connect(addr, nats.UserInfo("tyler", "T0pS3cr3t")) + if err != nil { + t.Fatalf("Error creating client: %v", err) + } + conn.Close() + + // Ensure the previous connection received an authorization error. + select { + case err := <-asyncErr: + if err != nats.ErrAuthorization { + t.Fatalf("Expected ErrAuthorization, got %v", err) + } + case <-time.After(2 * time.Second): + t.Fatal("Expected authorization error") + } + + // Ensure the previous connection was disconnected. + select { + case <-disconnected: + case <-time.After(2 * time.Second): + t.Fatal("Expected connection to be disconnected") + } +} + +// Ensure Reload supports disabling single user authentication. Test this by +// starting a server with authentication enabled, connect to it to verify, +// reload config using with authentication disabled, then ensure connecting +// with no credentials succeeds. +func TestConfigReloadDisableUserAuthentication(t *testing.T) { + dir, err := os.Getwd() + if err != nil { + t.Fatalf("Error getting working directory: %v", err) + } + config := filepath.Join(dir, "tmp.conf") + + if err := os.Symlink("./configs/single_user_authentication.conf", config); err != nil { + t.Fatalf("Error creating symlink: %v", err) + } + defer os.Remove(config) + + opts, err := ProcessConfigFile(config) + if err != nil { + t.Fatalf("Error processing config file: %v", err) + } + + server := RunServer(opts) + defer server.Shutdown() + + // Ensure we can connect as a sanity check. + addr := fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port) + nc, err := nats.Connect(addr, nats.UserInfo("tyler", "T0pS3cr3t")) + if err != nil { + t.Fatalf("Error creating client: %v", err) + } + nc.SetErrorHandler(func(nc *nats.Conn, sub *nats.Subscription, err error) { + t.Fatalf("Client received an unexpected error: %v", err) + }) + + // Disable authentication. + if err := os.Remove(config); err != nil { + t.Fatalf("Error deleting symlink: %v", err) + } + if err := os.Symlink("./configs/basic.conf", config); err != nil { + t.Fatalf("Error creating symlink: %v", err) + } + if err := server.Reload(); err != nil { + t.Fatalf("Error reloading config: %v", err) + } + + // Ensure connecting succeeds with no credentials. + conn, err := nats.Connect(addr) + if err != nil { + t.Fatalf("Error creating client: %v", err) + } + conn.Close() +} + +// Ensure Reload supports token authentication config changes. Test this by +// starting a server with token authentication enabled, connect to it to +// verify, reload config using a different token, ensure reconnect fails, then +// ensure reconnect succeeds when using the correct token. +func TestConfigReloadRotateTokenAuthentication(t *testing.T) { + dir, err := os.Getwd() + if err != nil { + t.Fatalf("Error getting working directory: %v", err) + } + config := filepath.Join(dir, "tmp.conf") + + if err := os.Symlink("./configs/token_authentication.conf", config); err != nil { + t.Fatalf("Error creating symlink: %v", err) + } + defer os.Remove(config) + + opts, err := ProcessConfigFile(config) + if err != nil { + t.Fatalf("Error processing config file: %v", err) + } + + server := RunServer(opts) + defer server.Shutdown() + + // Ensure we can connect as a sanity check. + addr := fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port) + nc, err := nats.Connect(addr, nats.Token("T0pS3cr3t")) + if err != nil { + t.Fatalf("Error creating client: %v", err) + } + disconnected := make(chan struct{}) + asyncErr := make(chan error) + nc.SetErrorHandler(func(nc *nats.Conn, sub *nats.Subscription, err error) { + asyncErr <- err + }) + nc.SetDisconnectHandler(func(*nats.Conn) { + disconnected <- struct{}{} + }) + + // Change authentication token. + if err := os.Remove(config); err != nil { + t.Fatalf("Error deleting symlink: %v", err) + } + if err := os.Symlink("./configs/reload/token_authentication.conf", config); err != nil { + t.Fatalf("Error creating symlink: %v", err) + } + if err := server.Reload(); err != nil { + t.Fatalf("Error reloading config: %v", err) + } + + // Ensure connecting fails. + if _, err := nats.Connect(addr, nats.Token("T0pS3cr3t")); err == nil { + t.Fatal("Expected connect to fail") + } + + // Ensure connecting succeeds when using new credentials. + conn, err := nats.Connect(addr, nats.Token("passw0rd")) + if err != nil { + t.Fatalf("Error creating client: %v", err) + } + conn.Close() + + // Ensure the previous connection received an authorization error. + select { + case err := <-asyncErr: + if err != nats.ErrAuthorization { + t.Fatalf("Expected ErrAuthorization, got %v", err) + } + case <-time.After(2 * time.Second): + t.Fatal("Expected authorization error") + } + + // Ensure the previous connection was disconnected. + select { + case <-disconnected: + case <-time.After(2 * time.Second): + t.Fatal("Expected connection to be disconnected") + } +} + +// Ensure Reload supports enabling token authentication. Test this by starting +// a server with authentication disabled, connect to it to verify, reload +// config using with a token, ensure reconnect fails, then ensure reconnect +// succeeds when using the correct token. +func TestConfigReloadEnableTokenAuthentication(t *testing.T) { + dir, err := os.Getwd() + if err != nil { + t.Fatalf("Error getting working directory: %v", err) + } + config := filepath.Join(dir, "tmp.conf") + + if err := os.Symlink("./configs/basic.conf", config); err != nil { + t.Fatalf("Error creating symlink: %v", err) + } + defer os.Remove(config) + + opts, err := ProcessConfigFile(config) + if err != nil { + t.Fatalf("Error processing config file: %v", err) + } + + server := RunServer(opts) + defer server.Shutdown() + + // Ensure we can connect as a sanity check. + addr := fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port) + nc, err := nats.Connect(addr) + if err != nil { + t.Fatalf("Error creating client: %v", err) + } + disconnected := make(chan struct{}) + asyncErr := make(chan error) + nc.SetErrorHandler(func(nc *nats.Conn, sub *nats.Subscription, err error) { + asyncErr <- err + }) + nc.SetDisconnectHandler(func(*nats.Conn) { + disconnected <- struct{}{} + }) + + // Enable authentication. + if err := os.Remove(config); err != nil { + t.Fatalf("Error deleting symlink: %v", err) + } + if err := os.Symlink("./configs/token_authentication.conf", config); err != nil { + t.Fatalf("Error creating symlink: %v", err) + } + if err := server.Reload(); err != nil { + t.Fatalf("Error reloading config: %v", err) + } + + // Ensure connecting fails. + if _, err := nats.Connect(addr); err == nil { + t.Fatal("Expected connect to fail") + } + + // Ensure connecting succeeds when using new credentials. + conn, err := nats.Connect(addr, nats.Token("T0pS3cr3t")) + if err != nil { + t.Fatalf("Error creating client: %v", err) + } + conn.Close() + + // Ensure the previous connection received an authorization error. + select { + case err := <-asyncErr: + if err != nats.ErrAuthorization { + t.Fatalf("Expected ErrAuthorization, got %v", err) + } + case <-time.After(2 * time.Second): + t.Fatal("Expected authorization error") + } + + // Ensure the previous connection was disconnected. + select { + case <-disconnected: + case <-time.After(2 * time.Second): + t.Fatal("Expected connection to be disconnected") + } +} + +// Ensure Reload supports disabling single token authentication. Test this by +// starting a server with authentication enabled, connect to it to verify, +// reload config using with authentication disabled, then ensure connecting +// with no token succeeds. +func TestConfigReloadDisableTokenAuthentication(t *testing.T) { + dir, err := os.Getwd() + if err != nil { + t.Fatalf("Error getting working directory: %v", err) + } + config := filepath.Join(dir, "tmp.conf") + + if err := os.Symlink("./configs/token_authentication.conf", config); err != nil { + t.Fatalf("Error creating symlink: %v", err) + } + defer os.Remove(config) + + opts, err := ProcessConfigFile(config) + if err != nil { + t.Fatalf("Error processing config file: %v", err) + } + + server := RunServer(opts) + defer server.Shutdown() + + // Ensure we can connect as a sanity check. + addr := fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port) + nc, err := nats.Connect(addr, nats.Token("T0pS3cr3t")) + if err != nil { + t.Fatalf("Error creating client: %v", err) + } + nc.SetErrorHandler(func(nc *nats.Conn, sub *nats.Subscription, err error) { + t.Fatalf("Client received an unexpected error: %v", err) + }) + + // Disable authentication. + if err := os.Remove(config); err != nil { + t.Fatalf("Error deleting symlink: %v", err) + } + if err := os.Symlink("./configs/basic.conf", config); err != nil { + t.Fatalf("Error creating symlink: %v", err) + } + if err := server.Reload(); err != nil { + t.Fatalf("Error reloading config: %v", err) + } + + // Ensure connecting succeeds with no credentials. + conn, err := nats.Connect(addr) + if err != nil { + t.Fatalf("Error creating client: %v", err) + } + conn.Close() +}