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",