Add support for reloading single-user and token authentication

This commit is contained in:
Tyler Treat
2017-06-12 11:52:11 -05:00
parent 855ca705c3
commit 9ba55f0f21
8 changed files with 596 additions and 8 deletions

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,6 @@
listen: localhost:4443
authorization {
user: derek
password: passw0rd
}

View File

@@ -0,0 +1,5 @@
listen: localhost:4443
authorization {
token: passw0rd
}

View File

@@ -0,0 +1,6 @@
listen: localhost:4443
authorization {
user: tyler
password: T0pS3cr3t
}

View File

@@ -0,0 +1,5 @@
listen: localhost:4443
authorization {
token: T0pS3cr3t
}

View File

@@ -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()
}
}
}

View File

@@ -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()
}