Repository: nats-io/nats-streaming-server
Pull request #1100 (2020-10-08 00:37:17)

[ADDED] Authentication fields in streaming configuration block
Abiltity to provide Username, Password or Token in the streaming
configuration block.

If the configuration is:
```
authorization {
  users [
    { user: foo, password: bar}
  ]
}
streaming {
  ..
}
```
the server would fail to start unless user passes `-user foo -pass bar`
from the command line. But there was no way to pass those from the
configuration file. You can now configure this way:

```
streaming {
  ..
  username: "foo"
  password: "bar"
}
```
in order for the server to be able to start without the additional
`-user` and `-pass` command line parameters.

Also adding the `token` configuration option.

Signed-off-by: Ivan Kozlovic <ivan@synadia.com>


Patch:
diff --git a/server/conf.go b/server/conf.go
index 512be0e..6f9c78d 100644
--- a/server/conf.go
+++ b/server/conf.go
@@ -179,6 +179,21 @@ func ProcessConfigFile(configFile string, opts *Options) error {
 			}
 			opts.Encrypt = true
 			opts.EncryptionKey = []byte(v.(string))
+		case "username", "user":
+			if err := checkType(k, reflect.String, v); err != nil {
+				return err
+			}
+			opts.Username = v.(string)
+		case "password", "pass":
+			if err := checkType(k, reflect.String, v); err != nil {
+				return err
+			}
+			opts.Password = v.(string)
+		case "token":
+			if err := checkType(k, reflect.String, v); err != nil {
+				return err
+			}
+			opts.Token = v.(string)
 		}
 	}
 	return nil
diff --git a/server/server.go b/server/server.go
index 09ebe1d..b92fca0 100644
--- a/server/server.go
+++ b/server/server.go
@@ -1322,6 +1322,9 @@ type Options struct {
 	IOSleepTime        int64         // Duration (in micro-seconds) the server waits for more message to fill up a batch.
 	NATSServerURL      string        // URL for external NATS Server to connect to. If empty, NATS Server is embedded.
 	NATSCredentials    string        // Credentials file for connecting to external NATS Server.
+	Username           string        // Username to use if not provided from command line
+	Password           string        // Password to use if not provided from command line
+	Token              string        // Authentication token to use if not provided from command line
 	ClientHBInterval   time.Duration // Interval at which server sends heartbeat to a client.
 	ClientHBTimeout    time.Duration // How long server waits for a heartbeat response.
 	ClientHBFailCount  int           // Number of failed heartbeats before server closes client connection.
@@ -1495,9 +1498,20 @@ func (s *StanServer) createNatsClientConn(name string) (*nats.Conn, error) {
 	if err != nil {
 		return nil, err
 	}
+	// From executable, these are provided through the command line `-user ...`,
+	// so they take precedence over streaming's configuration file
 	ncOpts.User = s.natsOpts.Username
+	if ncOpts.User == "" {
+		ncOpts.User = s.opts.Username
+	}
 	ncOpts.Password = s.natsOpts.Password
+	if ncOpts.Password == "" {
+		ncOpts.Password = s.opts.Password
+	}
 	ncOpts.Token = s.natsOpts.Authorization
+	if ncOpts.Token == "" {
+		ncOpts.Token = s.opts.Token
+	}
 
 	ncOpts.Name = fmt.Sprintf("_NSS-%s-%s", s.opts.ID, name)
 


Test patch:
diff --git a/server/conf_test.go b/server/conf_test.go
index 9f2fe48..399cc5f 100644
--- a/server/conf_test.go
+++ b/server/conf_test.go
@@ -84,6 +84,15 @@ func TestParseConfig(t *testing.T) {
 	if opts.NATSCredentials != "credentials.creds" {
 		t.Fatalf("Expected Credentials to be %q, got %q", "credentials.creds", opts.NATSCredentials)
 	}
+	if opts.Username != "user" {
+		t.Fatalf("Expected Username to be %q, got %q", "user", opts.Username)
+	}
+	if opts.Password != "password" {
+		t.Fatalf("Expected Password to be %q, got %q", "password", opts.Password)
+	}
+	if opts.Token != "token" {
+		t.Fatalf("Expected Token to be %q, got %q", "token", opts.Token)
+	}
 	if !opts.FileStoreOpts.CompactEnabled {
 		t.Fatalf("Expected CompactEnabled to be true, got false")
 	}
@@ -497,6 +506,9 @@ func TestParseWrongTypes(t *testing.T) {
 	expectFailureFor(t, "encryption_cipher: 123", wrongTypeErr)
 	expectFailureFor(t, "encryption_key: 123", wrongTypeErr)
 	expectFailureFor(t, "credentials: 123", wrongTypeErr)
+	expectFailureFor(t, "username: 123", wrongTypeErr)
+	expectFailureFor(t, "password: 123", wrongTypeErr)
+	expectFailureFor(t, "token: 123", wrongTypeErr)
 }
 
 func expectFailureFor(t *testing.T, content, errorMatch string) {
diff --git a/server/server_test.go b/server/server_test.go
index 3770a26..6950dc0 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -409,6 +409,28 @@ func createConnectionWithNatsOpts(t tLogger, clientName string,
 	return sc, nc
 }
 
+func createConfFile(t *testing.T, content []byte) string {
+	t.Helper()
+	conf, err := ioutil.TempFile("", "")
+	if err != nil {
+		t.Fatalf("Error creating conf file: %v", err)
+	}
+	fName := conf.Name()
+	conf.Close()
+	if err := ioutil.WriteFile(fName, content, 0666); err != nil {
+		os.Remove(fName)
+		t.Fatalf("Error writing conf file: %v", err)
+	}
+	return fName
+}
+
+func changeCurrentConfigContentWithNewContent(t *testing.T, curConfig string, content []byte) {
+	t.Helper()
+	if err := ioutil.WriteFile(curConfig, content, 0666); err != nil {
+		t.Fatalf("Error writing config: %v", err)
+	}
+}
+
 func NewDefaultConnection(t tLogger) stan.Conn {
 	sc, err := stan.Connect(clusterName, clientName)
 	if err != nil {
@@ -1443,3 +1465,93 @@ func TestServerRandomClientURL(t *testing.T) {
 		t.Fatal("options port did not match clientURL port")
 	}
 }
+
+func TestServerAuthInConfig(t *testing.T) {
+	conf := createConfFile(t, []byte(`
+		authorization {
+			users [
+				{user: foo, password: bar}
+			]
+		}
+		streaming {
+			username: foo
+			password: bar
+		}
+	`))
+	defer os.Remove(conf)
+
+	fs := flag.NewFlagSet("test", flag.ContinueOnError)
+	noPrint := func() {}
+	sopts, nopts, err := ConfigureOptions(fs, []string{"-c", conf}, noPrint, noPrint, noPrint)
+	if err != nil {
+		t.Fatalf("Error on configure: %v", err)
+	}
+	s := runServerWithOpts(t, sopts, nopts)
+	s.Shutdown()
+	s = nil
+
+	// Check that command line still takes precedence
+	fs = flag.NewFlagSet("test", flag.ContinueOnError)
+	sopts, nopts, err = ConfigureOptions(fs, []string{"-c", conf, "-user", "foo", "-pass", "wrong"}, noPrint, noPrint, noPrint)
+	if err != nil {
+		t.Fatalf("Error on configure: %v", err)
+	}
+	s, err = RunServerWithOpts(sopts, nopts)
+	if err == nil {
+		if s != nil {
+			s.Shutdown()
+		}
+		t.Fatal("Expected to fail to start")
+	}
+
+	conf = createConfFile(t, []byte(`
+		authorization {
+			token: mytoken
+		}
+		streaming {
+			token: mytoken
+		}
+	`))
+	defer os.Remove(conf)
+
+	fs = flag.NewFlagSet("test", flag.ContinueOnError)
+	sopts, nopts, err = ConfigureOptions(fs, []string{"-c", conf}, noPrint, noPrint, noPrint)
+	if err != nil {
+		t.Fatalf("Error on configure: %v", err)
+	}
+	s = runServerWithOpts(t, sopts, nopts)
+	s.Shutdown()
+
+	// For token, the authorization{ token: <value> } is replaced with value passed by `-auth`
+	// and also used to connect. So the test here shows that if `-auth` is specified, it
+	// will both use this as the authentication token and use that to create the connections.
+	// We show that streaming { token: wrong } does not prevent from running.
+	conf = createConfFile(t, []byte(`
+		streaming {
+			token: ignored_because_command_line_is_used
+		}
+	`))
+	defer os.Remove(conf)
+
+	fs = flag.NewFlagSet("test", flag.ContinueOnError)
+	sopts, nopts, err = ConfigureOptions(fs, []string{"-c", conf, "-auth", "realtoken"}, noPrint, noPrint, noPrint)
+	if err != nil {
+		t.Fatalf("Error on configure: %v", err)
+	}
+	s = runServerWithOpts(t, sopts, nopts)
+	defer s.Shutdown()
+
+	// Check that NATS connection really need to provide "realtoken" as authentication token.
+	nc, err := nats.Connect(nats.DefaultURL)
+	if err == nil {
+		if nc != nil {
+			nc.Close()
+		}
+		t.Fatal("Should not have connected")
+	}
+	nc, err = nats.Connect(nats.DefaultURL, nats.Token("realtoken"))
+	if err != nil {
+		t.Fatalf("Error connecting: %v", err)
+	}
+	nc.Close()
+}
diff --git a/server/signal_test.go b/server/signal_test.go
index eabf01e..3dd6693 100644
--- a/server/signal_test.go
+++ b/server/signal_test.go
@@ -156,28 +156,6 @@ func TestSignalTrapsSIGTERM(t *testing.T) {
 	}
 }
 
-func createConfFile(t *testing.T, content []byte) string {
-	t.Helper()
-	conf, err := ioutil.TempFile("", "")
-	if err != nil {
-		t.Fatalf("Error creating conf file: %v", err)
-	}
-	fName := conf.Name()
-	conf.Close()
-	if err := ioutil.WriteFile(fName, content, 0666); err != nil {
-		os.Remove(fName)
-		t.Fatalf("Error writing conf file: %v", err)
-	}
-	return fName
-}
-
-func changeCurrentConfigContentWithNewContent(t *testing.T, curConfig string, content []byte) {
-	t.Helper()
-	if err := ioutil.WriteFile(curConfig, content, 0666); err != nil {
-		t.Fatalf("Error writing config: %v", err)
-	}
-}
-
 func TestSignalReload(t *testing.T) {
 	conf := createConfFile(t, []byte(`debug: false`))
 	defer os.Remove(conf)
diff --git a/test/configs/test_parse.conf b/test/configs/test_parse.conf
index 000b054..7f3152e 100644
--- a/test/configs/test_parse.conf
+++ b/test/configs/test_parse.conf
@@ -17,6 +17,9 @@ streaming: {
   encryption_cipher: "AES"
   encryption_key: "key"
   credentials: "credentials.creds"
+  username: "user"
+  password: "password"
+  token: "token"
 
   store_limits: {
       max_channels: 11