mirror of
https://github.com/gogrlx/nats-server.git
synced 2026-04-02 03:38:42 -07:00
[ADDED] Basic auth for leafnodes
Added a way to specify which account an accepted leafnode connection
should be bound to when using simple auth (user/password).
Singleton:
```
leafnodes {
port: ...
authorization {
user: leaf
password: secret
account: TheAccount
}
}
```
With above configuration, if a soliciting server creates a LN connection
with url: `nats://leaf:secret@host:port`, then the accepting server
will bind the leafnode connection to the account "TheAccount". This account
need to exist otherwise the connection will be rejected.
Multi:
```
leafnodes {
port: ...
authorization {
users = [
{user: leaf1, password: secret, account: account1}
{user: leaf2, password: secret, account: account2}
]
}
}
```
With the above, if a server connects using `leaf1:secret@host:port`, then
the accepting server will bind the connection to account `account1`.
If user/password (either singleton or multi) is defined, then the connecting
server MUST provide the proper credentials otherwise the connection will
be rejected.
If no user/password info is provided, it is still possible to provide the
account the connection should be associated with:
```
leafnodes {
port: ...
authorization {
account: TheAccount
}
}
```
With the above, a connection without credentials will be bound to the
account "TheAccount".
If credentials are used (jwt, nkey or other), then the server will attempt
to authenticate and if successful associate to the account for that specific
user. If the user authentication fails (wrong password, no such user, etc..)
the connection will be also rejected.
Signed-off-by: Ivan Kozlovic <ivan@synadia.com>
This commit is contained in:
224
server/auth.go
224
server/auth.go
@@ -296,23 +296,19 @@ func (s *Server) checkAuthentication(c *client) bool {
|
||||
// isClientAuthorized will check the client against the proper authorization method and data.
|
||||
// This could be nkey, token, or username/password based.
|
||||
func (s *Server) isClientAuthorized(c *client) bool {
|
||||
// Snapshot server options by hand and only grab what we really need.
|
||||
s.optsMu.RLock()
|
||||
customClientAuthentication := s.opts.CustomClientAuthentication
|
||||
authorization := s.opts.Authorization
|
||||
username := s.opts.Username
|
||||
password := s.opts.Password
|
||||
tlsMap := s.opts.TLSMap
|
||||
s.optsMu.RUnlock()
|
||||
opts := s.getOpts()
|
||||
|
||||
// Check custom auth first, then jwts, then nkeys, then
|
||||
// multiple users with TLS map if enabled, then token,
|
||||
// then single user/pass.
|
||||
if customClientAuthentication != nil {
|
||||
return customClientAuthentication.Check(c)
|
||||
if opts.CustomClientAuthentication != nil {
|
||||
return opts.CustomClientAuthentication.Check(c)
|
||||
}
|
||||
|
||||
// Grab under lock but process after.
|
||||
return s.processClientOrLeafAuthentication(c)
|
||||
}
|
||||
|
||||
func (s *Server) processClientOrLeafAuthentication(c *client) bool {
|
||||
var (
|
||||
nkey *NkeyUser
|
||||
juc *jwt.UserClaims
|
||||
@@ -320,6 +316,7 @@ func (s *Server) isClientAuthorized(c *client) bool {
|
||||
user *User
|
||||
ok bool
|
||||
err error
|
||||
opts = s.getOpts()
|
||||
)
|
||||
|
||||
s.mu.Lock()
|
||||
@@ -364,7 +361,7 @@ func (s *Server) isClientAuthorized(c *client) bool {
|
||||
}
|
||||
} else if hasUsers {
|
||||
// Check if we are tls verify and are mapping users from the client_certificate
|
||||
if tlsMap {
|
||||
if opts.TLSMap {
|
||||
var euser string
|
||||
authorized := checkClientTLSCertSubject(c, func(u string) bool {
|
||||
var ok bool
|
||||
@@ -448,7 +445,9 @@ func (s *Server) isClientAuthorized(c *client) bool {
|
||||
}
|
||||
|
||||
nkey = buildInternalNkeyUser(juc, acc)
|
||||
c.RegisterNkeyUser(nkey)
|
||||
if err := c.RegisterNkeyUser(nkey); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Generate an event if we have a system account.
|
||||
s.accountConnectEvent(c)
|
||||
@@ -481,7 +480,9 @@ func (s *Server) isClientAuthorized(c *client) bool {
|
||||
c.Debugf("Signature not verified")
|
||||
return false
|
||||
}
|
||||
c.RegisterNkeyUser(nkey)
|
||||
if err := c.RegisterNkeyUser(nkey); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -497,13 +498,21 @@ func (s *Server) isClientAuthorized(c *client) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
if authorization != "" {
|
||||
return comparePasswords(authorization, c.opts.Authorization)
|
||||
} else if username != "" {
|
||||
if username != c.opts.Username {
|
||||
return false
|
||||
if c.kind == CLIENT {
|
||||
if opts.Authorization != "" {
|
||||
return comparePasswords(opts.Authorization, c.opts.Authorization)
|
||||
} else if opts.Username != "" {
|
||||
if opts.Username != c.opts.Username {
|
||||
return false
|
||||
}
|
||||
return comparePasswords(opts.Password, c.opts.Password)
|
||||
}
|
||||
return comparePasswords(password, c.opts.Password)
|
||||
} else if c.kind == LEAF {
|
||||
// There is no required username/password to connect and
|
||||
// there was no u/p in the CONNECT or none that matches the
|
||||
// know users. Register the leaf connection with global account
|
||||
// or the one specified in config (if provided).
|
||||
return s.registerLeafWithAccount(c, opts.LeafNode.Account)
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -607,141 +616,58 @@ func (s *Server) isGatewayAuthorized(c *client) bool {
|
||||
return comparePasswords(opts.Gateway.Password, c.opts.Password)
|
||||
}
|
||||
|
||||
// isLeafNodeAuthorized will check for auth for an inbound leaf node connection.
|
||||
func (s *Server) isLeafNodeAuthorized(c *client) bool {
|
||||
// FIXME(dlc) - This is duplicated from client auth, should be able to combine
|
||||
// and not fail so bad on DRY.
|
||||
|
||||
// Grab under lock but process after.
|
||||
var (
|
||||
juc *jwt.UserClaims
|
||||
acc *Account
|
||||
user *User
|
||||
ok bool
|
||||
err error
|
||||
)
|
||||
|
||||
s.mu.Lock()
|
||||
// Check if we have trustedKeys defined in the server. If so we require a user jwt.
|
||||
if s.trustedKeys != nil {
|
||||
if c.opts.JWT == "" {
|
||||
s.mu.Unlock()
|
||||
c.Debugf("Authentication requires a user JWT")
|
||||
return false
|
||||
}
|
||||
// So we have a valid user jwt here.
|
||||
juc, err = jwt.DecodeUserClaims(c.opts.JWT)
|
||||
func (s *Server) registerLeafWithAccount(c *client, account string) bool {
|
||||
var err error
|
||||
acc := s.globalAccount()
|
||||
if account != _EMPTY_ {
|
||||
acc, err = s.lookupAccount(account)
|
||||
if err != nil {
|
||||
s.mu.Unlock()
|
||||
c.Debugf("User JWT not valid: %v", err)
|
||||
s.Errorf("authentication of user %q failed, unable to lookup account %q: %v",
|
||||
c.opts.Username, account, err)
|
||||
return false
|
||||
}
|
||||
vr := jwt.CreateValidationResults()
|
||||
juc.Validate(vr)
|
||||
if vr.IsBlocking(true) {
|
||||
s.mu.Unlock()
|
||||
c.Debugf("User JWT no longer valid: %+v", vr)
|
||||
return false
|
||||
}
|
||||
} else if s.users != nil {
|
||||
if c.opts.Username != "" {
|
||||
user, ok = s.users[c.opts.Username]
|
||||
if !ok {
|
||||
s.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
// If we have a jwt and a userClaim, make sure we have the Account, etc associated.
|
||||
// We need to look up the account. This will use an account resolver if one is present.
|
||||
if juc != nil {
|
||||
issuer := juc.Issuer
|
||||
if juc.IssuerAccount != "" {
|
||||
issuer = juc.IssuerAccount
|
||||
}
|
||||
if acc, err = s.LookupAccount(issuer); acc == nil {
|
||||
c.Debugf("Account JWT lookup error: %v", err)
|
||||
return false
|
||||
}
|
||||
if !s.isTrustedIssuer(acc.Issuer) {
|
||||
c.Debugf("Account JWT not signed by trusted operator")
|
||||
return false
|
||||
}
|
||||
if juc.IssuerAccount != "" && !acc.hasIssuer(juc.Issuer) {
|
||||
c.Debugf("User JWT issuer is not known")
|
||||
return false
|
||||
}
|
||||
if acc.IsExpired() {
|
||||
c.Debugf("Account JWT has expired")
|
||||
return false
|
||||
}
|
||||
// Verify the signature against the nonce.
|
||||
if c.opts.Sig == "" {
|
||||
c.Debugf("Signature missing")
|
||||
return false
|
||||
}
|
||||
sig, err := base64.RawURLEncoding.DecodeString(c.opts.Sig)
|
||||
if err != nil {
|
||||
// Allow fallback to normal base64.
|
||||
sig, err = base64.StdEncoding.DecodeString(c.opts.Sig)
|
||||
if err != nil {
|
||||
c.Debugf("Signature not valid base64")
|
||||
return false
|
||||
}
|
||||
}
|
||||
pub, err := nkeys.FromPublicKey(juc.Subject)
|
||||
if err != nil {
|
||||
c.Debugf("User nkey not valid: %v", err)
|
||||
return false
|
||||
}
|
||||
if err := pub.Verify(c.nonce, sig); err != nil {
|
||||
c.Debugf("Signature not verified")
|
||||
return false
|
||||
}
|
||||
|
||||
nkey := buildInternalNkeyUser(juc, acc)
|
||||
if err := c.RegisterNkeyUser(nkey); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Generate an event if we have a system account.
|
||||
s.accountConnectEvent(c)
|
||||
|
||||
// Check if we need to set an auth timer if the user jwt expires.
|
||||
c.checkExpiration(juc.Claims())
|
||||
return true
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
ok = comparePasswords(user.Password, c.opts.Password)
|
||||
// If we are authorized, register the user which will properly setup any permissions
|
||||
// for pub/sub authorizations.
|
||||
if ok {
|
||||
c.RegisterUser(user)
|
||||
// Generate an event if we have a system account and this is not the $G account.
|
||||
s.accountConnectEvent(c)
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// FIXME(dlc) - Add ability to support remote account bindings via
|
||||
// other auth like user or nkey and tlsMapping.
|
||||
|
||||
// For now this means we are binding the leafnode to the global account.
|
||||
c.registerWithAccount(s.globalAccount())
|
||||
|
||||
// Snapshot server options.
|
||||
opts := s.getOpts()
|
||||
|
||||
if opts.LeafNode.Username == "" {
|
||||
return true
|
||||
}
|
||||
if opts.LeafNode.Username != c.opts.Username {
|
||||
if err = c.registerWithAccount(acc); err != nil {
|
||||
return false
|
||||
}
|
||||
return comparePasswords(opts.LeafNode.Password, c.opts.Password)
|
||||
return true
|
||||
}
|
||||
|
||||
// isLeafNodeAuthorized will check for auth for an inbound leaf node connection.
|
||||
func (s *Server) isLeafNodeAuthorized(c *client) bool {
|
||||
opts := s.getOpts()
|
||||
|
||||
isAuthorized := func(username, password, account string) bool {
|
||||
if username != c.opts.Username {
|
||||
return false
|
||||
}
|
||||
if !comparePasswords(password, c.opts.Password) {
|
||||
return false
|
||||
}
|
||||
return s.registerLeafWithAccount(c, account)
|
||||
}
|
||||
|
||||
// If leafnodes config has an authorization{} stanza, this takes precedence.
|
||||
// The user in CONNECT mutch match. We will bind to the account associated
|
||||
// with that user (from the leafnode's authorization{} config).
|
||||
if opts.LeafNode.Username != _EMPTY_ {
|
||||
return isAuthorized(opts.LeafNode.Username, opts.LeafNode.Password, opts.LeafNode.Account)
|
||||
} else if len(opts.LeafNode.Users) > 0 {
|
||||
// This is expected to be a very small array.
|
||||
for _, u := range opts.LeafNode.Users {
|
||||
if u.Username == c.opts.Username {
|
||||
return isAuthorized(u.Username, u.Password, u.Account.Name)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// We are here if we accept leafnode connections without any credential.
|
||||
|
||||
// Still, if the CONNECT has some user info, we will bind to the
|
||||
// user's account or to the specified default account (if provided)
|
||||
// or to the global account.
|
||||
return s.processClientOrLeafAuthentication(c)
|
||||
}
|
||||
|
||||
// Support for bcrypt stored passwords and tokens.
|
||||
|
||||
@@ -1236,6 +1236,37 @@ func TestConfigCheck(t *testing.T) {
|
||||
errorLine: 14,
|
||||
errorPos: 71,
|
||||
},
|
||||
{
|
||||
name: "mixing single and multi users in leafnode authorization",
|
||||
config: `
|
||||
leafnodes {
|
||||
authorization {
|
||||
user: user1
|
||||
password: pwd
|
||||
users = [{user: user2, password: pwd}]
|
||||
}
|
||||
}
|
||||
`,
|
||||
err: errors.New("can not have a single user/pass and a users array"),
|
||||
errorLine: 3,
|
||||
errorPos: 20,
|
||||
},
|
||||
{
|
||||
name: "dulpicate usernames in leafnode authorization",
|
||||
config: `
|
||||
leafnodes {
|
||||
authorization {
|
||||
users = [
|
||||
{user: user, password: pwd}
|
||||
{user: user, password: pwd}
|
||||
]
|
||||
}
|
||||
}
|
||||
`,
|
||||
err: errors.New(`duplicate user "user" detected in leafnode authorization`),
|
||||
errorLine: 3,
|
||||
errorPos: 20,
|
||||
},
|
||||
}
|
||||
|
||||
checkConfig := func(config string) error {
|
||||
|
||||
@@ -88,6 +88,9 @@ func (s *Server) remoteLeafNodeStillValid(remote *leafNodeCfg) bool {
|
||||
|
||||
// Ensure that leafnode is properly configured.
|
||||
func validateLeafNode(o *Options) error {
|
||||
if err := validateLeafNodeAuthOptions(o); err != nil {
|
||||
return err
|
||||
}
|
||||
if o.LeafNode.Port == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -102,6 +105,26 @@ func validateLeafNode(o *Options) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Used to validate user names in LeafNode configuration.
|
||||
// - rejects mix of single and multiple users.
|
||||
// - rejects duplicate user names.
|
||||
func validateLeafNodeAuthOptions(o *Options) error {
|
||||
if len(o.LeafNode.Users) == 0 {
|
||||
return nil
|
||||
}
|
||||
if o.LeafNode.Username != _EMPTY_ {
|
||||
return fmt.Errorf("can not have a single user/pass and a users array")
|
||||
}
|
||||
users := map[string]struct{}{}
|
||||
for _, u := range o.LeafNode.Users {
|
||||
if _, exists := users[u.Username]; exists {
|
||||
return fmt.Errorf("duplicate user %q detected in leafnode authorization", u.Username)
|
||||
}
|
||||
users[u.Username] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) reConnectToRemoteLeafNode(remote *leafNodeCfg) {
|
||||
delay := s.getOpts().LeafNode.ReconnectInterval
|
||||
select {
|
||||
|
||||
@@ -22,6 +22,8 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
type captureLeafNodeRandomIPLogger struct {
|
||||
@@ -492,3 +494,233 @@ func TestLeafNodeRTT(t *testing.T) {
|
||||
checkRTT(t, sa)
|
||||
checkRTT(t, sb)
|
||||
}
|
||||
|
||||
func TestLeafNodeValidateAuthOptions(t *testing.T) {
|
||||
opts := DefaultOptions()
|
||||
opts.LeafNode.Username = "user1"
|
||||
opts.LeafNode.Password = "pwd"
|
||||
opts.LeafNode.Users = []*User{&User{Username: "user", Password: "pwd"}}
|
||||
if _, err := NewServer(opts); err == nil || !strings.Contains(err.Error(),
|
||||
"can not have a single user/pass and a users array") {
|
||||
t.Fatalf("Expected error about mixing single/multi users, got %v", err)
|
||||
}
|
||||
|
||||
// Check duplicate user names
|
||||
opts.LeafNode.Username = _EMPTY_
|
||||
opts.LeafNode.Password = _EMPTY_
|
||||
opts.LeafNode.Users = append(opts.LeafNode.Users, &User{Username: "user", Password: "pwd"})
|
||||
if _, err := NewServer(opts); err == nil || !strings.Contains(err.Error(), "duplicate user") {
|
||||
t.Fatalf("Expected error about duplicate user, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeafNodeBasicAuthSingleton(t *testing.T) {
|
||||
opts := DefaultOptions()
|
||||
opts.LeafNode.Port = -1
|
||||
opts.LeafNode.Account = "unknown"
|
||||
if s, err := NewServer(opts); err == nil || !strings.Contains(err.Error(), "cannot find") {
|
||||
if s != nil {
|
||||
s.Shutdown()
|
||||
}
|
||||
t.Fatalf("Expected error about account not found, got %v", err)
|
||||
}
|
||||
|
||||
template := `
|
||||
port: -1
|
||||
accounts: {
|
||||
ACC1: { users = [{user: "user1", password: "user1"}] }
|
||||
ACC2: { users = [{user: "user2", password: "user2"}] }
|
||||
}
|
||||
leafnodes: {
|
||||
port: -1
|
||||
authorization {
|
||||
%s
|
||||
account: "ACC1"
|
||||
}
|
||||
}
|
||||
`
|
||||
for iter, test := range []struct {
|
||||
name string
|
||||
userSpec string
|
||||
lnURLCreds string
|
||||
shouldFail bool
|
||||
}{
|
||||
{"no user creds required and no user so binds to ACC1", "", "", false},
|
||||
{"no user creds required and pick user2 associated to ACC2", "", "user2:user2@", false},
|
||||
{"no user creds required and unknown user should fail", "", "unknown:user@", true},
|
||||
{"user creds required so binds to ACC1", "user: \"ln\"\npass: \"pwd\"", "ln:pwd@", false},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
conf := createConfFile(t, []byte(fmt.Sprintf(template, test.userSpec)))
|
||||
defer os.Remove(conf)
|
||||
s1, o1 := RunServerWithConfig(conf)
|
||||
defer s1.Shutdown()
|
||||
|
||||
// Create a sub on "foo" for account ACC1 (user user1), which is the one
|
||||
// bound to the accepted LN connection.
|
||||
ncACC1 := natsConnect(t, fmt.Sprintf("nats://user1:user1@%s:%d", o1.Host, o1.Port))
|
||||
defer ncACC1.Close()
|
||||
sub1 := natsSubSync(t, ncACC1, "foo")
|
||||
natsFlush(t, ncACC1)
|
||||
|
||||
// Create a sub on "foo" for account ACC2 (user user2). This one should
|
||||
// not receive any message.
|
||||
ncACC2 := natsConnect(t, fmt.Sprintf("nats://user2:user2@%s:%d", o1.Host, o1.Port))
|
||||
defer ncACC2.Close()
|
||||
sub2 := natsSubSync(t, ncACC2, "foo")
|
||||
natsFlush(t, ncACC2)
|
||||
|
||||
conf = createConfFile(t, []byte(fmt.Sprintf(`
|
||||
port: -1
|
||||
leafnodes: {
|
||||
remotes = [ { url: "nats-leaf://%s%s:%d" } ]
|
||||
}
|
||||
`, test.lnURLCreds, o1.LeafNode.Host, o1.LeafNode.Port)))
|
||||
defer os.Remove(conf)
|
||||
s2, _ := RunServerWithConfig(conf)
|
||||
defer s2.Shutdown()
|
||||
|
||||
if test.shouldFail {
|
||||
// Wait a bit and ensure that there is no leaf node connection
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
||||
if n := s1.NumLeafNodes(); n != 0 {
|
||||
return fmt.Errorf("Expected no leafnode connection, got %v", n)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
checkLeafNodeConnected(t, s2)
|
||||
|
||||
nc := natsConnect(t, s2.ClientURL())
|
||||
defer nc.Close()
|
||||
natsPub(t, nc, "foo", []byte("hello"))
|
||||
// If url contains known user, even when there is no credentials
|
||||
// required, the connection will be bound to the user's account.
|
||||
if iter == 1 {
|
||||
// Should not receive on "ACC1", but should on "ACC2"
|
||||
if _, err := sub1.NextMsg(100 * time.Millisecond); err != nats.ErrTimeout {
|
||||
t.Fatalf("Expected timeout error, got %v", err)
|
||||
}
|
||||
natsNexMsg(t, sub2, time.Second)
|
||||
} else {
|
||||
// Should receive on "ACC1"...
|
||||
natsNexMsg(t, sub1, time.Second)
|
||||
// but not received on "ACC2" since leafnode bound to account "ACC1".
|
||||
if _, err := sub2.NextMsg(100 * time.Millisecond); err != nats.ErrTimeout {
|
||||
t.Fatalf("Expected timeout error, got %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeafNodeBasicAuthMultiple(t *testing.T) {
|
||||
conf := createConfFile(t, []byte(`
|
||||
port: -1
|
||||
accounts: {
|
||||
S1ACC1: { users = [{user: "user1", password: "user1"}] }
|
||||
S1ACC2: { users = [{user: "user2", password: "user2"}] }
|
||||
}
|
||||
leafnodes: {
|
||||
port: -1
|
||||
authorization {
|
||||
users = [
|
||||
{user: "ln1", password: "ln1", account: "S1ACC1"}
|
||||
{user: "ln2", password: "ln2", account: "S1ACC2"}
|
||||
]
|
||||
}
|
||||
}
|
||||
`))
|
||||
defer os.Remove(conf)
|
||||
s1, o1 := RunServerWithConfig(conf)
|
||||
defer s1.Shutdown()
|
||||
|
||||
// Make sure that we reject a LN connection if user does not match
|
||||
conf = createConfFile(t, []byte(fmt.Sprintf(`
|
||||
port: -1
|
||||
leafnodes: {
|
||||
remotes = [{url: "nats-leaf://wron:user@%s:%d"}]
|
||||
}
|
||||
`, o1.LeafNode.Host, o1.LeafNode.Port)))
|
||||
defer os.Remove(conf)
|
||||
s2, _ := RunServerWithConfig(conf)
|
||||
defer s2.Shutdown()
|
||||
// Give a chance for s2 to attempt to connect and make sure that s1
|
||||
// did not register a LN connection.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if n := s1.NumLeafNodes(); n != 0 {
|
||||
t.Fatalf("Expected no leafnode connection, got %v", n)
|
||||
}
|
||||
s2.Shutdown()
|
||||
|
||||
ncACC1 := natsConnect(t, fmt.Sprintf("nats://user1:user1@%s:%d", o1.Host, o1.Port))
|
||||
defer ncACC1.Close()
|
||||
sub1 := natsSubSync(t, ncACC1, "foo")
|
||||
natsFlush(t, ncACC1)
|
||||
|
||||
ncACC2 := natsConnect(t, fmt.Sprintf("nats://user2:user2@%s:%d", o1.Host, o1.Port))
|
||||
defer ncACC2.Close()
|
||||
sub2 := natsSubSync(t, ncACC2, "foo")
|
||||
natsFlush(t, ncACC2)
|
||||
|
||||
// We will start s2 with 2 LN connections that should bind local account S2ACC1
|
||||
// to account S1ACC1 and S2ACC2 to account S1ACC2 on s1.
|
||||
conf = createConfFile(t, []byte(fmt.Sprintf(`
|
||||
port: -1
|
||||
accounts {
|
||||
S2ACC1 { users = [{user: "user1", password: "user1"}] }
|
||||
S2ACC2 { users = [{user: "user2", password: "user2"}] }
|
||||
}
|
||||
leafnodes: {
|
||||
remotes = [
|
||||
{
|
||||
url: "nats-leaf://ln1:ln1@%s:%d"
|
||||
account: "S2ACC1"
|
||||
}
|
||||
{
|
||||
url: "nats-leaf://ln2:ln2@%s:%d"
|
||||
account: "S2ACC2"
|
||||
}
|
||||
]
|
||||
}
|
||||
`, o1.LeafNode.Host, o1.LeafNode.Port, o1.LeafNode.Host, o1.LeafNode.Port)))
|
||||
defer os.Remove(conf)
|
||||
s2, o2 := RunServerWithConfig(conf)
|
||||
defer s2.Shutdown()
|
||||
|
||||
checkFor(t, 5*time.Second, 100*time.Millisecond, func() error {
|
||||
if nln := s2.NumLeafNodes(); nln != 2 {
|
||||
return fmt.Errorf("Expected 2 connected leafnodes for server %q, got %d", s2.ID(), nln)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Create a user connection on s2 that binds to S2ACC1 (use user1).
|
||||
nc1 := natsConnect(t, fmt.Sprintf("nats://user1:user1@%s:%d", o2.Host, o2.Port))
|
||||
defer nc1.Close()
|
||||
|
||||
// Create an user connection on s2 that binds to S2ACC2 (use user2).
|
||||
nc2 := natsConnect(t, fmt.Sprintf("nats://user2:user2@%s:%d", o2.Host, o2.Port))
|
||||
defer nc2.Close()
|
||||
|
||||
// Now if a message is published from nc1, sub1 should receive it since
|
||||
// their account are bound together.
|
||||
natsPub(t, nc1, "foo", []byte("hello"))
|
||||
natsNexMsg(t, sub1, time.Second)
|
||||
// But sub2 should not receive it since different account.
|
||||
if _, err := sub2.NextMsg(100 * time.Millisecond); err != nats.ErrTimeout {
|
||||
t.Fatalf("Expected timeout error, got %v", err)
|
||||
}
|
||||
|
||||
// Now use nc2 (S2ACC2) to publish
|
||||
natsPub(t, nc2, "foo", []byte("hello"))
|
||||
// Expect sub2 to receive and sub1 not to.
|
||||
natsNexMsg(t, sub2, time.Second)
|
||||
if _, err := sub1.NextMsg(100 * time.Millisecond); err != nats.ErrTimeout {
|
||||
t.Fatalf("Expected timeout error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
126
server/opts.go
126
server/opts.go
@@ -107,6 +107,8 @@ type LeafNodeOpts struct {
|
||||
Port int `json:"port,omitempty"`
|
||||
Username string `json:"-"`
|
||||
Password string `json:"-"`
|
||||
Account string `json:"-"`
|
||||
Users []*User `json:"-"`
|
||||
AuthTimeout float64 `json:"auth_timeout,omitempty"`
|
||||
TLSConfig *tls.Config `json:"-"`
|
||||
TLSTimeout float64 `json:"tls_timeout,omitempty"`
|
||||
@@ -288,6 +290,7 @@ type authorization struct {
|
||||
user string
|
||||
pass string
|
||||
token string
|
||||
acc string
|
||||
// Multiple Nkeys/Users
|
||||
nkeys []*NkeyUser
|
||||
users []*User
|
||||
@@ -1039,20 +1042,21 @@ func parseLeafNodes(v interface{}, opts *Options, errors *[]error, warnings *[]e
|
||||
case "host", "net":
|
||||
opts.LeafNode.Host = mv.(string)
|
||||
case "authorization":
|
||||
auth, err := parseAuthorization(tk, opts, errors, warnings)
|
||||
auth, err := parseLeafAuthorization(tk, errors, warnings)
|
||||
if err != nil {
|
||||
*errors = append(*errors, err)
|
||||
continue
|
||||
}
|
||||
if auth.users != nil {
|
||||
err := &configErr{tk, fmt.Sprintf("Leafnode authorization does not allow multiple users")}
|
||||
*errors = append(*errors, err)
|
||||
continue
|
||||
}
|
||||
opts.LeafNode.Username = auth.user
|
||||
opts.LeafNode.Password = auth.pass
|
||||
opts.LeafNode.AuthTimeout = auth.timeout
|
||||
|
||||
opts.LeafNode.Account = auth.acc
|
||||
opts.LeafNode.Users = auth.users
|
||||
// Validate user info config for leafnode authorization
|
||||
if err := validateLeafNodeAuthOptions(opts); err != nil {
|
||||
*errors = append(*errors, &configErr{tk, err.Error()})
|
||||
continue
|
||||
}
|
||||
case "remotes":
|
||||
// Parse the remote options here.
|
||||
remotes, err := parseRemoteLeafNodes(mv, errors, warnings)
|
||||
@@ -1095,6 +1099,114 @@ func parseLeafNodes(v interface{}, opts *Options, errors *[]error, warnings *[]e
|
||||
return nil
|
||||
}
|
||||
|
||||
// This is the authorization parser adapter for the leafnode's
|
||||
// authorization config.
|
||||
func parseLeafAuthorization(v interface{}, errors *[]error, warnings *[]error) (*authorization, error) {
|
||||
var (
|
||||
am map[string]interface{}
|
||||
tk token
|
||||
auth = &authorization{}
|
||||
)
|
||||
_, v = unwrapValue(v)
|
||||
am = v.(map[string]interface{})
|
||||
for mk, mv := range am {
|
||||
tk, mv = unwrapValue(mv)
|
||||
switch strings.ToLower(mk) {
|
||||
case "user", "username":
|
||||
auth.user = mv.(string)
|
||||
case "pass", "password":
|
||||
auth.pass = mv.(string)
|
||||
case "timeout":
|
||||
at := float64(1)
|
||||
switch mv := mv.(type) {
|
||||
case int64:
|
||||
at = float64(mv)
|
||||
case float64:
|
||||
at = mv
|
||||
}
|
||||
auth.timeout = at
|
||||
case "users":
|
||||
users, err := parseLeafUsers(tk, errors, warnings)
|
||||
if err != nil {
|
||||
*errors = append(*errors, err)
|
||||
continue
|
||||
}
|
||||
auth.users = users
|
||||
case "account":
|
||||
auth.acc = mv.(string)
|
||||
default:
|
||||
if !tk.IsUsedVariable() {
|
||||
err := &unknownConfigFieldErr{
|
||||
field: mk,
|
||||
configErr: configErr{
|
||||
token: tk,
|
||||
},
|
||||
}
|
||||
*errors = append(*errors, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
// This is a trimmed down version of parseUsers that is adapted
|
||||
// for the users possibly defined in the authorization{} section
|
||||
// of leafnodes {}.
|
||||
func parseLeafUsers(mv interface{}, errors *[]error, warnings *[]error) ([]*User, error) {
|
||||
var (
|
||||
tk token
|
||||
users = []*User{}
|
||||
)
|
||||
tk, mv = unwrapValue(mv)
|
||||
// Make sure we have an array
|
||||
uv, ok := mv.([]interface{})
|
||||
if !ok {
|
||||
return nil, &configErr{tk, fmt.Sprintf("Expected users field to be an array, got %v", mv)}
|
||||
}
|
||||
for _, u := range uv {
|
||||
tk, u = unwrapValue(u)
|
||||
// Check its a map/struct
|
||||
um, ok := u.(map[string]interface{})
|
||||
if !ok {
|
||||
err := &configErr{tk, fmt.Sprintf("Expected user entry to be a map/struct, got %v", u)}
|
||||
*errors = append(*errors, err)
|
||||
continue
|
||||
}
|
||||
user := &User{}
|
||||
for k, v := range um {
|
||||
tk, v = unwrapValue(v)
|
||||
switch strings.ToLower(k) {
|
||||
case "user", "username":
|
||||
user.Username = v.(string)
|
||||
case "pass", "password":
|
||||
user.Password = v.(string)
|
||||
case "account":
|
||||
// We really want to save just the account name here, but
|
||||
// the User object is *Account. So we create an account object
|
||||
// but it won't be registered anywhere. The server will just
|
||||
// use opts.LeafNode.Users[].Account.Name. Alternatively
|
||||
// we need to create internal objects to store u/p and account
|
||||
// name and have a server structure to hold that.
|
||||
user.Account = NewAccount(v.(string))
|
||||
default:
|
||||
if !tk.IsUsedVariable() {
|
||||
err := &unknownConfigFieldErr{
|
||||
field: k,
|
||||
configErr: configErr{
|
||||
token: tk,
|
||||
},
|
||||
}
|
||||
*errors = append(*errors, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func parseRemoteLeafNodes(v interface{}, errors *[]error, warnings *[]error) ([]*RemoteLeafOpts, error) {
|
||||
tk, v := unwrapValue(v)
|
||||
ra, ok := v.([]interface{})
|
||||
|
||||
@@ -305,9 +305,29 @@ func NewServer(opts *Options) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// In local config mode, if remote leafs are configured,
|
||||
// make sure that if they reference local accounts, they exist.
|
||||
if len(opts.TrustedOperators) == 0 && len(opts.LeafNode.Remotes) > 0 {
|
||||
// In local config mode, check that leafnode configuration
|
||||
// refers to account that exist.
|
||||
if len(opts.TrustedOperators) == 0 {
|
||||
checkAccountExists := func(accName string) error {
|
||||
if accName == _EMPTY_ {
|
||||
return nil
|
||||
}
|
||||
if _, ok := s.accounts.Load(accName); !ok {
|
||||
return fmt.Errorf("cannot find account %q specified in leafnode authorization", accName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := checkAccountExists(opts.LeafNode.Account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, lu := range opts.LeafNode.Users {
|
||||
if lu.Account == nil {
|
||||
continue
|
||||
}
|
||||
if err := checkAccountExists(lu.Account.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, r := range opts.LeafNode.Remotes {
|
||||
if r.LocalAccount == _EMPTY_ {
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user