Repository: nats-io/nats-server
Pull request #3671 (2022-11-28 15:12:32)

[ADDED] User Information requests at $SYS.REQ.USER.INFO
Allow connected users to query information about bound account name and user permissions. This even works for users in the system account itself.

We do not return detailed account information here to allow for separate security/subject permissions for that and jetstream account information.

This should be added to NATS cli as a parallel call to account info when merged.

Note many of the changes are fixes to low level historical tests. When you add a base level system subscription that is imported by other accounts lots of test updates need to happen.

Signed-off-by: Derek Collison <derek@nats.io>

/cc @nats-io/core



Patch:
diff --git a/server/accounts.go b/server/accounts.go
index 1ccb6295..64111bac 100644
--- a/server/accounts.go
+++ b/server/accounts.go
@@ -1567,9 +1567,14 @@ func (a *Account) checkStreamImportsForCycles(to string, visited map[string]bool
 // SetServiceImportSharing will allow sharing of information about requests with the export account.
 // Used for service latency tracking at the moment.
 func (a *Account) SetServiceImportSharing(destination *Account, to string, allow bool) error {
+	return a.setServiceImportSharing(destination, to, true, allow)
+}
+
+// setServiceImportSharing will allow sharing of information about requests with the export account.
+func (a *Account) setServiceImportSharing(destination *Account, to string, check, allow bool) error {
 	a.mu.Lock()
 	defer a.mu.Unlock()
-	if a.isClaimAccount() {
+	if check && a.isClaimAccount() {
 		return fmt.Errorf("claim based accounts can not be updated directly")
 	}
 	for _, si := range a.imports.services {
diff --git a/server/client.go b/server/client.go
index 946fc803..1e6d7a6e 100644
--- a/server/client.go
+++ b/server/client.go
@@ -250,6 +250,7 @@ type client struct {
 	darray     []string
 	pcd        map[*client]struct{}
 	atmr       *time.Timer
+	expires    time.Time
 	ping       pinfo
 	msgb       [msgScratchSize]byte
 	last       time.Time
@@ -960,6 +961,61 @@ func (c *client) setPermissions(perms *Permissions) {
 	}
 }
 
+// Build public permissions from internal ones.
+// Used for user info requests.
+func (c *client) publicPermissions() *Permissions {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	if c.perms == nil {
+		return nil
+	}
+	perms := &Permissions{
+		Publish:   &SubjectPermission{},
+		Subscribe: &SubjectPermission{},
+	}
+
+	_subs := [32]*subscription{}
+
+	// Publish
+	if c.perms.pub.allow != nil {
+		subs := _subs[:0]
+		c.perms.pub.allow.All(&subs)
+		for _, sub := range subs {
+			perms.Publish.Allow = append(perms.Publish.Allow, string(sub.subject))
+		}
+	}
+	if c.perms.pub.deny != nil {
+		subs := _subs[:0]
+		c.perms.pub.deny.All(&subs)
+		for _, sub := range subs {
+			perms.Publish.Deny = append(perms.Publish.Deny, string(sub.subject))
+		}
+	}
+	// Subsribe
+	if c.perms.sub.allow != nil {
+		subs := _subs[:0]
+		c.perms.sub.allow.All(&subs)
+		for _, sub := range subs {
+			perms.Subscribe.Allow = append(perms.Subscribe.Allow, string(sub.subject))
+		}
+	}
+	if c.perms.sub.deny != nil {
+		subs := _subs[:0]
+		c.perms.sub.deny.All(&subs)
+		for _, sub := range subs {
+			perms.Subscribe.Deny = append(perms.Subscribe.Deny, string(sub.subject))
+		}
+	}
+	// Responses.
+	if c.perms.resp != nil {
+		rp := *c.perms.resp
+		perms.Response = &rp
+	}
+
+	return perms
+}
+
 type denyType int
 
 const (
@@ -4656,9 +4712,23 @@ func (c *client) awaitingAuth() bool {
 func (c *client) setExpirationTimer(d time.Duration) {
 	c.mu.Lock()
 	c.atmr = time.AfterFunc(d, c.authExpired)
+	// This is an JWT expiration.
+	if c.flags.isSet(connectReceived) {
+		c.expires = time.Now().Add(d).Truncate(time.Second)
+	}
 	c.mu.Unlock()
 }
 
+// Return when this client expires via a claim, or 0 if not set.
+func (c *client) claimExpiration() time.Duration {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	if c.expires.IsZero() {
+		return 0
+	}
+	return time.Until(c.expires).Truncate(time.Second)
+}
+
 // Possibly flush the connection and then close the low level connection.
 // The boolean `minimalFlush` indicates if the flush operation should have a
 // minimal write deadline.
@@ -5302,16 +5372,16 @@ func (c *client) doTLSHandshake(typ string, solicit bool, url *url.URL, tlsConfi
 // Lock should be held.
 func (c *client) getRawAuthUser() string {
 	switch {
-	case c.opts.Nkey != "":
+	case c.opts.Nkey != _EMPTY_:
 		return c.opts.Nkey
-	case c.opts.Username != "":
+	case c.opts.Username != _EMPTY_:
 		return c.opts.Username
-	case c.opts.JWT != "":
+	case c.opts.JWT != _EMPTY_:
 		return c.pubKey
-	case c.opts.Token != "":
+	case c.opts.Token != _EMPTY_:
 		return c.opts.Token
 	default:
-		return ""
+		return _EMPTY_
 	}
 }
 
@@ -5319,12 +5389,14 @@ func (c *client) getRawAuthUser() string {
 // Lock should be held.
 func (c *client) getAuthUser() string {
 	switch {
-	case c.opts.Nkey != "":
+	case c.opts.Nkey != _EMPTY_:
 		return fmt.Sprintf("Nkey %q", c.opts.Nkey)
-	case c.opts.Username != "":
+	case c.opts.Username != _EMPTY_:
 		return fmt.Sprintf("User %q", c.opts.Username)
-	case c.opts.JWT != "":
+	case c.opts.JWT != _EMPTY_:
 		return fmt.Sprintf("JWT User %q", c.pubKey)
+	case c.opts.Token != _EMPTY_:
+		return fmt.Sprintf("Token %q", c.opts.Token)
 	default:
 		return `User "N/A"`
 	}
diff --git a/server/events.go b/server/events.go
index 6c61442f..ac7e11aa 100644
--- a/server/events.go
+++ b/server/events.go
@@ -62,6 +62,10 @@ const (
 	remoteLatencyEventSubj   = "$SYS.LATENCY.M2.%s"
 	inboxRespSubj            = "$SYS._INBOX.%s.%s"
 
+	// Used to return information to a user on bound account and user permissions.
+	userDirectInfoSubj = "$SYS.REQ.USER.INFO"
+	userDirectReqSubj  = "$SYS.REQ.USER.%s.INFO"
+
 	// FIXME(dlc) - Should account scope, even with wc for now, but later on
 	// we can then shard as needed.
 	accNumSubsReqSubj = "$SYS.REQ.ACCOUNT.NSUBS"
@@ -1030,6 +1034,13 @@ func (s *Server) initEventTracking() {
 		}
 	}
 
+	// User info.
+	// TODO(dlc) - Can be internal and not forwarded since bound server for the client connection
+	// is only one that will answer. This breaks tests since we still forward on remote server connect.
+	if _, err := s.sysSubscribe(fmt.Sprintf(userDirectReqSubj, "*"), s.userInfoReq); err != nil {
+		s.Errorf("Error setting up internal tracking: %v", err)
+	}
+
 	// For now only the STATZ subject has an account specific ping equivalent.
 	if _, err := s.sysSubscribe(fmt.Sprintf(accPingReqSubj, "STATZ"),
 		func(sub *subscription, c *client, _ *Account, subject, reply string, msg []byte) {
@@ -1064,6 +1075,40 @@ func (s *Server) initEventTracking() {
 	}
 }
 
+// UserInfo returns basic information to a user about bound account and user permissions.
+// For account information they will need to ping that separately, and this allows security
+// controls on each subsystem if desired, e.g. account info, jetstream account info, etc.
+type UserInfo struct {
+	UserID      string        `json:"user"`
+	Account     string        `json:"account"`
+	Permissions *Permissions  `json:"permissions,omitempty"`
+	Expires     time.Duration `json:"expires,omitempty"`
+}
+
+// Process a user info request.
+func (s *Server) userInfoReq(sub *subscription, c *client, _ *Account, subject, reply string, msg []byte) {
+	if !s.EventsEnabled() || reply == _EMPTY_ {
+		return
+	}
+
+	response := &ServerAPIResponse{Server: &ServerInfo{}}
+
+	ci, _, _, _, err := s.getRequestInfo(c, msg)
+	if err != nil {
+		response.Error = &ApiError{Code: http.StatusBadRequest}
+		s.sendInternalResponse(reply, response)
+		return
+	}
+
+	response.Data = &UserInfo{
+		UserID:      ci.User,
+		Account:     ci.Account,
+		Permissions: c.publicPermissions(),
+		Expires:     c.claimExpiration(),
+	}
+	s.sendInternalResponse(reply, response)
+}
+
 // register existing accounts with any system exports.
 func (s *Server) registerSystemImportsForExisting() {
 	var accounts []*Account
@@ -1117,6 +1162,20 @@ func (s *Server) addSystemAccountExports(sacc *Account) {
 		}
 	}
 
+	// User info export.
+	userInfoSubj := fmt.Sprintf(userDirectReqSubj, "*")
+	if !sacc.hasServiceExportMatching(userInfoSubj) {
+		if err := sacc.AddServiceExport(userInfoSubj, nil); err != nil {
+			s.Errorf("Error adding system service export for %q: %v", userInfoSubj, err)
+		}
+		mappedSubj := fmt.Sprintf(userDirectReqSubj, sacc.GetName())
+		if err := sacc.AddServiceImport(sacc, userDirectInfoSubj, mappedSubj); err != nil {
+			s.Errorf("Error setting up system service import %s: %v", mappedSubj, err)
+		}
+		// Make sure to share details.
+		sacc.setServiceImportSharing(sacc, mappedSubj, false, true)
+	}
+
 	// Register any accounts that existed prior.
 	s.registerSystemImportsForExisting()
 
@@ -1721,6 +1780,12 @@ func (s *Server) registerSystemImports(a *Account) {
 	importSrvc(fmt.Sprintf(accPingReqSubj, "CONNZ"), mappedConnzSubj)
 	importSrvc(fmt.Sprintf(serverPingReqSubj, "CONNZ"), mappedConnzSubj)
 	importSrvc(fmt.Sprintf(accPingReqSubj, "STATZ"), fmt.Sprintf(accDirectReqSubj, a.Name, "STATZ"))
+
+	// This is for user's looking up their own info.
+	mappedSubject := fmt.Sprintf(userDirectReqSubj, a.Name)
+	importSrvc(userDirectInfoSubj, mappedSubject)
+	// Make sure to share details.
+	a.setServiceImportSharing(sacc, mappedSubject, false, true)
 }
 
 // Setup tracking for this account. This allows us to track global account activity.
diff --git a/server/jetstream.go b/server/jetstream.go
index 46112298..c35a1ee4 100644
--- a/server/jetstream.go
+++ b/server/jetstream.go
@@ -2584,7 +2584,7 @@ func friendlyBytes(bytes int64) string {
 }
 
 func isValidName(name string) bool {
-	if name == "" {
+	if name == _EMPTY_ {
 		return false
 	}
 	return !strings.ContainsAny(name, " \t\r\n\f.*>")
diff --git a/server/opts.go b/server/opts.go
index d2cf0fc3..b00f4c42 100644
--- a/server/opts.go
+++ b/server/opts.go
@@ -904,7 +904,7 @@ func (o *Options) processConfigFileLine(k string, v interface{}, errors *[]error
 		}
 	case "store_dir", "storedir":
 		// Check if JetStream configuration is also setting the storage directory.
-		if o.StoreDir != "" {
+		if o.StoreDir != _EMPTY_ {
 			*errors = append(*errors, &configErr{tk, "Duplicate 'store_dir' configuration"})
 			return
 		}
@@ -1127,8 +1127,8 @@ func (o *Options) processConfigFileLine(k string, v interface{}, errors *[]error
 			}
 		case map[string]interface{}:
 			del := false
-			dir := ""
-			dirType := ""
+			dir := _EMPTY_
+			dirType := _EMPTY_
 			limit := int64(0)
 			ttl := time.Duration(0)
 			sync := time.Duration(0)
@@ -1169,7 +1169,7 @@ func (o *Options) processConfigFileLine(k string, v interface{}, errors *[]error
 				*errors = append(*errors, &configErr{tk, err.Error()})
 				return
 			}
-			if dir == "" {
+			if dir == _EMPTY_ {
 				*errors = append(*errors, &configErr{tk, "dir has no value and needs to point to a directory"})
 				return
 			}
@@ -1817,7 +1817,7 @@ func getStorageSize(v interface{}) (int64, error) {
 		return 0, fmt.Errorf("must be int64 or string")
 	}
 
-	if s == "" {
+	if s == _EMPTY_ {
 		return 0, nil
 	}
 
@@ -2856,7 +2856,7 @@ func parseAccounts(v interface{}, opts *Options, errors *[]error, warnings *[]er
 			*errors = append(*errors, &configErr{tk, msg})
 			continue
 		}
-		if stream.pre != "" {
+		if stream.pre != _EMPTY_ {
 			if err := stream.acc.AddStreamImport(ta, stream.sub, stream.pre); err != nil {
 				msg := fmt.Sprintf("Error adding stream import %q: %v", stream.sub, err)
 				*errors = append(*errors, &configErr{tk, msg})
@@ -3310,16 +3310,16 @@ func parseImportStreamOrService(v interface{}, errors, warnings *[]error) (*impo
 				*errors = append(*errors, err)
 				continue
 			}
-			if accountName == "" || subject == "" {
+			if accountName == _EMPTY_ || subject == _EMPTY_ {
 				err := &configErr{tk, "Expect an account name and a subject"}
 				*errors = append(*errors, err)
 				continue
 			}
 			curStream = &importStream{an: accountName, sub: subject}
-			if to != "" {
+			if to != _EMPTY_ {
 				curStream.to = to
 			}
-			if pre != "" {
+			if pre != _EMPTY_ {
 				curStream.pre = pre
 			}
 		case "service":
@@ -3340,13 +3340,13 @@ func parseImportStreamOrService(v interface{}, errors, warnings *[]error) (*impo
 				*errors = append(*errors, err)
 				continue
 			}
-			if accountName == "" || subject == "" {
+			if accountName == _EMPTY_ || subject == _EMPTY_ {
 				err := &configErr{tk, "Expect an account name and a subject"}
 				*errors = append(*errors, err)
 				continue
 			}
 			curService = &importService{an: accountName, sub: subject}
-			if to != "" {
+			if to != _EMPTY_ {
 				curService.to = to
 			} else {
 				curService.to = subject
@@ -3364,7 +3364,7 @@ func parseImportStreamOrService(v interface{}, errors, warnings *[]error) (*impo
 			}
 			if curStream != nil {
 				curStream.to = to
-				if curStream.pre != "" {
+				if curStream.pre != _EMPTY_ {
 					err := &configErr{tk, "Stream import can not have a 'prefix' and a 'to' property"}
 					*errors = append(*errors, err)
 					continue
@@ -3541,7 +3541,7 @@ func parseUsers(mv interface{}, opts *Options, errors *[]error, warnings *[]erro
 		// Place perms if we have them.
 		if perms != nil {
 			// nkey takes precedent.
-			if nkey.Nkey != "" {
+			if nkey.Nkey != _EMPTY_ {
 				nkey.Permissions = perms
 			} else {
 				user.Permissions = perms
@@ -3549,15 +3549,15 @@ func parseUsers(mv interface{}, opts *Options, errors *[]error, warnings *[]erro
 		}
 
 		// Check to make sure we have at least an nkey or username <password> defined.
-		if nkey.Nkey == "" && user.Username == "" {
+		if nkey.Nkey == _EMPTY_ && user.Username == _EMPTY_ {
 			return nil, nil, &configErr{tk, "User entry requires a user"}
-		} else if nkey.Nkey != "" {
+		} else if nkey.Nkey != _EMPTY_ {
 			// Make sure the nkey a proper public nkey for a user..
 			if !nkeys.IsValidPublicUserKey(nkey.Nkey) {
 				return nil, nil, &configErr{tk, "Not a valid public nkey for a user"}
 			}
 			// If we have user or password defined here that is an error.
-			if user.Username != "" || user.Password != "" {
+			if user.Username != _EMPTY_ || user.Password != _EMPTY_ {
 				return nil, nil, &configErr{tk, "Nkey users do not take usernames or passwords"}
 			}
 			keys = append(keys, nkey)
@@ -4259,11 +4259,11 @@ func GenTLSConfig(tc *TLSConfigOpts) (*tls.Config, error) {
 	}
 
 	switch {
-	case tc.CertFile != "" && tc.KeyFile == "":
+	case tc.CertFile != _EMPTY_ && tc.KeyFile == _EMPTY_:
 		return nil, fmt.Errorf("missing 'key_file' in TLS configuration")
-	case tc.CertFile == "" && tc.KeyFile != "":
+	case tc.CertFile == _EMPTY_ && tc.KeyFile != _EMPTY_:
 		return nil, fmt.Errorf("missing 'cert_file' in TLS configuration")
-	case tc.CertFile != "" && tc.KeyFile != "":
+	case tc.CertFile != _EMPTY_ && tc.KeyFile != _EMPTY_:
 		// Now load in cert and private key
 		cert, err := tls.LoadX509KeyPair(tc.CertFile, tc.KeyFile)
 		if err != nil {
@@ -4281,7 +4281,7 @@ func GenTLSConfig(tc *TLSConfigOpts) (*tls.Config, error) {
 		config.ClientAuth = tls.RequireAndVerifyClientCert
 	}
 	// Add in CAs if applicable.
-	if tc.CaFile != "" {
+	if tc.CaFile != _EMPTY_ {
 		rootPEM, err := os.ReadFile(tc.CaFile)
 		if err != nil || rootPEM == nil {
 			return nil, err
@@ -4312,28 +4312,28 @@ func MergeOptions(fileOpts, flagOpts *Options) *Options {
 	if flagOpts.Port != 0 {
 		opts.Port = flagOpts.Port
 	}
-	if flagOpts.Host != "" {
+	if flagOpts.Host != _EMPTY_ {
 		opts.Host = flagOpts.Host
 	}
 	if flagOpts.DontListen {
 		opts.DontListen = flagOpts.DontListen
 	}
-	if flagOpts.ClientAdvertise != "" {
+	if flagOpts.ClientAdvertise != _EMPTY_ {
 		opts.ClientAdvertise = flagOpts.ClientAdvertise
 	}
-	if flagOpts.Username != "" {
+	if flagOpts.Username != _EMPTY_ {
 		opts.Username = flagOpts.Username
 	}
-	if flagOpts.Password != "" {
+	if flagOpts.Password != _EMPTY_ {
 		opts.Password = flagOpts.Password
 	}
-	if flagOpts.Authorization != "" {
+	if flagOpts.Authorization != _EMPTY_ {
 		opts.Authorization = flagOpts.Authorization
 	}
 	if flagOpts.HTTPPort != 0 {
 		opts.HTTPPort = flagOpts.HTTPPort
 	}
-	if flagOpts.HTTPBasePath != "" {
+	if flagOpts.HTTPBasePath != _EMPTY_ {
 		opts.HTTPBasePath = flagOpts.HTTPBasePath
 	}
 	if flagOpts.Debug {
@@ -4345,19 +4345,19 @@ func MergeOptions(fileOpts, flagOpts *Options) *Options {
 	if flagOpts.Logtime {
 		opts.Logtime = true
 	}
-	if flagOpts.LogFile != "" {
+	if flagOpts.LogFile != _EMPTY_ {
 		opts.LogFile = flagOpts.LogFile
 	}
-	if flagOpts.PidFile != "" {
+	if flagOpts.PidFile != _EMPTY_ {
 		opts.PidFile = flagOpts.PidFile
 	}
-	if flagOpts.PortsFileDir != "" {
+	if flagOpts.PortsFileDir != _EMPTY_ {
 		opts.PortsFileDir = flagOpts.PortsFileDir
 	}
 	if flagOpts.ProfPort != 0 {
 		opts.ProfPort = flagOpts.ProfPort
 	}
-	if flagOpts.Cluster.ListenStr != "" {
+	if flagOpts.Cluster.ListenStr != _EMPTY_ {
 		opts.Cluster.ListenStr = flagOpts.Cluster.ListenStr
 	}
 	if flagOpts.Cluster.NoAdvertise {
@@ -4366,10 +4366,10 @@ func MergeOptions(fileOpts, flagOpts *Options) *Options {
 	if flagOpts.Cluster.ConnectRetries != 0 {
 		opts.Cluster.ConnectRetries = flagOpts.Cluster.ConnectRetries
 	}
-	if flagOpts.Cluster.Advertise != "" {
+	if flagOpts.Cluster.Advertise != _EMPTY_ {
 		opts.Cluster.Advertise = flagOpts.Cluster.Advertise
 	}
-	if flagOpts.RoutesStr != "" {
+	if flagOpts.RoutesStr != _EMPTY_ {
 		mergeRoutes(&opts, flagOpts)
 	}
 	if flagOpts.JetStream {
@@ -4485,10 +4485,10 @@ func getInterfaceIPs() ([]net.IP, error) {
 
 func setBaselineOptions(opts *Options) {
 	// Setup non-standard Go defaults
-	if opts.Host == "" {
+	if opts.Host == _EMPTY_ {
 		opts.Host = DEFAULT_HOST
 	}
-	if opts.HTTPHost == "" {
+	if opts.HTTPHost == _EMPTY_ {
 		// Default to same bind from server if left undefined
 		opts.HTTPHost = opts.Host
 	}
@@ -4514,7 +4514,7 @@ func setBaselineOptions(opts *Options) {
 		opts.AuthTimeout = getDefaultAuthTimeout(opts.TLSConfig, opts.TLSTimeout)
 	}
 	if opts.Cluster.Port != 0 {
-		if opts.Cluster.Host == "" {
+		if opts.Cluster.Host == _EMPTY_ {
 			opts.Cluster.Host = DEFAULT_HOST
 		}
 		if opts.Cluster.TLSTimeout == 0 {
@@ -4525,7 +4525,7 @@ func setBaselineOptions(opts *Options) {
 		}
 	}
 	if opts.LeafNode.Port != 0 {
-		if opts.LeafNode.Host == "" {
+		if opts.LeafNode.Host == _EMPTY_ {
 			opts.LeafNode.Host = DEFAULT_HOST
 		}
 		if opts.LeafNode.TLSTimeout == 0 {
@@ -4539,7 +4539,7 @@ func setBaselineOptions(opts *Options) {
 	for _, r := range opts.LeafNode.Remotes {
 		if r != nil {
 			for _, u := range r.URLs {
-				if u.Port() == "" {
+				if u.Port() == _EMPTY_ {
 					u.Host = net.JoinHostPort(u.Host, strconv.Itoa(DEFAULT_LEAFNODE_PORT))
 				}
 			}
@@ -4573,7 +4573,7 @@ func setBaselineOptions(opts *Options) {
 		opts.LameDuckGracePeriod = DEFAULT_LAME_DUCK_GRACE_PERIOD
 	}
 	if opts.Gateway.Port != 0 {
-		if opts.Gateway.Host == "" {
+		if opts.Gateway.Host == _EMPTY_ {
 			opts.Gateway.Host = DEFAULT_HOST
 		}
 		if opts.Gateway.TLSTimeout == 0 {
@@ -4590,12 +4590,12 @@ func setBaselineOptions(opts *Options) {
 		opts.ReconnectErrorReports = DEFAULT_RECONNECT_ERROR_REPORTS
 	}
 	if opts.Websocket.Port != 0 {
-		if opts.Websocket.Host == "" {
+		if opts.Websocket.Host == _EMPTY_ {
 			opts.Websocket.Host = DEFAULT_HOST
 		}
 	}
 	if opts.MQTT.Port != 0 {
-		if opts.MQTT.Host == "" {
+		if opts.MQTT.Host == _EMPTY_ {
 			opts.MQTT.Host = DEFAULT_HOST
 		}
 		if opts.MQTT.TLSTimeout == 0 {
@@ -4643,13 +4643,13 @@ func ConfigureOptions(fs *flag.FlagSet, args []string, printVersion, printHelp,
 	fs.BoolVar(&showHelp, "help", false, "Show this message.")
 	fs.IntVar(&opts.Port, "port", 0, "Port to listen on.")
 	fs.IntVar(&opts.Port, "p", 0, "Port to listen on.")
-	fs.StringVar(&opts.ServerName, "n", "", "Server name.")
-	fs.StringVar(&opts.ServerName, "name", "", "Server name.")
-	fs.StringVar(&opts.ServerName, "server_name", "", "Server name.")
-	fs.StringVar(&opts.Host, "addr", "", "Network host to listen on.")
-	fs.StringVar(&opts.Host, "a", "", "Network host to listen on.")
-	fs.StringVar(&opts.Host, "net", "", "Network host to listen on.")
-	fs.StringVar(&opts.ClientAdvertise, "client_advertise", "", "Client URL to advertise to other servers.")
+	fs.StringVar(&opts.ServerName, "n", _EMPTY_, "Server name.")
+	fs.StringVar(&opts.ServerName, "name", _EMPTY_, "Server name.")
+	fs.StringVar(&opts.ServerName, "server_name", _EMPTY_, "Server name.")
+	fs.StringVar(&opts.Host, "addr", _EMPTY_, "Network host to listen on.")
+	fs.StringVar(&opts.Host, "a", _EMPTY_, "Network host to listen on.")
+	fs.StringVar(&opts.Host, "net", _EMPTY_, "Network host to listen on.")
+	fs.StringVar(&opts.ClientAdvertise, "client_advertise", _EMPTY_, "Client URL to advertise to other servers.")
 	fs.BoolVar(&opts.Debug, "D", false, "Enable Debug logging.")
 	fs.BoolVar(&opts.Debug, "debug", false, "Enable Debug logging.")
 	fs.BoolVar(&opts.Trace, "V", false, "Enable Trace logging.")
@@ -4659,49 +4659,49 @@ func ConfigureOptions(fs *flag.FlagSet, args []string, printVersion, printHelp,
 	fs.BoolVar(&dbgAndTrcAndVerboseTrc, "DVV", false, "Enable Debug and Verbose Trace logging. (Traces system account as well)")
 	fs.BoolVar(&opts.Logtime, "T", true, "Timestamp log entries.")
 	fs.BoolVar(&opts.Logtime, "logtime", true, "Timestamp log entries.")
-	fs.StringVar(&opts.Username, "user", "", "Username required for connection.")
-	fs.StringVar(&opts.Password, "pass", "", "Password required for connection.")
-	fs.StringVar(&opts.Authorization, "auth", "", "Authorization token required for connection.")
+	fs.StringVar(&opts.Username, "user", _EMPTY_, "Username required for connection.")
+	fs.StringVar(&opts.Password, "pass", _EMPTY_, "Password required for connection.")
+	fs.StringVar(&opts.Authorization, "auth", _EMPTY_, "Authorization token required for connection.")
 	fs.IntVar(&opts.HTTPPort, "m", 0, "HTTP Port for /varz, /connz endpoints.")
 	fs.IntVar(&opts.HTTPPort, "http_port", 0, "HTTP Port for /varz, /connz endpoints.")
 	fs.IntVar(&opts.HTTPSPort, "ms", 0, "HTTPS Port for /varz, /connz endpoints.")
 	fs.IntVar(&opts.HTTPSPort, "https_port", 0, "HTTPS Port for /varz, /connz endpoints.")
-	fs.StringVar(&configFile, "c", "", "Configuration file.")
-	fs.StringVar(&configFile, "config", "", "Configuration file.")
+	fs.StringVar(&configFile, "c", _EMPTY_, "Configuration file.")
+	fs.StringVar(&configFile, "config", _EMPTY_, "Configuration file.")
 	fs.BoolVar(&opts.CheckConfig, "t", false, "Check configuration and exit.")
-	fs.StringVar(&signal, "sl", "", "Send signal to nats-server process (stop, quit, reopen, reload).")
-	fs.StringVar(&signal, "signal", "", "Send signal to nats-server process (stop, quit, reopen, reload).")
-	fs.StringVar(&opts.PidFile, "P", "", "File to store process pid.")
-	fs.StringVar(&opts.PidFile, "pid", "", "File to store process pid.")
-	fs.StringVar(&opts.PortsFileDir, "ports_file_dir", "", "Creates a ports file in the specified directory (<executable_name>_<pid>.ports).")
-	fs.StringVar(&opts.LogFile, "l", "", "File to store logging output.")
-	fs.StringVar(&opts.LogFile, "log", "", "File to store logging output.")
+	fs.StringVar(&signal, "sl", _EMPTY_, "Send signal to nats-server process (stop, quit, reopen, reload).")
+	fs.StringVar(&signal, "signal", _EMPTY_, "Send signal to nats-server process (stop, quit, reopen, reload).")
+	fs.StringVar(&opts.PidFile, "P", _EMPTY_, "File to store process pid.")
+	fs.StringVar(&opts.PidFile, "pid", _EMPTY_, "File to store process pid.")
+	fs.StringVar(&opts.PortsFileDir, "ports_file_dir", _EMPTY_, "Creates a ports file in the specified directory (<executable_name>_<pid>.ports).")
+	fs.StringVar(&opts.LogFile, "l", _EMPTY_, "File to store logging output.")
+	fs.StringVar(&opts.LogFile, "log", _EMPTY_, "File to store logging output.")
 	fs.Int64Var(&opts.LogSizeLimit, "log_size_limit", 0, "Logfile size limit being auto-rotated")
 	fs.BoolVar(&opts.Syslog, "s", false, "Enable syslog as log method.")
 	fs.BoolVar(&opts.Syslog, "syslog", false, "Enable syslog as log method.")
-	fs.StringVar(&opts.RemoteSyslog, "r", "", "Syslog server addr (udp://127.0.0.1:514).")
-	fs.StringVar(&opts.RemoteSyslog, "remote_syslog", "", "Syslog server addr (udp://127.0.0.1:514).")
+	fs.StringVar(&opts.RemoteSyslog, "r", _EMPTY_, "Syslog server addr (udp://127.0.0.1:514).")
+	fs.StringVar(&opts.RemoteSyslog, "remote_syslog", _EMPTY_, "Syslog server addr (udp://127.0.0.1:514).")
 	fs.BoolVar(&showVersion, "version", false, "Print version information.")
 	fs.BoolVar(&showVersion, "v", false, "Print version information.")
 	fs.IntVar(&opts.ProfPort, "profile", 0, "Profiling HTTP port.")
-	fs.StringVar(&opts.RoutesStr, "routes", "", "Routes to actively solicit a connection.")
-	fs.StringVar(&opts.Cluster.ListenStr, "cluster", "", "Cluster url from which members can solicit routes.")
-	fs.StringVar(&opts.Cluster.ListenStr, "cluster_listen", "", "Cluster url from which members can solicit routes.")
-	fs.StringVar(&opts.Cluster.Advertise, "cluster_advertise", "", "Cluster URL to advertise to other servers.")
+	fs.StringVar(&opts.RoutesStr, "routes", _EMPTY_, "Routes to actively solicit a connection.")
+	fs.StringVar(&opts.Cluster.ListenStr, "cluster", _EMPTY_, "Cluster url from which members can solicit routes.")
+	fs.StringVar(&opts.Cluster.ListenStr, "cluster_listen", _EMPTY_, "Cluster url from which members can solicit routes.")
+	fs.StringVar(&opts.Cluster.Advertise, "cluster_advertise", _EMPTY_, "Cluster URL to advertise to other servers.")
 	fs.BoolVar(&opts.Cluster.NoAdvertise, "no_advertise", false, "Advertise known cluster IPs to clients.")
 	fs.IntVar(&opts.Cluster.ConnectRetries, "connect_retries", 0, "For implicit routes, number of connect retries.")
-	fs.StringVar(&opts.Cluster.Name, "cluster_name", "", "Cluster Name, if not set one will be dynamically generated.")
+	fs.StringVar(&opts.Cluster.Name, "cluster_name", _EMPTY_, "Cluster Name, if not set one will be dynamically generated.")
 	fs.BoolVar(&showTLSHelp, "help_tls", false, "TLS help.")
 	fs.BoolVar(&opts.TLS, "tls", false, "Enable TLS.")
 	fs.BoolVar(&opts.TLSVerify, "tlsverify", false, "Enable TLS with client verification.")
-	fs.StringVar(&opts.TLSCert, "tlscert", "", "Server certificate file.")
-	fs.StringVar(&opts.TLSKey, "tlskey", "", "Private key for server certificate.")
-	fs.StringVar(&opts.TLSCaCert, "tlscacert", "", "Client certificate CA for verification.")
+	fs.StringVar(&opts.TLSCert, "tlscert", _EMPTY_, "Server certificate file.")
+	fs.StringVar(&opts.TLSKey, "tlskey", _EMPTY_, "Private key for server certificate.")
+	fs.StringVar(&opts.TLSCaCert, "tlscacert", _EMPTY_, "Client certificate CA for verification.")
 	fs.IntVar(&opts.MaxTracedMsgLen, "max_traced_msg_len", 0, "Maximum printable length for traced messages. 0 for unlimited.")
 	fs.BoolVar(&opts.JetStream, "js", false, "Enable JetStream.")
 	fs.BoolVar(&opts.JetStream, "jetstream", false, "Enable JetStream.")
-	fs.StringVar(&opts.StoreDir, "sd", "", "Storage directory.")
-	fs.StringVar(&opts.StoreDir, "store_dir", "", "Storage directory.")
+	fs.StringVar(&opts.StoreDir, "sd", _EMPTY_, "Storage directory.")
+	fs.StringVar(&opts.StoreDir, "store_dir", _EMPTY_, "Storage directory.")
 
 	// The flags definition above set "default" values to some of the options.
 	// Calling Parse() here will override the default options with any value
@@ -4852,7 +4852,7 @@ func ConfigureOptions(fs *flag.FlagSet, args []string, printVersion, printHelp,
 				flagErr = overrideCluster(opts)
 			case "routes":
 				// Keep in mind that the flag has updated opts.RoutesStr at this point.
-				if opts.RoutesStr == "" {
+				if opts.RoutesStr == _EMPTY_ {
 					// Set routes array to nil since routes string is empty
 					opts.Routes = nil
 					return
@@ -4877,7 +4877,7 @@ func ConfigureOptions(fs *flag.FlagSet, args []string, printVersion, printHelp,
 	// If we don't have cluster defined in the configuration
 	// file and no cluster listen string override, but we do
 	// have a routes override, we need to report misconfiguration.
-	if opts.RoutesStr != "" && opts.Cluster.ListenStr == "" && opts.Cluster.Host == "" && opts.Cluster.Port == 0 {
+	if opts.RoutesStr != _EMPTY_ && opts.Cluster.ListenStr == _EMPTY_ && opts.Cluster.Host == _EMPTY_ && opts.Cluster.Port == 0 {
 		return nil, errors.New("solicited routes require cluster capabilities, e.g. --cluster")
 	}
 
@@ -4897,10 +4897,10 @@ func normalizeBasePath(p string) string {
 
 // overrideTLS is called when at least "-tls=true" has been set.
 func overrideTLS(opts *Options) error {
-	if opts.TLSCert == "" {
+	if opts.TLSCert == _EMPTY_ {
 		return errors.New("TLS Server certificate must be present and valid")
 	}
-	if opts.TLSKey == "" {
+	if opts.TLSKey == _EMPTY_ {
 		return errors.New("TLS Server private key must be present and valid")
 	}
 
@@ -4920,7 +4920,7 @@ func overrideTLS(opts *Options) error {
 // has explicitly be set in the command line. If it is set to empty string, it will
 // clear the Cluster options.
 func overrideCluster(opts *Options) error {
-	if opts.Cluster.ListenStr == "" {
+	if opts.Cluster.ListenStr == _EMPTY_ {
 		// This one is enough to disable clustering.
 		opts.Cluster.Port = 0
 		return nil
@@ -4963,8 +4963,8 @@ func overrideCluster(opts *Options) error {
 	} else {
 		// Since we override from flag and there is no user/pwd, make
 		// sure we clear what we may have gotten from config file.
-		opts.Cluster.Username = ""
-		opts.Cluster.Password = ""
+		opts.Cluster.Username = _EMPTY_
+		opts.Cluster.Password = _EMPTY_
 	}
 
 	return nil
@@ -5004,9 +5004,9 @@ func homeDir() (string, error) {
 		userProfile := os.Getenv("USERPROFILE")
 
 		home := filepath.Join(homeDrive, homePath)
-		if homeDrive == "" || homePath == "" {
-			if userProfile == "" {
-				return "", errors.New("nats: failed to get home dir, require %HOMEDRIVE% and %HOMEPATH% or %USERPROFILE%")
+		if homeDrive == _EMPTY_ || homePath == _EMPTY_ {
+			if userProfile == _EMPTY_ {
+				return _EMPTY_, errors.New("nats: failed to get home dir, require %HOMEDRIVE% and %HOMEPATH% or %USERPROFILE%")
 			}
 			home = userProfile
 		}
@@ -5015,8 +5015,8 @@ func homeDir() (string, error) {
 	}
 
 	home := os.Getenv("HOME")
-	if home == "" {
-		return "", errors.New("failed to get home dir, require $HOME")
+	if home == _EMPTY_ {
+		return _EMPTY_, errors.New("failed to get home dir, require $HOME")
 	}
 	return home, nil
 }
@@ -5030,7 +5030,7 @@ func expandPath(p string) (string, error) {
 
 	home, err := homeDir()
 	if err != nil {
-		return "", err
+		return _EMPTY_, err
 	}
 
 	return filepath.Join(home, p[1:]), nil


Test patch:
diff --git a/server/auth_test.go b/server/auth_test.go
index 28d6d753..145cab06 100644
--- a/server/auth_test.go
+++ b/server/auth_test.go
@@ -294,19 +294,26 @@ func TestUserConnectionDeadline(t *testing.T) {
 
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
 
-	nc, err := nats.Connect(s.ClientURL(), nats.UserInfo("valid", ""), nats.NoReconnect(), nats.ErrorHandler(func(nc *nats.Conn, _ *nats.Subscription, err error) {
-		dcerr = err
-		cancel()
-	}))
+	nc, err := nats.Connect(
+		s.ClientURL(),
+		nats.UserInfo("valid", _EMPTY_),
+		nats.NoReconnect(),
+		nats.ErrorHandler(func(nc *nats.Conn, _ *nats.Subscription, err error) {
+			dcerr = err
+			cancel()
+		}))
 	if err != nil {
 		t.Fatalf("Expected client to connect, got: %s", err)
 	}
 
 	<-ctx.Done()
 
-	if nc.IsConnected() {
-		t.Fatalf("Expected to be disconnected")
-	}
+	checkFor(t, 2*time.Second, 100*time.Millisecond, func() error {
+		if nc.IsConnected() {
+			return fmt.Errorf("Expected to be disconnected")
+		}
+		return nil
+	})
 
 	if dcerr == nil || dcerr.Error() != "nats: authentication expired" {
 		t.Fatalf("Expected a auth expired error: got: %v", dcerr)
diff --git a/server/client_test.go b/server/client_test.go
index 60c43a23..0199fb3a 100644
--- a/server/client_test.go
+++ b/server/client_test.go
@@ -2605,3 +2605,79 @@ func TestClientAuthRequiredNoAuthUser(t *testing.T) {
 		t.Fatalf("Expected AuthRequired to be false due to 'no_auth_user'")
 	}
 }
+
+func TestClientUserInfoReq(t *testing.T) {
+	conf := createConfFile(t, []byte(`
+		listen: 127.0.0.1:-1
+		PERMS = {
+			publish = { allow: "$SYS.REQ.>", deny: "$SYS.REQ.ACCOUNT.>" }
+			subscribe = "_INBOX.>"
+			allow_responses: true
+		}
+		accounts: {
+			A: { users: [ { user: dlc, password: pass, permissions: $PERMS } ] }
+			$SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] }
+		}
+		no_auth_user: dlc
+	`))
+	defer removeFile(t, conf)
+
+	s, _ := RunServerWithConfig(conf)
+	defer s.Shutdown()
+
+	nc, err := nats.Connect(s.ClientURL())
+	require_NoError(t, err)
+	defer nc.Close()
+
+	resp, err := nc.Request("$SYS.REQ.USER.INFO", nil, time.Second)
+	require_NoError(t, err)
+
+	response := ServerAPIResponse{Data: &UserInfo{}}
+	err = json.Unmarshal(resp.Data, &response)
+	require_NoError(t, err)
+
+	userInfo := response.Data.(*UserInfo)
+
+	dlc := &UserInfo{
+		UserID:  "dlc",
+		Account: "A",
+		Permissions: &Permissions{
+			Publish: &SubjectPermission{
+				Allow: []string{"$SYS.REQ.>"},
+				Deny:  []string{"$SYS.REQ.ACCOUNT.>"},
+			},
+			Subscribe: &SubjectPermission{
+				Allow: []string{"_INBOX.>"},
+			},
+			Response: &ResponsePermission{
+				MaxMsgs: DEFAULT_ALLOW_RESPONSE_MAX_MSGS,
+				Expires: DEFAULT_ALLOW_RESPONSE_EXPIRATION,
+			},
+		},
+	}
+	if !reflect.DeepEqual(dlc, userInfo) {
+		t.Fatalf("User info for %q did not match", "dlc")
+	}
+
+	// Make sure system users work ok too.
+	nc, err = nats.Connect(s.ClientURL(), nats.UserInfo("admin", "s3cr3t!"))
+	require_NoError(t, err)
+	defer nc.Close()
+
+	resp, err = nc.Request("$SYS.REQ.USER.INFO", nil, time.Second)
+	require_NoError(t, err)
+
+	response = ServerAPIResponse{Data: &UserInfo{}}
+	err = json.Unmarshal(resp.Data, &response)
+	require_NoError(t, err)
+
+	userInfo = response.Data.(*UserInfo)
+
+	admin := &UserInfo{
+		UserID:  "admin",
+		Account: "$SYS",
+	}
+	if !reflect.DeepEqual(admin, userInfo) {
+		t.Fatalf("User info for %q did not match", "admin")
+	}
+}
diff --git a/server/events_test.go b/server/events_test.go
index 61ce61b2..09c7ee3f 100644
--- a/server/events_test.go
+++ b/server/events_test.go
@@ -1268,7 +1268,7 @@ func TestAccountReqMonitoring(t *testing.T) {
 	// query SUBSZ for account
 	resp, err := ncSys.Request(subsz, nil, time.Second)
 	require_NoError(t, err)
-	require_Contains(t, string(resp.Data), `"num_subscriptions":4,`)
+	require_Contains(t, string(resp.Data), `"num_subscriptions":5,`)
 	// create a subscription
 	sub, err := nc.Subscribe("foo", func(msg *nats.Msg) {})
 	require_NoError(t, err)
@@ -1278,7 +1278,7 @@ func TestAccountReqMonitoring(t *testing.T) {
 	// query SUBSZ for account
 	resp, err = ncSys.Request(subsz, nil, time.Second)
 	require_NoError(t, err)
-	require_Contains(t, string(resp.Data), `"num_subscriptions":5,`, `"subject":"foo"`)
+	require_Contains(t, string(resp.Data), `"num_subscriptions":6,`, `"subject":"foo"`)
 	// query connections for account
 	resp, err = ncSys.Request(connz, nil, time.Second)
 	require_NoError(t, err)
@@ -1418,7 +1418,7 @@ func TestAccountReqInfo(t *testing.T) {
 		t.Fatalf("Unmarshalling failed: %v", err)
 	} else if len(info.Exports) != 1 {
 		t.Fatalf("Unexpected value: %v", info.Exports)
-	} else if len(info.Imports) != 3 {
+	} else if len(info.Imports) != 4 {
 		t.Fatalf("Unexpected value: %+v", info.Imports)
 	} else if info.Exports[0].Subject != "req.*" {
 		t.Fatalf("Unexpected value: %v", info.Exports)
@@ -1426,7 +1426,7 @@ func TestAccountReqInfo(t *testing.T) {
 		t.Fatalf("Unexpected value: %v", info.Exports)
 	} else if info.Exports[0].ResponseType != jwt.ResponseTypeSingleton {
 		t.Fatalf("Unexpected value: %v", info.Exports)
-	} else if info.SubCnt != 3 {
+	} else if info.SubCnt != 4 {
 		t.Fatalf("Unexpected value: %v", info.SubCnt)
 	} else {
 		checkCommon(&info, &srv, pub1, ajwt1)
@@ -1439,7 +1439,7 @@ func TestAccountReqInfo(t *testing.T) {
 		t.Fatalf("Unmarshalling failed: %v", err)
 	} else if len(info.Exports) != 0 {
 		t.Fatalf("Unexpected value: %v", info.Exports)
-	} else if len(info.Imports) != 4 {
+	} else if len(info.Imports) != 5 {
 		t.Fatalf("Unexpected value: %+v", info.Imports)
 	}
 	// Here we need to find our import
@@ -1457,7 +1457,7 @@ func TestAccountReqInfo(t *testing.T) {
 		t.Fatalf("Unexpected value: %+v", si)
 	} else if si.Account != pub1 {
 		t.Fatalf("Unexpected value: %+v", si)
-	} else if info.SubCnt != 4 {
+	} else if info.SubCnt != 5 {
 		t.Fatalf("Unexpected value: %+v", si)
 	} else {
 		checkCommon(&info, &srv, pub2, ajwt2)
@@ -1664,7 +1664,7 @@ func TestSystemAccountWithGateways(t *testing.T) {
 
 	// If this tests fails with wrong number after 10 seconds we may have
 	// added a new inititial subscription for the eventing system.
-	checkExpectedSubs(t, 47, sa)
+	checkExpectedSubs(t, 50, sa)
 
 	// Create a client on B and see if we receive the event
 	urlb := fmt.Sprintf("nats://%s:%d", ob.Host, ob.Port)
diff --git a/server/jwt_test.go b/server/jwt_test.go
index e00d3248..2845f7f3 100644
--- a/server/jwt_test.go
+++ b/server/jwt_test.go
@@ -3992,7 +3992,7 @@ func TestJWTTimeExpiration(t *testing.T) {
 		t.Run("simple expiration "+l, func(t *testing.T) {
 			start := time.Now()
 			creds := createUserWithLimit(t, kp, doNotExpire, func(j *jwt.UserPermissionLimits) {
-				if l == "" {
+				if l == _EMPTY_ {
 					j.Times = []jwt.TimeRange{newTimeRange(start, validFor)}
 				} else {
 					loc, err := time.LoadLocation(l)
@@ -6699,3 +6699,50 @@ func TestServerOperatorModeNoAuthRequired(t *testing.T) {
 
 	require_True(t, nc.AuthRequired())
 }
+
+func TestServerOperatorModeUserInfoExpiration(t *testing.T) {
+	_, spub := createKey(t)
+	sysClaim := jwt.NewAccountClaims(spub)
+	sysClaim.Name = "$SYS"
+	sysJwt, err := sysClaim.Encode(oKp)
+	require_NoError(t, err)
+
+	akp, apub := createKey(t)
+	accClaim := jwt.NewAccountClaims(apub)
+	accClaim.Name = "TEST"
+	accJwt, err := accClaim.Encode(oKp)
+	require_NoError(t, err)
+
+	conf := createConfFile(t, []byte(fmt.Sprintf(`
+		listen: 127.0.0.1:-1
+		operator: %s
+		system_account: %s
+		resolver: MEM
+		resolver_preload: {
+			%s: %s
+			%s: %s
+		}
+    `, ojwt, spub, apub, accJwt, spub, sysJwt)))
+	defer removeFile(t, conf)
+
+	s, _ := RunServerWithConfig(conf)
+	defer s.Shutdown()
+
+	expires := time.Now().Add(time.Minute)
+	creds := createUserWithLimit(t, akp, expires, func(j *jwt.UserPermissionLimits) {})
+
+	nc := natsConnect(t, s.ClientURL(), nats.UserCredentials(creds))
+	defer nc.Close()
+
+	resp, err := nc.Request("$SYS.REQ.USER.INFO", nil, time.Second)
+	require_NoError(t, err)
+	now := time.Now()
+
+	response := ServerAPIResponse{Data: &UserInfo{}}
+	err = json.Unmarshal(resp.Data, &response)
+	require_NoError(t, err)
+
+	userInfo := response.Data.(*UserInfo)
+	require_True(t, userInfo.Expires != 0)
+	require_True(t, expires.Sub(now).Truncate(time.Second) == userInfo.Expires)
+}
diff --git a/server/leafnode_test.go b/server/leafnode_test.go
index bb81d85b..78a77a35 100644
--- a/server/leafnode_test.go
+++ b/server/leafnode_test.go
@@ -1319,7 +1319,7 @@ func TestLeafNodePermissions(t *testing.T) {
 	// Create a sub on ">" on LN1
 	subAll := natsSubSync(t, nc1, ">")
 	// this should be registered in LN2 (there is 1 sub for LN1 $LDS subject) + SYS IMPORTS
-	checkSubs(ln2.globalAccount(), 10)
+	checkSubs(ln2.globalAccount(), 12)
 
 	// Check deny export clause from messages published from LN2
 	for _, test := range []struct {
@@ -1346,7 +1346,7 @@ func TestLeafNodePermissions(t *testing.T) {
 
 	subAll.Unsubscribe()
 	// Goes down by 1.
-	checkSubs(ln2.globalAccount(), 9)
+	checkSubs(ln2.globalAccount(), 11)
 
 	// We used to make sure we would not do subscriptions however that
 	// was incorrect. We need to check publishes, not the subscriptions.
@@ -1371,7 +1371,7 @@ func TestLeafNodePermissions(t *testing.T) {
 	} {
 		t.Run(test.name, func(t *testing.T) {
 			sub := natsSubSync(t, nc2, test.subSubject)
-			checkSubs(ln2.globalAccount(), 10)
+			checkSubs(ln2.globalAccount(), 12)
 
 			if !test.ok {
 				nc1.Publish(test.pubSubject, []byte("msg"))
@@ -1379,12 +1379,12 @@ func TestLeafNodePermissions(t *testing.T) {
 					t.Fatalf("Did not expect to get the message")
 				}
 			} else {
-				checkSubs(ln1.globalAccount(), 9)
+				checkSubs(ln1.globalAccount(), 11)
 				nc1.Publish(test.pubSubject, []byte("msg"))
 				natsNexMsg(t, sub, time.Second)
 			}
 			sub.Unsubscribe()
-			checkSubs(ln1.globalAccount(), 8)
+			checkSubs(ln1.globalAccount(), 10)
 		})
 	}
 }
@@ -1505,8 +1505,8 @@ func TestLeafNodeExportPermissionsNotForSpecialSubs(t *testing.T) {
 	// The deny is totally restrictive, but make sure that we still accept the $LDS, $GR and _GR_ go from LN1.
 	checkFor(t, time.Second, 15*time.Millisecond, func() error {
 		// We should have registered the 3 subs from the accepting leafnode.
-		if n := ln2.globalAccount().TotalSubs(); n != 8 {
-			return fmt.Errorf("Expected %d subs, got %v", 8, n)
+		if n := ln2.globalAccount().TotalSubs(); n != 9 {
+			return fmt.Errorf("Expected %d subs, got %v", 9, n)
 		}
 		return nil
 	})
@@ -3474,8 +3474,8 @@ func TestLeafNodeRouteSubWithOrigin(t *testing.T) {
 	r1.Shutdown()
 	checkFor(t, time.Second, 15*time.Millisecond, func() error {
 		acc := l2.GlobalAccount()
-		if n := acc.TotalSubs(); n != 4 {
-			return fmt.Errorf("Account %q should have 3 subs, got %v", acc.GetName(), n)
+		if n := acc.TotalSubs(); n != 5 {
+			return fmt.Errorf("Account %q should have 5 subs, got %v", acc.GetName(), n)
 		}
 		return nil
 	})
diff --git a/server/monitor_test.go b/server/monitor_test.go
index 0e666bb6..e0f5a97d 100644
--- a/server/monitor_test.go
+++ b/server/monitor_test.go
@@ -3914,8 +3914,8 @@ func TestMonitorLeafz(t *testing.T) {
 				t.Fatalf("RTT not tracked?")
 			}
 			// LDS should be only one.
-			if ln.NumSubs != 4 || len(ln.Subs) != 4 {
-				t.Fatalf("Expected 3 subs, got %v (%v)", ln.NumSubs, ln.Subs)
+			if ln.NumSubs != 5 || len(ln.Subs) != 5 {
+				t.Fatalf("Expected 5 subs, got %v (%v)", ln.NumSubs, ln.Subs)
 			}
 		}
 	}
@@ -3933,7 +3933,7 @@ func TestMonitorAccountz(t *testing.T) {
 	body = string(readBody(t, fmt.Sprintf("http://127.0.0.1:%d%s?acc=$SYS", s.MonitorAddr().Port, AccountzPath)))
 	require_Contains(t, body, `"account_detail": {`)
 	require_Contains(t, body, `"account_name": "$SYS",`)
-	require_Contains(t, body, `"subscriptions": 42,`)
+	require_Contains(t, body, `"subscriptions": 44,`)
 	require_Contains(t, body, `"is_system": true,`)
 	require_Contains(t, body, `"system_account": "$SYS"`)
 
diff --git a/test/leafnode_test.go b/test/leafnode_test.go
index abd84fd7..e81fddb8 100644
--- a/test/leafnode_test.go
+++ b/test/leafnode_test.go
@@ -199,7 +199,7 @@ func setupLeaf(t *testing.T, lc net.Conn, expectedSubs int) (sendFun, expectFun)
 	send, expect := setupConn(t, lc)
 	// A loop detection subscription is sent, so consume this here, along
 	// with the ones that caller expect on setup.
-	expectNumberOfProtos(t, expect, lsubRe, expectedSubs, infoRe, pingRe)
+	expectNumberOfProtos(t, expect, lsubRe, expectedSubs, infoStartRe, pingRe)
 	return send, expect
 }
 
@@ -439,7 +439,7 @@ func TestLeafNodeAndRoutes(t *testing.T) {
 	lc := createLeafConn(t, optsA.LeafNode.Host, optsA.LeafNode.Port)
 	defer lc.Close()
 
-	leafSend, leafExpect := setupLeaf(t, lc, 5)
+	leafSend, leafExpect := setupLeaf(t, lc, 6)
 	leafSend("PING\r\n")
 	leafExpect(pongRe)
 
@@ -833,7 +833,7 @@ func TestLeafNodeGatewaySendsSystemEvent(t *testing.T) {
 	defer lc.Close()
 
 	// This is for our global responses since we are setting up GWs above.
-	leafSend, leafExpect := setupLeaf(t, lc, 7)
+	leafSend, leafExpect := setupLeaf(t, lc, 8)
 	leafSend("PING\r\n")
 	leafExpect(pongRe)
 
@@ -874,17 +874,18 @@ func TestLeafNodeGatewayInterestPropagation(t *testing.T) {
 	lc := createLeafConn(t, opts.LeafNode.Host, opts.LeafNode.Port)
 	defer lc.Close()
 	_, leafExpect := setupConn(t, lc)
-	buf := leafExpect(infoRe)
-	buf = infoRe.ReplaceAll(buf, []byte(nil))
+	buf := leafExpect(infoStartRe)
+	buf = infoStartRe.ReplaceAll(buf, []byte(nil))
+
 	foundFoo := false
-	for count := 0; count != 9; {
+	for count := 0; count < 10; {
 		// skip first time if we still have data (buf from above may already have some left)
 		if count != 0 || len(buf) == 0 {
 			buf = append(buf, leafExpect(anyRe)...)
 		}
 		count += len(lsubRe.FindAllSubmatch(buf, -1))
-		if count > 9 {
-			t.Fatalf("Expected %v matches, got %v (buf=%s)", 8, count, buf)
+		if count > 10 {
+			t.Fatalf("Expected %v matches, got %v (buf=%s)", 10, count, buf)
 		}
 		if strings.Contains(string(buf), "foo") {
 			foundFoo = true
@@ -936,7 +937,7 @@ func TestLeafNodeWithRouteAndGateway(t *testing.T) {
 	defer lc.Close()
 
 	// This is for our global responses since we are setting up GWs above.
-	leafSend, leafExpect := setupLeaf(t, lc, 7)
+	leafSend, leafExpect := setupLeaf(t, lc, 8)
 	leafSend("PING\r\n")
 	leafExpect(pongRe)
 
@@ -995,7 +996,7 @@ func TestLeafNodeWithGatewaysAndStaggeredStart(t *testing.T) {
 	lc := createLeafConn(t, opts.LeafNode.Host, opts.LeafNode.Port)
 	defer lc.Close()
 
-	leafSend, leafExpect := setupLeaf(t, lc, 7)
+	leafSend, leafExpect := setupLeaf(t, lc, 8)
 	leafSend("PING\r\n")
 	leafExpect(pongRe)
 
@@ -1035,7 +1036,7 @@ func TestLeafNodeWithGatewaysServerRestart(t *testing.T) {
 	lc := createLeafConn(t, opts.LeafNode.Host, opts.LeafNode.Port)
 	defer lc.Close()
 
-	leafSend, leafExpect := setupLeaf(t, lc, 7)
+	leafSend, leafExpect := setupLeaf(t, lc, 8)
 	leafSend("PING\r\n")
 	leafExpect(pongRe)
 
@@ -1069,7 +1070,7 @@ func TestLeafNodeWithGatewaysServerRestart(t *testing.T) {
 	lc = createLeafConn(t, opts.LeafNode.Host, opts.LeafNode.Port)
 	defer lc.Close()
 
-	_, leafExpect = setupLeaf(t, lc, 7)
+	_, leafExpect = setupLeaf(t, lc, 8)
 
 	// Now wait on GW solicit to fire
 	time.Sleep(500 * time.Millisecond)
@@ -2063,7 +2064,7 @@ func TestLeafNodeExportImportComplexSetup(t *testing.T) {
 
 	// Wait for the sub to propagate to s2. LDS + subject above.
 	checkFor(t, 2*time.Second, 15*time.Millisecond, func() error {
-		if acc1.RoutedSubs() != 5 {
+		if acc1.RoutedSubs() != 6 {
 			return fmt.Errorf("Still no routed subscription: %d", acc1.RoutedSubs())
 		}
 		return nil
@@ -2649,7 +2650,7 @@ func TestLeafNodeSwitchGatewayToInterestModeOnly(t *testing.T) {
 	defer lc.Close()
 
 	// This is for our global responses since we are setting up GWs above.
-	leafSend, leafExpect := setupLeaf(t, lc, 7)
+	leafSend, leafExpect := setupLeaf(t, lc, 8)
 	leafSend("PING\r\n")
 	leafExpect(pongRe)
 }
diff --git a/test/test.go b/test/test.go
index 8fe94b6a..b7342443 100644
--- a/test/test.go
+++ b/test/test.go
@@ -345,25 +345,26 @@ func sendProto(t tLogger, c net.Conn, op string) {
 }
 
 var (
-	anyRe     = regexp.MustCompile(`.*`)
-	infoRe    = regexp.MustCompile(`INFO\s+([^\r\n]+)\r\n`)
-	pingRe    = regexp.MustCompile(`^PING\r\n`)
-	pongRe    = regexp.MustCompile(`^PONG\r\n`)
-	hmsgRe    = regexp.MustCompile(`(?:(?:HMSG\s+([^\s]+)\s+([^\s]+)\s+(([^\s]+)[^\S\r\n]+)?(\d+)\s+(\d+)\s*\r\n([^\\r\\n]*?)\r\n)+?)`)
-	msgRe     = regexp.MustCompile(`(?:(?:MSG\s+([^\s]+)\s+([^\s]+)\s+(([^\s]+)[^\S\r\n]+)?(\d+)\s*\r\n([^\\r\\n]*?)\r\n)+?)`)
-	rawMsgRe  = regexp.MustCompile(`(?:(?:MSG\s+([^\s]+)\s+([^\s]+)\s+(([^\s]+)[^\S\r\n]+)?(\d+)\s*\r\n(.*?)))`)
-	okRe      = regexp.MustCompile(`\A\+OK\r\n`)
-	errRe     = regexp.MustCompile(`\A\-ERR\s+([^\r\n]+)\r\n`)
-	connectRe = regexp.MustCompile(`CONNECT\s+([^\r\n]+)\r\n`)
-	rsubRe    = regexp.MustCompile(`RS\+\s+([^\s]+)\s+([^\s]+)\s*([^\s]+)?\s*(\d+)?\r\n`)
-	runsubRe  = regexp.MustCompile(`RS\-\s+([^\s]+)\s+([^\s]+)\s*([^\s]+)?\r\n`)
-	rmsgRe    = regexp.MustCompile(`(?:(?:RMSG\s+([^\s]+)\s+([^\s]+)\s+(?:([|+]\s+([\w\s]+)|[^\s]+)[^\S\r\n]+)?(\d+)\s*\r\n([^\\r\\n]*?)\r\n)+?)`)
-	asubRe    = regexp.MustCompile(`A\+\s+([^\r\n]+)\r\n`)
-	aunsubRe  = regexp.MustCompile(`A\-\s+([^\r\n]+)\r\n`)
-	lsubRe    = regexp.MustCompile(`LS\+\s+([^\s]+)\s*([^\s]+)?\s*(\d+)?\r\n`)
-	lunsubRe  = regexp.MustCompile(`LS\-\s+([^\s]+)\s*([^\s]+)?\r\n`)
-	lmsgRe    = regexp.MustCompile(`(?:(?:LMSG\s+([^\s]+)\s+(?:([|+]\s+([\w\s]+)|[^\s]+)[^\S\r\n]+)?(\d+)\s*\r\n([^\\r\\n]*?)\r\n)+?)`)
-	rlsubRe   = regexp.MustCompile(`LS\+\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s*([^\s]+)?\s*(\d+)?\r\n`)
+	anyRe       = regexp.MustCompile(`.*`)
+	infoRe      = regexp.MustCompile(`INFO\s+([^\r\n]+)\r\n`)
+	infoStartRe = regexp.MustCompile(`^INFO\s+([^\r\n]+)\r\n`)
+	pingRe      = regexp.MustCompile(`^PING\r\n`)
+	pongRe      = regexp.MustCompile(`^PONG\r\n`)
+	hmsgRe      = regexp.MustCompile(`(?:(?:HMSG\s+([^\s]+)\s+([^\s]+)\s+(([^\s]+)[^\S\r\n]+)?(\d+)\s+(\d+)\s*\r\n([^\\r\\n]*?)\r\n)+?)`)
+	msgRe       = regexp.MustCompile(`(?:(?:MSG\s+([^\s]+)\s+([^\s]+)\s+(([^\s]+)[^\S\r\n]+)?(\d+)\s*\r\n([^\\r\\n]*?)\r\n)+?)`)
+	rawMsgRe    = regexp.MustCompile(`(?:(?:MSG\s+([^\s]+)\s+([^\s]+)\s+(([^\s]+)[^\S\r\n]+)?(\d+)\s*\r\n(.*?)))`)
+	okRe        = regexp.MustCompile(`\A\+OK\r\n`)
+	errRe       = regexp.MustCompile(`\A\-ERR\s+([^\r\n]+)\r\n`)
+	connectRe   = regexp.MustCompile(`CONNECT\s+([^\r\n]+)\r\n`)
+	rsubRe      = regexp.MustCompile(`RS\+\s+([^\s]+)\s+([^\s]+)\s*([^\s]+)?\s*(\d+)?\r\n`)
+	runsubRe    = regexp.MustCompile(`RS\-\s+([^\s]+)\s+([^\s]+)\s*([^\s]+)?\r\n`)
+	rmsgRe      = regexp.MustCompile(`(?:(?:RMSG\s+([^\s]+)\s+([^\s]+)\s+(?:([|+]\s+([\w\s]+)|[^\s]+)[^\S\r\n]+)?(\d+)\s*\r\n([^\\r\\n]*?)\r\n)+?)`)
+	asubRe      = regexp.MustCompile(`A\+\s+([^\r\n]+)\r\n`)
+	aunsubRe    = regexp.MustCompile(`A\-\s+([^\r\n]+)\r\n`)
+	lsubRe      = regexp.MustCompile(`LS\+\s+([^\s]+)\s*([^\s]+)?\s*(\d+)?\r\n`)
+	lunsubRe    = regexp.MustCompile(`LS\-\s+([^\s]+)\s*([^\s]+)?\r\n`)
+	lmsgRe      = regexp.MustCompile(`(?:(?:LMSG\s+([^\s]+)\s+(?:([|+]\s+([\w\s]+)|[^\s]+)[^\S\r\n]+)?(\d+)\s*\r\n([^\\r\\n]*?)\r\n)+?)`)
+	rlsubRe     = regexp.MustCompile(`LS\+\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s*([^\s]+)?\s*(\d+)?\r\n`)
 )
 
 const (