From f4f3d3baf13e024f2f13635c9efd33ff73cb60a2 Mon Sep 17 00:00:00 2001 From: Derek Collison Date: Sun, 2 Dec 2018 20:34:33 -0800 Subject: [PATCH] Updates for operator based configurations. Added update to parse and load operator JWTs. Changed to add in signing keys from operator JWT to list of trusted keys. Added URL account resolver. Added account claim updates by system messages. Signed-off-by: Derek Collison --- main.go | 9 +- server/accounts.go | 74 ++++++++++- server/auth.go | 6 +- server/client.go | 2 +- server/const.go | 4 +- server/errors.go | 3 + server/events.go | 25 ++++ server/events_test.go | 96 +++++++++++++- server/jwt.go | 99 ++++++++++++++ server/jwt_test.go | 48 ++++++- server/nkey.go | 2 +- server/opts.go | 76 +++++++++-- server/server.go | 167 +++++++++++++++++------ server/trust_test.go | 50 +++---- test/configs/nkeys/op.jwt | 3 + test/configs/nkeys/sigkeys.txt | 20 +++ test/configs/nkeys/test.seed | 7 + test/configs/operator.conf | 16 +++ test/operator_test.go | 235 +++++++++++++++++++++++++++++++++ test/test.go | 6 +- vendor/manifest | 8 -- 21 files changed, 847 insertions(+), 109 deletions(-) create mode 100644 server/jwt.go create mode 100644 test/configs/nkeys/op.jwt create mode 100644 test/configs/nkeys/sigkeys.txt create mode 100644 test/configs/nkeys/test.seed create mode 100644 test/configs/operator.conf create mode 100644 test/operator_test.go diff --git a/main.go b/main.go index ac78e829..6f7b0071 100644 --- a/main.go +++ b/main.go @@ -87,14 +87,17 @@ func main() { fs.Usage, server.PrintTLSHelpAndDie) if err != nil { - server.PrintAndDie(err.Error()) + server.PrintAndDie(fmt.Sprintf("ERR: %s", err)) } else if opts.CheckConfig { - fmt.Fprintf(os.Stderr, "configuration file %s test is successful\n", opts.ConfigFile) + fmt.Fprintf(os.Stderr, "configuration file %s is valid\n", opts.ConfigFile) os.Exit(0) } // Create the server with appropriate options. - s := server.New(opts) + s, err := server.NewServer(opts) + if err != nil { + server.PrintAndDie(fmt.Sprintf("ERR: %s", err)) + } // Configure the logger based on the flags s.ConfigureLogger() diff --git a/server/accounts.go b/server/accounts.go index 4f5922bf..5c2ca665 100644 --- a/server/accounts.go +++ b/server/accounts.go @@ -14,6 +14,7 @@ package server import ( + "fmt" "io/ioutil" "net/http" "net/url" @@ -135,6 +136,12 @@ func (a *Account) MaxTotalClientsReached() bool { return false } +func (a *Account) MaxActiveClients() int { + a.mu.RLock() + defer a.mu.RUnlock() + return a.mconns +} + // RoutedSubs returns how many subjects we would send across a route when first // connected or expressing interest. Local client subs. func (a *Account) RoutedSubs() int { @@ -777,6 +784,13 @@ func (s *Server) SetAccountResolver(ar AccountResolver) { s.mu.Unlock() } +// AccountResolver returns the registered account resolver. +func (s *Server) AccountResolver() AccountResolver { + s.mu.Lock() + defer s.mu.Unlock() + return s.accResolver +} + // updateAccountClaims will update and existing account with new claims. // This will replace any exports or imports previously defined. func (s *Server) updateAccountClaims(a *Account, ac *jwt.AccountClaims) { @@ -925,17 +939,67 @@ func buildInternalNkeyUser(uc *jwt.UserClaims, acc *Account) *NkeyUser { // AccountResolver interface. This is to fetch Account JWTs by public nkeys type AccountResolver interface { - Fetch(pub string) (string, error) + Fetch(name string) (string, error) + Store(name, jwt string) error } // Mostly for testing. type MemAccResolver struct { - sync.Map + sm sync.Map } -func (m *MemAccResolver) Fetch(pub string) (string, error) { - if j, ok := m.Load(pub); ok { +func (m *MemAccResolver) Fetch(name string) (string, error) { + if j, ok := m.sm.Load(name); ok { return j.(string), nil } - return "", ErrMissingAccount + return _EMPTY_, ErrMissingAccount +} + +func (m *MemAccResolver) Store(name, jwt string) error { + m.sm.Store(name, jwt) + return nil +} + +// +type URLAccResolver struct { + url string + c *http.Client +} + +// NewURLAccResolver returns a new resolver for the given base URL. +func NewURLAccResolver(url string) (*URLAccResolver, error) { + if !strings.HasSuffix(url, "/") { + url += "/" + } + // Do basic test to see if anyone is home. + // FIXME(dlc) - Make timeout configurable post MVP. + ur := &URLAccResolver{ + url: url, + c: &http.Client{Timeout: 2 * time.Second}, + } + if _, err := ur.Fetch(""); err != nil { + return nil, err + } + return ur, nil +} + +// Fetch will fetch the account jwt claims from the base url, appending the +// account name onto the end. +func (ur *URLAccResolver) Fetch(name string) (string, error) { + url := ur.url + name + resp, err := ur.c.Get(url) + if err != nil || resp == nil || resp.StatusCode != http.StatusOK { + return _EMPTY_, fmt.Errorf("URL(%q) returned error", url) + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return _EMPTY_, err + } + return string(body), nil +} + +// Store is not implemented for URL Resolver. +func (ur *URLAccResolver) Store(name, jwt string) error { + return fmt.Errorf("Store operation not supported on URL Resolver") } diff --git a/server/auth.go b/server/auth.go index 95eb4b4d..5da5c651 100644 --- a/server/auth.go +++ b/server/auth.go @@ -200,7 +200,7 @@ func (s *Server) configureAuthorization() { // This just checks and sets up the user map if we have multiple users. if opts.CustomClientAuthentication != nil { s.info.AuthRequired = true - } else if len(s.trustedNkeys) > 0 { + } else if len(s.trustedKeys) > 0 { s.info.AuthRequired = true } else if opts.Nkeys != nil || opts.Users != nil { // Support both at the same time. @@ -275,8 +275,8 @@ func (s *Server) isClientAuthorized(c *client) bool { return true } - // Check if we have trustedNkeys defined in the server. If so we require a user jwt. - if s.trustedNkeys != nil { + // 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") diff --git a/server/client.go b/server/client.go index 9289aa26..e4f4be8f 100644 --- a/server/client.go +++ b/server/client.go @@ -1106,7 +1106,7 @@ func (c *client) authViolation() { var hasTrustedNkeys, hasNkeys, hasUsers bool if s := c.srv; s != nil { s.mu.Lock() - hasTrustedNkeys = len(s.trustedNkeys) > 0 + hasTrustedNkeys = len(s.trustedKeys) > 0 hasNkeys = s.nkeys != nil hasUsers = s.users != nil s.mu.Unlock() diff --git a/server/const.go b/server/const.go index bcd9e558..72393c48 100644 --- a/server/const.go +++ b/server/const.go @@ -34,8 +34,8 @@ const ( var ( // gitCommit injected at build gitCommit string - // trustedNkeys is a whitespace separated array of trusted operator's public nkeys. - trustedNkeys string + // trustedKeys is a whitespace separated array of trusted operator's public nkeys. + trustedKeys string ) const ( diff --git a/server/errors.go b/server/errors.go index ec0b812b..e9d56261 100644 --- a/server/errors.go +++ b/server/errors.go @@ -75,6 +75,9 @@ var ( // ErrAccountValidation is returned when an account has failed validation. ErrAccountValidation = errors.New("Account Validation Failed") + // ErrAccountExpired is returned when an account has expired. + ErrAccountExpired = errors.New("Account Expired") + // ErrNoAccountResolver is returned when we attempt an update but do not have an account resolver. ErrNoAccountResolver = errors.New("Account Resolver Missing") diff --git a/server/events.go b/server/events.go index 0b83dd10..25570a49 100644 --- a/server/events.go +++ b/server/events.go @@ -28,10 +28,13 @@ const ( disconnectEventSubj = "$SYS.ACCOUNT.%s.DISCONNECT" accConnsEventSubj = "$SYS.SERVER.ACCOUNT.%s.CONNS" accConnsReqSubj = "$SYS.REQ.ACCOUNT.%s.CONNS" + accUpdateEventSubj = "$SYS.ACCOUNT.%s.CLAIMS.UPDATE" connsRespSubj = "$SYS._INBOX_.%s" shutdownEventSubj = "$SYS.SERVER.%s.SHUTDOWN" shutdownEventTokens = 4 serverSubjectIndex = 2 + accUpdateTokens = 5 + accUpdateAccIndex = 2 ) // Used to send and receive messages from inside the server. @@ -279,6 +282,28 @@ func (s *Server) initEventTracking() { if _, err := s.sysSubscribe(subject, s.remoteServerShutdown); err != nil { s.Errorf("Error setting up internal tracking: %v", err) } + // Listen for account claims updates. + subject = fmt.Sprintf(accUpdateEventSubj, "*") + if _, err := s.sysSubscribe(subject, s.accountClaimUpdate); err != nil { + s.Errorf("Error setting up internal tracking: %v", err) + } + +} + +// accountClaimUpdate will receive claim updates for accounts. +func (s *Server) accountClaimUpdate(sub *subscription, subject, reply string, msg []byte) { + s.mu.Lock() + defer s.mu.Unlock() + if !s.eventsEnabled() { + return + } + toks := strings.Split(subject, tsep) + if len(toks) < accUpdateTokens { + s.Debugf("Received account claims update on bad subject %q", subject) + return + } + accName := toks[accUpdateAccIndex] + s.updateAccountWithClaimJWT(s.accounts[accName], string(msg)) } // processRemoteServerShutdown will update any affected accounts. diff --git a/server/events_test.go b/server/events_test.go index e70ba9c0..c89c01a9 100644 --- a/server/events_test.go +++ b/server/events_test.go @@ -17,6 +17,9 @@ import ( "bytes" "encoding/json" "fmt" + "net/http" + "net/http/httptest" + "os" "strings" "testing" "time" @@ -60,8 +63,8 @@ func runTrustedServer(t *testing.T) (*Server, *Options) { opts := DefaultOptions() kp, _ := nkeys.FromSeed(oSeed) pub, _ := kp.PublicKey() - opts.TrustedNkeys = []string{pub} - opts.accResolver = &MemAccResolver{} + opts.TrustedKeys = []string{pub} + opts.AccountResolver = &MemAccResolver{} s := RunServer(opts) return s, opts } @@ -87,8 +90,8 @@ func runTrustedCluster(t *testing.T) (*Server, *Options, *Server, *Options) { optsA := DefaultOptions() optsA.Cluster.Host = "127.0.0.1" - optsA.TrustedNkeys = []string{pub} - optsA.accResolver = mr + optsA.TrustedKeys = []string{pub} + optsA.AccountResolver = mr optsA.SystemAccount = apub sa := RunServer(optsA) @@ -405,7 +408,7 @@ func TestSystemAccountConnectionLimitsServersStaggered(t *testing.T) { } // Restart server B. - optsB.accResolver = sa.accResolver + optsB.AccountResolver = sa.accResolver optsB.SystemAccount = sa.systemAccount().Name sb = RunServer(optsB) defer sb.Shutdown() @@ -533,3 +536,86 @@ func TestSystemAccountConnectionLimitsServerShutdownForced(t *testing.T) { return nil }) } + +func TestSystemAccountFromConfig(t *testing.T) { + kp, _ := nkeys.FromSeed(oSeed) + opub, _ := kp.PublicKey() + akp, _ := nkeys.CreateAccount() + apub, _ := akp.PublicKey() + nac := jwt.NewAccountClaims(apub) + ajwt, err := nac.Encode(kp) + if err != nil { + t.Fatalf("Error generating account JWT: %v", err) + } + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(ajwt)) + })) + defer ts.Close() + + confTemplate := ` + listen: -1 + trusted: %s + system_account: %s + resolver: URL("%s/ngs/v1/accounts/jwt/") + ` + + conf := createConfFile(t, []byte(fmt.Sprintf(confTemplate, opub, apub, ts.URL))) + defer os.Remove(conf) + + s, _ := RunServerWithConfig(conf) + defer s.Shutdown() + + if acc := s.SystemAccount(); acc == nil || acc.Name != apub { + t.Fatalf("System Account not properly set") + } +} + +func TestAccountClaimsUpdates(t *testing.T) { + s, opts := runTrustedServer(t) + defer s.Shutdown() + + sacc, sakp := createAccount(s) + s.setSystemAccount(sacc) + + // Let's create an account account. + okp, _ := nkeys.FromSeed(oSeed) + akp, _ := nkeys.CreateAccount() + pub, _ := akp.PublicKey() + nac := jwt.NewAccountClaims(pub) + nac.Limits.Conn = 4 + ajwt, _ := nac.Encode(okp) + + addAccountToMemResolver(s, pub, ajwt) + + acc := s.LookupAccount(pub) + if acc.MaxActiveClients() != 4 { + t.Fatalf("Expected to see a limit of 4 connections") + } + + // Simulate a systems publisher so we can do an account claims update. + url := fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port) + nc, err := nats.Connect(url, createUserCreds(t, s, sakp)) + if err != nil { + t.Fatalf("Error on connect: %v", err) + } + defer nc.Close() + + // Update the account + nac = jwt.NewAccountClaims(pub) + nac.Limits.Conn = 8 + issAt := time.Now().Add(-30 * time.Second).Unix() + nac.IssuedAt = issAt + expires := time.Now().Add(2 * time.Second).Unix() + nac.Expires = expires + ajwt, _ = nac.Encode(okp) + + // Publish to the system update subject. + claimUpdateSubj := fmt.Sprintf(accUpdateEventSubj, pub) + nc.Publish(claimUpdateSubj, []byte(ajwt)) + nc.Flush() + + acc = s.LookupAccount(pub) + if acc.MaxActiveClients() != 8 { + t.Fatalf("Account was not updated") + } +} diff --git a/server/jwt.go b/server/jwt.go new file mode 100644 index 00000000..c747c023 --- /dev/null +++ b/server/jwt.go @@ -0,0 +1,99 @@ +// Copyright 2018 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "fmt" + "io/ioutil" + "regexp" + + "github.com/nats-io/jwt" + "github.com/nats-io/nkeys" +) + +var nscDecoratedRe = regexp.MustCompile(`\s*(?:(?:[-]{3,}[^\n]*[-]{3,}\n)(.+)(?:\n\s*[-]{3,}[^\n]*[-]{3,}[\n]*))`) + +// readOperatorJWT +func readOperatorJWT(jwtfile string) (*jwt.OperatorClaims, error) { + contents, err := ioutil.ReadFile(jwtfile) + if err != nil { + return nil, err + } + defer wipeSlice(contents) + + var claim string + items := nscDecoratedRe.FindAllSubmatch(contents, -1) + if len(items) == 0 { + claim = string(contents) + } else { + // First result should be the JWT. + // We copy here so that if the file contained a seed file too we wipe appropriately. + raw := items[0][1] + tmp := make([]byte, len(raw)) + copy(tmp, raw) + claim = string(tmp) + } + opc, err := jwt.DecodeOperatorClaims(claim) + if err != nil { + return nil, err + } + return opc, nil +} + +// Just wipe slice with 'x', for clearing contents of nkey seed file. +func wipeSlice(buf []byte) { + for i := range buf { + buf[i] = 'x' + } +} + +// validateTrustedOperators will check that we do not have conflicts with +// assigned trusted keys and trusted operators. If operators are defined we +// will expand the trusted keys in options. +func validateTrustedOperators(o *Options) error { + if len(o.TrustedOperators) == 0 { + return nil + } + if o.AllowNewAccounts { + return fmt.Errorf("operators do not allow dynamic creation of new accounts") + } + if o.AccountResolver == nil { + return fmt.Errorf("operators require an account resolver to be configured") + } + if len(o.Accounts) > 0 { + return fmt.Errorf("operators do not allow Accounts to be configured directly") + } + if len(o.Users) > 0 || len(o.Nkeys) > 0 { + return fmt.Errorf("operators do not allow users to be configured directly") + } + if len(o.TrustedOperators) > 0 && len(o.TrustedKeys) > 0 { + return fmt.Errorf("conflicting options for 'TrustedKeys' and 'TrustedOperators'") + } + // If we have operators, fill in the trusted keys. + // FIXME(dlc) - We had TrustedKeys before TrsutedOperators. The jwt.OperatorClaims + // has a DidSign(). Use that longer term. For now we can expand in place. + for _, opc := range o.TrustedOperators { + if o.TrustedKeys == nil { + o.TrustedKeys = make([]string, 0, 4) + } + o.TrustedKeys = append(o.TrustedKeys, opc.Issuer) + o.TrustedKeys = append(o.TrustedKeys, opc.SigningKeys...) + } + for _, key := range o.TrustedKeys { + if !nkeys.IsValidPublicOperatorKey(key) { + return fmt.Errorf("trusted Keys %q are required to be a valid public operator nkey", key) + } + } + return nil +} diff --git a/server/jwt_test.go b/server/jwt_test.go index 7527b67d..3187f373 100644 --- a/server/jwt_test.go +++ b/server/jwt_test.go @@ -20,6 +20,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "os" "strings" "testing" "time" @@ -29,7 +30,8 @@ import ( ) var ( - oSeed = []byte("SOAL7GTNI66CTVVNXBNQMG6V2HTDRWC3HGEP7D2OUTWNWSNYZDXWFOX4SU") + // This matches ./configs/nkeys_jwts/test.seed + oSeed = []byte("SOAFYNORQLQFJYBYNUGC5D7SH2MXMUX5BFEWWGHN3EK4VGG5TPT5DZP7QU") aSeed = []byte("SAANRM6JVDEYZTR6DXCWUSDDJHGOHAFITXEQBSEZSY5JENTDVRZ6WNKTTY") ) @@ -37,7 +39,7 @@ func opTrustBasicSetup() *Server { kp, _ := nkeys.FromSeed(oSeed) pub, _ := kp.PublicKey() opts := defaultServerOptions - opts.TrustedNkeys = []string{pub} + opts.TrustedKeys = []string{pub} s, _, _, _ := rawSetup(opts) return s } @@ -49,9 +51,9 @@ func buildMemAccResolver(s *Server) { s.mu.Unlock() } -func addAccountToMemResolver(s *Server, pub, jwt string) { +func addAccountToMemResolver(s *Server, pub, jwtclaim string) { s.mu.Lock() - s.accResolver.(*MemAccResolver).Store(pub, jwt) + s.accResolver.Store(pub, jwtclaim) s.mu.Unlock() } @@ -159,7 +161,7 @@ func TestJWTUserBadTrusted(t *testing.T) { } // Now place bad trusted key s.mu.Lock() - s.trustedNkeys = []string{"bad"} + s.trustedKeys = []string{"bad"} s.mu.Unlock() buildMemAccResolver(s) @@ -1376,3 +1378,39 @@ func TestJWTAccountServiceImportExpires(t *testing.T) { parseAsyncB("PING\r\n") expectPong(crb) } + +func TestAccountURLResolver(t *testing.T) { + kp, _ := nkeys.FromSeed(oSeed) + akp, _ := nkeys.CreateAccount() + apub, _ := akp.PublicKey() + nac := jwt.NewAccountClaims(apub) + ajwt, err := nac.Encode(kp) + if err != nil { + t.Fatalf("Error generating account JWT: %v", err) + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(ajwt)) + })) + defer ts.Close() + + confTemplate := ` + listen: -1 + resolver: URL("%s/ngs/v1/accounts/jwt/") + ` + conf := createConfFile(t, []byte(fmt.Sprintf(confTemplate, ts.URL))) + defer os.Remove(conf) + + s, opts := RunServerWithConfig(conf) + pub, _ := kp.PublicKey() + opts.TrustedKeys = []string{pub} + defer s.Shutdown() + + acc := s.LookupAccount(apub) + if acc == nil { + t.Fatalf("Expected to receive an account") + } + if acc.Name != apub { + t.Fatalf("Account name did not match claim key") + } +} diff --git a/server/nkey.go b/server/nkey.go index 111f4688..de671a01 100644 --- a/server/nkey.go +++ b/server/nkey.go @@ -29,7 +29,7 @@ func (s *Server) nonceRequired() bool { s.optsMu.RLock() defer s.optsMu.RUnlock() - return len(s.opts.Nkeys) > 0 || len(s.opts.TrustedNkeys) > 0 + return len(s.opts.Nkeys) > 0 || len(s.opts.TrustedKeys) > 0 } // Generate a nonce for INFO challenge. diff --git a/server/opts.go b/server/opts.go index 3a1a3d16..2c60bc08 100644 --- a/server/opts.go +++ b/server/opts.go @@ -24,11 +24,13 @@ import ( "net" "net/url" "os" + "regexp" "strconv" "strings" "time" "github.com/nats-io/gnatsd/conf" + "github.com/nats-io/jwt" "github.com/nats-io/nkeys" ) @@ -129,7 +131,11 @@ type Options struct { RQSubsSweep time.Duration `json:"-"` // Deprecated MaxClosedClients int `json:"-"` LameDuckDuration time.Duration `json:"-"` - TrustedNkeys []string `json:"-"` + + // Operating a trusted NATS server + TrustedKeys []string `json:"-"` + TrustedOperators []*jwt.OperatorClaims `json:"-"` + AccountResolver AccountResolver `json:"-"` CustomClientAuthentication Authentication `json:"-"` CustomRouterAuthentication Authentication `json:"-"` @@ -139,9 +145,6 @@ type Options struct { // private fields, used for testing gatewaysSolicitDelay time.Duration - - // Used to spin up a memory account resolver for testing. - accResolver AccountResolver } type netResolver interface { @@ -489,12 +492,67 @@ func (o *Options) ProcessConfigFile(configFile string) error { continue } o.LameDuckDuration = dur - case "trusted": + case "operator", "operators", "roots", "root", "root_operators", "root_operator": + opFiles := []string{} switch v.(type) { case string: - o.TrustedNkeys = []string{v.(string)} + opFiles = append(opFiles, v.(string)) case []string: - o.TrustedNkeys = v.([]string) + opFiles = append(opFiles, v.([]string)...) + } + // Assume for now these are file names. + // TODO(dlc) - If we try to read the file and it fails we could treat the string + // as the JWT itself. + o.TrustedOperators = make([]*jwt.OperatorClaims, 0, len(opFiles)) + for _, fname := range opFiles { + opc, err := readOperatorJWT(fname) + if err != nil { + err := &configErr{tk, fmt.Sprintf("error parsing operator JWT: %v", err)} + errors = append(errors, err) + continue + } + o.TrustedOperators = append(o.TrustedOperators, opc) + } + case "resolver", "account_resolver", "accounts_resolver": + var memResolverRe = regexp.MustCompile(`(MEM|MEMORY|mem|memory)\s*`) + var resolverRe = regexp.MustCompile(`(?:URL|url){1}(?:\({1}\s*"?([^\s"]*)"?\s*\){1})?\s*`) + str, ok := v.(string) + if !ok { + err := &configErr{tk, fmt.Sprintf("error parsing operator resolver, wrong type %T", v)} + errors = append(errors, err) + continue + } + if memResolverRe.MatchString(str) { + o.AccountResolver = &MemAccResolver{} + } else { + items := resolverRe.FindStringSubmatch(str) + if len(items) == 2 { + url := items[1] + if ur, err := NewURLAccResolver(url); err != nil { + err := &configErr{tk, fmt.Sprintf("URL account resolver error: %v", err)} + errors = append(errors, err) + } else { + o.AccountResolver = ur + } + } + } + if o.AccountResolver == nil { + err := &configErr{tk, fmt.Sprintf("error parsing account resolver, should be MEM or URL(\"url\")")} + errors = append(errors, err) + } + case "system_account", "system": + if sa, ok := v.(string); !ok { + err := &configErr{tk, fmt.Sprintf("system account name must be a string")} + errors = append(errors, err) + } else { + o.SystemAccount = sa + } + case "trusted", "trusted_keys": + switch v.(type) { + case string: + o.TrustedKeys = []string{v.(string)} + case []string: + o.TrustedKeys = v.([]string) case []interface{}: keys := make([]string, 0, len(v.([]interface{}))) for _, mv := range v.([]interface{}) { @@ -507,13 +565,13 @@ func (o *Options) ProcessConfigFile(configFile string) error { continue } } - o.TrustedNkeys = keys + o.TrustedKeys = keys default: err := &configErr{tk, fmt.Sprintf("error parsing trusted: unsupported type %T", v)} errors = append(errors, err) } // Do a quick sanity check on keys - for _, key := range o.TrustedNkeys { + for _, key := range o.TrustedKeys { if !nkeys.IsValidPublicOperatorKey(key) { err := &configErr{tk, fmt.Sprintf("trust key %q required to be a valid public operator nkey", key)} errors = append(errors, err) diff --git a/server/server.go b/server/server.go index 522066f6..fb7c39fc 100644 --- a/server/server.go +++ b/server/server.go @@ -161,7 +161,7 @@ type Server struct { ldmCh chan bool // Trusted public operator keys. - trustedNkeys []string + trustedKeys []string } // Make sure all are 64bits for atomic use @@ -173,8 +173,16 @@ type stats struct { slowConsumers int64 } +// DEPRECATED: Use NewServer(opts) // New will setup a new server struct after parsing the options. func New(opts *Options) *Server { + s, _ := NewServer(opts) + return s +} + +// NewServer will setup a new server struct after parsing the options. +// Could return an error if options can not be validated. +func NewServer(opts *Options) (*Server, error) { setBaselineOptions(opts) // Process TLS options, including whether we require client certificates. @@ -189,10 +197,8 @@ func New(opts *Options) *Server { // server will always be started with configuration parsing (that could // report issues). Its options can be (incorrectly) set by hand when // server is embedded. If there is an error, return nil. - // TODO(ik): Should probably have a new NewServer() API that returns (*Server, error) - // so user can know what's wrong. if err := validateOptions(opts); err != nil { - return nil + return nil, err } info := Info{ @@ -221,9 +227,9 @@ func New(opts *Options) *Server { configTime: now, } - // Trusted root keys. - if !s.processTrustedNkeys() { - return nil + // Trusted root operator keys. + if !s.processTrustedKeys() { + return nil, fmt.Errorf("Error processing trusted operator keys") } s.mu.Lock() @@ -245,7 +251,7 @@ func New(opts *Options) *Server { // may try to send things to gateways. gws, err := newGateway(opts) if err != nil { - return nil + return nil, err } s.gateway = gws @@ -269,7 +275,7 @@ func New(opts *Options) *Server { // For tracking accounts if err := s.configureAccounts(); err != nil { - return nil + return nil, err } // Used to setup Authorization. @@ -278,10 +284,14 @@ func New(opts *Options) *Server { // Start signal handler s.handleSignals() - return s + return s, nil } func validateOptions(o *Options) error { + // Check that the trust configuration is correct. + if err := validateTrustedOperators(o); err != nil { + return err + } // Check that gateway is properly configured. Returns no error // if there is no gateway defined. return validateGatewayOptions(o) @@ -318,11 +328,11 @@ func (s *Server) configureAccounts() error { s.registerAccount(acc) } // Check for configured account resolvers. - if opts.accResolver != nil { - s.accResolver = opts.accResolver + if opts.AccountResolver != nil { + s.accResolver = opts.AccountResolver } - // Check that if we have a SystemAccount it can - // be properly resolved. + + // Set the system account if it was configured. if opts.SystemAccount != _EMPTY_ { if acc := s.lookupAccount(opts.SystemAccount); acc == nil { return ErrMissingAccount @@ -347,7 +357,7 @@ func (s *Server) generateRouteInfoJSON() { func (s *Server) isTrustedIssuer(issuer string) bool { s.mu.Lock() defer s.mu.Unlock() - for _, tk := range s.trustedNkeys { + for _, tk := range s.trustedKeys { if tk == issuer { return true } @@ -355,25 +365,25 @@ func (s *Server) isTrustedIssuer(issuer string) bool { return false } -// processTrustedNkeys will process stamped and option based +// processTrustedKeys will process stamped and option based // trusted nkeys. Returns success. -func (s *Server) processTrustedNkeys() bool { - if trustedNkeys != "" && !s.initStampedTrustedNkeys() { +func (s *Server) processTrustedKeys() bool { + if trustedKeys != "" && !s.initStampedTrustedKeys() { return false - } else if s.opts.TrustedNkeys != nil { - for _, key := range s.opts.TrustedNkeys { + } else if s.opts.TrustedKeys != nil { + for _, key := range s.opts.TrustedKeys { if !nkeys.IsValidPublicOperatorKey(key) { return false } } - s.trustedNkeys = s.opts.TrustedNkeys + s.trustedKeys = s.opts.TrustedKeys } return true } -// checkTrustedNkeyString will check that the string is a valid array +// checkTrustedKeyString will check that the string is a valid array // of public operator nkeys. -func checkTrustedNkeyString(keys string) []string { +func checkTrustedKeyString(keys string) []string { tks := strings.Fields(keys) if len(tks) == 0 { return nil @@ -387,19 +397,19 @@ func checkTrustedNkeyString(keys string) []string { return tks } -// initStampedTrustedNkeys will check the stamped trusted keys -// and will set the server field 'trustedNkeys'. Returns whether +// initStampedTrustedKeys will check the stamped trusted keys +// and will set the server field 'trustedKeys'. Returns whether // it succeeded or not. -func (s *Server) initStampedTrustedNkeys() bool { +func (s *Server) initStampedTrustedKeys() bool { // Check to see if we have an override in options, which will cause us to fail. - if len(s.opts.TrustedNkeys) > 0 { + if len(s.opts.TrustedKeys) > 0 { return false } - tks := checkTrustedNkeyString(trustedNkeys) + tks := checkTrustedKeyString(trustedKeys) if len(tks) == 0 { return false } - s.trustedNkeys = tks + s.trustedKeys = tks return true } @@ -509,20 +519,56 @@ func (s *Server) RegisterAccount(name string) (*Account, error) { return acc, nil } +// SetSystemAccount will set the internal system account. +// If root operators are present it will also check validity. +func (s *Server) SetSystemAccount(accName string) error { + s.mu.Lock() + if acc := s.accounts[accName]; acc != nil { + s.mu.Unlock() + return s.setSystemAccount(acc) + } + // If we are here we do not have local knowledge of this account. + // Do this one by hand to return more useful error. + ac, jwt, err := s.fetchAccountClaims(accName) + if err != nil { + s.mu.Unlock() + return err + } + acc := s.buildInternalAccount(ac) + acc.claimJWT = jwt + s.registerAccount(acc) + s.mu.Unlock() + return s.setSystemAccount(acc) +} + +// SystemAccount returns the system account if set. +func (s *Server) SystemAccount() *Account { + s.mu.Lock() + defer s.mu.Unlock() + if s.sys != nil { + return s.sys.account + } + return nil +} + // Assign an system account. Should only be called once. // This sets up a server to send and receive messages from inside // the server itself. func (s *Server) setSystemAccount(acc *Account) error { if acc == nil { - return fmt.Errorf("system account is nil") + return ErrMissingAccount + } + // Don't try to fix this here. + if acc.IsExpired() { + return ErrAccountExpired } if !s.isTrustedIssuer(acc.Issuer) { - return fmt.Errorf("system account not a trusted account") + return ErrAccountValidation } s.mu.Lock() if s.sys != nil { s.mu.Unlock() - return fmt.Errorf("system account already exists") + return ErrAccountExists } s.sys = &internal{ @@ -602,7 +648,7 @@ func (s *Server) registerAccount(acc *Account) { // lookupAccount is a function to return the account structure // associated with an account name. -// Lock shiould be held on entry. +// Lock should be held on entry. func (s *Server) lookupAccount(name string) *Account { acc := s.accounts[name] if acc != nil { @@ -651,6 +697,26 @@ func (s *Server) updateAccount(acc *Account) bool { return false } +// updateAccountWithClaimJWT will chack and apply the claim update. +func (s *Server) updateAccountWithClaimJWT(acc *Account, claimJWT string) bool { + if acc == nil { + return false + } + acc.updated = time.Now() + if acc.claimJWT != "" && acc.claimJWT == claimJWT { + s.Debugf("Requested account update for [%s], same claims detected", acc.Name) + return false + } + accClaims, _, err := s.verifyAccountClaims(claimJWT) + if err == nil && accClaims != nil { + acc.claimJWT = claimJWT + s.updateAccountClaims(acc, accClaims) + return true + } + return false + +} + // fetchRawAccountClaims will grab raw account claims iff we have a resolver. // Lock is held upon entry. func (s *Server) fetchRawAccountClaims(name string) (string, error) { @@ -660,8 +726,14 @@ func (s *Server) fetchRawAccountClaims(name string) (string, error) { } // Need to do actual Fetch without the lock. s.mu.Unlock() + start := time.Now() claimJWT, err := accResolver.Fetch(name) + fetchTime := time.Since(start) s.mu.Lock() + s.Debugf("Account resolver fetch time was %v\n", fetchTime) + if fetchTime > time.Second { + s.Warnf("Account resolver took %v to fetch account", fetchTime) + } if err != nil { return "", err } @@ -669,6 +741,7 @@ func (s *Server) fetchRawAccountClaims(name string) (string, error) { } // fetchAccountClaims will attempt to fetch new claims if a resolver is present. +// Lock is held upon entry. func (s *Server) fetchAccountClaims(name string) (*jwt.AccountClaims, string, error) { claimJWT, err := s.fetchRawAccountClaims(name) if err != nil { @@ -695,11 +768,10 @@ func (s *Server) verifyAccountClaims(claimJWT string) (*jwt.AccountClaims, strin // Lock should be held upon entry. func (s *Server) fetchAccount(name string) *Account { if accClaims, claimJWT, _ := s.fetchAccountClaims(name); accClaims != nil { - if acc := s.buildInternalAccount(accClaims); acc != nil { - acc.claimJWT = claimJWT - s.registerAccount(acc) - return acc - } + acc := s.buildInternalAccount(accClaims) + acc.claimJWT = claimJWT + s.registerAccount(acc) + return acc } return nil } @@ -730,6 +802,20 @@ func (s *Server) Start() { // Snapshot server options. opts := s.getOpts() + hasOperators := len(opts.TrustedOperators) > 0 + if hasOperators { + s.Noticef("Trusted Operators") + } + for _, opc := range opts.TrustedOperators { + s.Noticef(" System : %q", opc.Audience) + s.Noticef(" Operator: %q", opc.Name) + s.Noticef(" Issued : %v", time.Unix(opc.IssuedAt, 0)) + s.Noticef(" Expires : %v", time.Unix(opc.Expires, 0)) + } + if hasOperators && opts.SystemAccount == _EMPTY_ { + s.Warnf("Trusted Operators should utilize a System Account") + } + // Log the pid to a file if opts.PidFile != _EMPTY_ { if err := s.logPid(); err != nil { @@ -745,7 +831,10 @@ func (s *Server) Start() { // Setup system account which will start eventing stack. if sa := opts.SystemAccount; sa != _EMPTY_ { - s.setSystemAccount(s.lookupAccount(sa)) + if err := s.SetSystemAccount(sa); err != nil { + s.Fatalf("Can't set system account: %v", err) + return + } } // Start up gateway if needed. Do this before starting the routes, because diff --git a/server/trust_test.go b/server/trust_test.go index 8164aaca..99821fff 100644 --- a/server/trust_test.go +++ b/server/trust_test.go @@ -25,60 +25,60 @@ const ( t2 = "OAHC7NGAHG3YVPTD6QOUFZGPM2OMU6EOS67O2VHBUOA6BJLPTWFHGLKU" ) -func TestStampedTrustedNkeys(t *testing.T) { +func TestStampedTrustedKeys(t *testing.T) { opts := DefaultOptions() - defer func() { trustedNkeys = "" }() + defer func() { trustedKeys = "" }() // Set this to a bad key. We require valid operator public keys. - trustedNkeys = "bad" + trustedKeys = "bad" if s := New(opts); s != nil { s.Shutdown() - t.Fatalf("Expected a bad trustedNkeys to return nil server") + t.Fatalf("Expected a bad trustedKeys to return nil server") } - trustedNkeys = t1 + trustedKeys = t1 s := New(opts) if s == nil { t.Fatalf("Expected non-nil server") } - if len(s.trustedNkeys) != 1 || s.trustedNkeys[0] != t1 { + if len(s.trustedKeys) != 1 || s.trustedKeys[0] != t1 { t.Fatalf("Trusted Nkeys not setup properly") } - trustedNkeys = strings.Join([]string{t1, t2}, " ") + trustedKeys = strings.Join([]string{t1, t2}, " ") if s = New(opts); s == nil { t.Fatalf("Expected non-nil server") } - if len(s.trustedNkeys) != 2 || s.trustedNkeys[0] != t1 || s.trustedNkeys[1] != t2 { + if len(s.trustedKeys) != 2 || s.trustedKeys[0] != t1 || s.trustedKeys[1] != t2 { t.Fatalf("Trusted Nkeys not setup properly") } - opts.TrustedNkeys = []string{"OVERRIDE ME"} + opts.TrustedKeys = []string{"OVERRIDE ME"} if s = New(opts); s != nil { - t.Fatalf("Expected opts.TrustedNkeys to return nil server") + t.Fatalf("Expected opts.TrustedKeys to return nil server") } } func TestTrustedKeysOptions(t *testing.T) { - trustedNkeys = "" + trustedKeys = "" opts := DefaultOptions() - opts.TrustedNkeys = []string{"bad"} + opts.TrustedKeys = []string{"bad"} if s := New(opts); s != nil { s.Shutdown() - t.Fatalf("Expected a bad opts.TrustedNkeys to return nil server") + t.Fatalf("Expected a bad opts.TrustedKeys to return nil server") } - opts.TrustedNkeys = []string{t1} + opts.TrustedKeys = []string{t1} s := New(opts) if s == nil { t.Fatalf("Expected non-nil server") } - if len(s.trustedNkeys) != 1 || s.trustedNkeys[0] != t1 { + if len(s.trustedKeys) != 1 || s.trustedKeys[0] != t1 { t.Fatalf("Trusted Nkeys not setup properly via options") } - opts.TrustedNkeys = []string{t1, t2} + opts.TrustedKeys = []string{t1, t2} if s = New(opts); s == nil { t.Fatalf("Expected non-nil server") } - if len(s.trustedNkeys) != 2 || s.trustedNkeys[0] != t1 || s.trustedNkeys[1] != t2 { + if len(s.trustedKeys) != 2 || s.trustedKeys[0] != t1 || s.trustedKeys[1] != t2 { t.Fatalf("Trusted Nkeys not setup properly via options") } } @@ -90,11 +90,11 @@ func TestTrustConfigOption(t *testing.T) { if err != nil { t.Fatalf("Error parsing config: %v", err) } - if l := len(opts.TrustedNkeys); l != 1 { + if l := len(opts.TrustedKeys); l != 1 { t.Fatalf("Expected 1 trusted key, got %d", l) } - if opts.TrustedNkeys[0] != t1 { - t.Fatalf("Expected trusted key to be %q, got %q", t1, opts.TrustedNkeys[0]) + if opts.TrustedKeys[0] != t1 { + t.Fatalf("Expected trusted key to be %q, got %q", t1, opts.TrustedKeys[0]) } confFileName = createConfFile(t, []byte(fmt.Sprintf("trusted = [%q, %q]", t1, t2))) @@ -103,14 +103,14 @@ func TestTrustConfigOption(t *testing.T) { if err != nil { t.Fatalf("Error parsing config: %v", err) } - if l := len(opts.TrustedNkeys); l != 2 { + if l := len(opts.TrustedKeys); l != 2 { t.Fatalf("Expected 2 trusted key, got %d", l) } - if opts.TrustedNkeys[0] != t1 { - t.Fatalf("Expected trusted key to be %q, got %q", t1, opts.TrustedNkeys[0]) + if opts.TrustedKeys[0] != t1 { + t.Fatalf("Expected trusted key to be %q, got %q", t1, opts.TrustedKeys[0]) } - if opts.TrustedNkeys[1] != t2 { - t.Fatalf("Expected trusted key to be %q, got %q", t2, opts.TrustedNkeys[1]) + if opts.TrustedKeys[1] != t2 { + t.Fatalf("Expected trusted key to be %q, got %q", t2, opts.TrustedKeys[1]) } // Now do a bad one. diff --git a/test/configs/nkeys/op.jwt b/test/configs/nkeys/op.jwt new file mode 100644 index 00000000..93356896 --- /dev/null +++ b/test/configs/nkeys/op.jwt @@ -0,0 +1,3 @@ +-----BEGIN TEST OPERATOR JWT----- +eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJhdWQiOiJURVNUUyIsImV4cCI6MTg1OTEyMTI3NSwianRpIjoiWE5MWjZYWVBIVE1ESlFSTlFPSFVPSlFHV0NVN01JNVc1SlhDWk5YQllVS0VRVzY3STI1USIsImlhdCI6MTU0Mzc2MTI3NSwiaXNzIjoiT0NBVDMzTVRWVTJWVU9JTUdOR1VOWEo2NkFIMlJMU0RBRjNNVUJDWUFZNVFNSUw2NU5RTTZYUUciLCJuYW1lIjoiU3luYWRpYSBDb21tdW5pY2F0aW9ucyBJbmMuIiwibmJmIjoxNTQzNzYxMjc1LCJzdWIiOiJPQ0FUMzNNVFZVMlZVT0lNR05HVU5YSjY2QUgyUkxTREFGM01VQkNZQVk1UU1JTDY1TlFNNlhRRyIsInR5cGUiOiJvcGVyYXRvciIsIm5hdHMiOnsic2lnbmluZ19rZXlzIjpbIk9EU0tSN01ZRlFaNU1NQUo2RlBNRUVUQ1RFM1JJSE9GTFRZUEpSTUFWVk40T0xWMllZQU1IQ0FDIiwiT0RTS0FDU1JCV1A1MzdEWkRSVko2NTdKT0lHT1BPUTZLRzdUNEhONk9LNEY2SUVDR1hEQUhOUDIiLCJPRFNLSTM2TFpCNDRPWTVJVkNSNlA1MkZaSlpZTVlXWlZXTlVEVExFWjVUSzJQTjNPRU1SVEFCUiJdfX0.hyfz6E39BMUh0GLzovFfk3wT4OfualftjdJ_eYkLfPvu5tZubYQ_Pn9oFYGCV_6yKy3KMGhWGUCyCdHaPhalBw +------END TEST OPERATOR JWT------ \ No newline at end of file diff --git a/test/configs/nkeys/sigkeys.txt b/test/configs/nkeys/sigkeys.txt new file mode 100644 index 00000000..590aadc5 --- /dev/null +++ b/test/configs/nkeys/sigkeys.txt @@ -0,0 +1,20 @@ + +######################################################## +# TESTS ONLY # +######################################################## + +# These are the public signing keys + +-----BEGIN SIGNING KEYS----- +ODSKR7MYFQZ5MMAJ6FPMEETCTE3RIHOFLTYPJRMAVVN4OLV2YYAMHCAC +ODSKACSRBWP537DZDRVJ657JOIGOPOQ6KG7T4HN6OK4F6IECGXDAHNP2 +ODSKI36LZB44OY5IVCR6P52FZJZYMYWZVWNUDTLEZ5TK2PN3OEMRTABR +------END SIGNING KEYS------ + +# These are the seeds. + +----BEGIN SIGNING SEEDS----- +SOAO7RDW6CLJORHHBS4DPYYIIIAASEIUJ5WWS5FMWLNTFHUCKQ5CAC45AA +SOAEL3NFOTU6YK3DBTEKQYZ2C5IWSVZWWZCQDASBUOHJKBFLVANK27JMMQ +SOACSMP662P2BZDKVF6WCB6FIQYORADDWWWEAI55QY24CQRTY4METUING4 +------END SIGING SEEDS------ diff --git a/test/configs/nkeys/test.seed b/test/configs/nkeys/test.seed new file mode 100644 index 00000000..8a60e9c8 --- /dev/null +++ b/test/configs/nkeys/test.seed @@ -0,0 +1,7 @@ +######################################################## +# TESTS ONLY # +######################################################## + +-----BEGIN TEST OPERATOR SEED----- +SOAFYNORQLQFJYBYNUGC5D7SH2MXMUX5BFEWWGHN3EK4VGG5TPT5DZP7QU +------END TEST OPERATOR SEED------ diff --git a/test/configs/operator.conf b/test/configs/operator.conf new file mode 100644 index 00000000..6cbc2a80 --- /dev/null +++ b/test/configs/operator.conf @@ -0,0 +1,16 @@ +# Server that loads an operator JWT + +listen: 127.0.0.1:22222 + +# Can be an array of filenames as well. +# Key can be operator, operators, roots, root, root_operators, root_operator + +operator = "./configs/nkeys/op.jwt" + +# This is for account resolution. +# Can be MEMORY (Testing) or can be URL(url). +# The resolver will append the account name to url for retrieval. +# E.g. +# resolver = URL("https://api.synadia.com/ngs/v1/accounts/jwt") +# +resolver = MEMORY \ No newline at end of file diff --git a/test/operator_test.go b/test/operator_test.go new file mode 100644 index 00000000..f292b001 --- /dev/null +++ b/test/operator_test.go @@ -0,0 +1,235 @@ +// Copyright 2018 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "fmt" + "testing" + + "github.com/nats-io/gnatsd/server" + "github.com/nats-io/go-nats" + "github.com/nats-io/jwt" + "github.com/nats-io/nkeys" +) + +const testOpConfig = "./configs/operator.conf" + +// This matches ./configs/nkeys_jwts/test.seed +// Test operator seed. +var oSeed = []byte("SOAFYNORQLQFJYBYNUGC5D7SH2MXMUX5BFEWWGHN3EK4VGG5TPT5DZP7QU") + +// This is a signing key seed. +var skSeed = []byte("SOAEL3NFOTU6YK3DBTEKQYZ2C5IWSVZWWZCQDASBUOHJKBFLVANK27JMMQ") + +func checkKeys(t *testing.T, opts *server.Options, opc *jwt.OperatorClaims, expected int) { + // We should have filled in the TrustedKeys here. + if len(opts.TrustedKeys) != expected { + t.Fatalf("Should have %d trusted keys, got %d", expected, len(opts.TrustedKeys)) + } + // Check that we properly placed all keys from the opc into TrustedKeys + chkMember := func(s string) { + for _, c := range opts.TrustedKeys { + if s == c { + return + } + } + t.Fatalf("Expected %q to be in TrustedKeys", s) + } + chkMember(opc.Issuer) + for _, sk := range opc.SigningKeys { + chkMember(sk) + } +} + +// This will test that we enforce certain restrictions when you use trusted operators. +// Like auth is always true, can't define accounts or users, required to define an account resolver, etc. +func TestOperatorRestrictions(t *testing.T) { + opts, err := server.ProcessConfigFile(testOpConfig) + if err != nil { + t.Fatalf("Error processing config file: %v", err) + } + if _, err := server.NewServer(opts); err != nil { + t.Fatalf("Expected to create a server successfully") + } + // TrustedKeys get defined when processing from above, trying again with + // same opts should not work. + if _, err := server.NewServer(opts); err == nil { + t.Fatalf("Expected an error with TrustedKeys defined") + } + // Must wipe and rebuild to succeed. + wipeOpts := func() { + opts.TrustedKeys = nil + opts.Accounts = nil + opts.Users = nil + opts.Nkeys = nil + opts.AllowNewAccounts = false + } + + wipeOpts() + opts.Accounts = []*server.Account{&server.Account{Name: "TEST"}} + if _, err := server.NewServer(opts); err == nil { + t.Fatalf("Expected an error with Accounts defined") + } + wipeOpts() + opts.Users = []*server.User{&server.User{Username: "TEST"}} + if _, err := server.NewServer(opts); err == nil { + t.Fatalf("Expected an error with Users defined") + } + wipeOpts() + opts.Nkeys = []*server.NkeyUser{&server.NkeyUser{Nkey: "TEST"}} + if _, err := server.NewServer(opts); err == nil { + t.Fatalf("Expected an error with Nkey Users defined") + } + wipeOpts() + opts.AllowNewAccounts = true + if _, err := server.NewServer(opts); err == nil { + t.Fatalf("Expected an error with AllowNewAccounts set to true") + } + + wipeOpts() + opts.AccountResolver = nil + if _, err := server.NewServer(opts); err == nil { + t.Fatalf("Expected an error without an AccountResolver defined") + } +} + +func TestOperatorConfig(t *testing.T) { + opts, err := server.ProcessConfigFile(testOpConfig) + if err != nil { + t.Fatalf("Error processing config file: %v", err) + } + // Check we have the TrustedOperators + if len(opts.TrustedOperators) != 1 { + t.Fatalf("Expected to load the operator") + } + _, err = server.NewServer(opts) + if err != nil { + t.Fatalf("Expected to create a server: %v", err) + } + // We should have filled in the TrustedKeys here. + // Our master key (issuer) plus the signing keys (3). + checkKeys(t, opts, opts.TrustedOperators[0], 4) +} + +func runOperatorServer(t *testing.T) (*server.Server, *server.Options) { + return RunServerWithConfig(testOpConfig) +} + +func createAccountForOperatorKey(t *testing.T, s *server.Server, seed []byte) (*server.Account, nkeys.KeyPair) { + t.Helper() + okp, _ := nkeys.FromSeed(seed) + akp, _ := nkeys.CreateAccount() + pub, _ := akp.PublicKey() + nac := jwt.NewAccountClaims(pub) + jwt, _ := nac.Encode(okp) + if err := s.AccountResolver().Store(pub, jwt); err != nil { + t.Fatalf("Account Resolver returned an error: %v", err) + } + return s.LookupAccount(pub), akp +} + +func createAccount(t *testing.T, s *server.Server) (*server.Account, nkeys.KeyPair) { + t.Helper() + return createAccountForOperatorKey(t, s, oSeed) +} + +func createUserCreds(t *testing.T, s *server.Server, akp nkeys.KeyPair) nats.Option { + t.Helper() + kp, _ := nkeys.CreateUser() + pub, _ := kp.PublicKey() + nuc := jwt.NewUserClaims(pub) + ujwt, err := nuc.Encode(akp) + if err != nil { + t.Fatalf("Error generating user JWT: %v", err) + } + userCB := func() (string, error) { + return ujwt, nil + } + sigCB := func(nonce []byte) ([]byte, error) { + sig, _ := kp.Sign(nonce) + return sig, nil + } + return nats.UserJWT(userCB, sigCB) +} + +func TestOperatorServer(t *testing.T) { + s, opts := runOperatorServer(t) + defer s.Shutdown() + + url := fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port) + if _, err := nats.Connect(url); err == nil { + t.Fatalf("Expected to fail with no credentials") + } + + _, akp := createAccount(t, s) + nc, err := nats.Connect(url, createUserCreds(t, s, akp)) + if err != nil { + t.Fatalf("Error on connect: %v", err) + } + nc.Close() + + // Now create an account from another operator, this should fail. + okp, _ := nkeys.CreateOperator() + seed, _ := okp.Seed() + _, akp = createAccountForOperatorKey(t, s, seed) + _, err = nats.Connect(url, createUserCreds(t, s, akp)) + if err == nil { + t.Fatalf("Expected error on connect") + } +} + +func TestOperatorSystemAccount(t *testing.T) { + s, _ := runOperatorServer(t) + defer s.Shutdown() + + // Create an account from another operator, this should fail if used as a system account. + okp, _ := nkeys.CreateOperator() + seed, _ := okp.Seed() + acc, _ := createAccountForOperatorKey(t, s, seed) + if err := s.SetSystemAccount(acc.Name); err == nil { + t.Fatalf("Expected this to fail") + } + if acc := s.SystemAccount(); acc != nil { + t.Fatalf("Expected no account to be set for system account") + } + + acc, _ = createAccount(t, s) + if err := s.SetSystemAccount(acc.Name); err != nil { + t.Fatalf("Expected this succeed, got %v", err) + } + if sysAcc := s.SystemAccount(); sysAcc != acc { + t.Fatalf("Did not get matching account for system account") + } +} + +func TestOperatorSigningKeys(t *testing.T) { + s, opts := runOperatorServer(t) + defer s.Shutdown() + + // Create an account with a signing key, not the master key. + acc, akp := createAccountForOperatorKey(t, s, skSeed) + + // Make sure we can set system account. + if err := s.SetSystemAccount(acc.Name); err != nil { + t.Fatalf("Expected this succeed, got %v", err) + } + + // Make sure we can create users with it too. + url := fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port) + nc, err := nats.Connect(url, createUserCreds(t, s, akp)) + if err != nil { + t.Fatalf("Error on connect: %v", err) + } + nc.Close() +} diff --git a/test/test.go b/test/test.go index 4ec1dae6..4aef9522 100644 --- a/test/test.go +++ b/test/test.go @@ -53,9 +53,9 @@ func RunServer(opts *server.Options) *server.Server { if opts == nil { opts = &DefaultTestOptions } - s := server.New(opts) - if s == nil { - panic("No NATS Server object returned.") + s, err := server.NewServer(opts) + if err != nil || s == nil { + panic(fmt.Sprintf("No NATS Server object returned: %v", err)) } // Run server in Go routine. diff --git a/vendor/manifest b/vendor/manifest index 2f6052a4..9b7e3e1e 100644 --- a/vendor/manifest +++ b/vendor/manifest @@ -25,14 +25,6 @@ "branch": "master", "notests": true }, - { - "importpath": "github.com/pkg/errors", - "repository": "https://github.com/pkg/errors", - "vcs": "git", - "revision": "059132a15dd08d6704c67711dae0cf35ab991756", - "branch": "master", - "notests": true - }, { "importpath": "golang.org/x/crypto/bcrypt", "repository": "https://go.googlesource.com/crypto",