From 5b42b99dc1713907699c46310eede6359577b348 Mon Sep 17 00:00:00 2001 From: Derek Collison Date: Mon, 24 Jun 2019 16:41:01 -0700 Subject: [PATCH] Allow operator to be inline JWT. Also preloads just warn on validation issues, do not stop starting or reloads. We issue validation warnings now to the log. Signed-off-by: Derek Collison --- server/jwt.go | 11 +++- server/monitor.go | 4 +- server/opts.go | 19 +++--- server/reload.go | 2 + server/server.go | 37 +++++++++++- test/configs/operator_inline.conf | 14 +++++ test/operator_test.go | 96 ++++++++++++++++++++++++++++++- 7 files changed, 167 insertions(+), 16 deletions(-) create mode 100644 test/configs/operator_inline.conf diff --git a/server/jwt.go b/server/jwt.go index 79b51141..7d8b2746 100644 --- a/server/jwt.go +++ b/server/jwt.go @@ -17,6 +17,7 @@ import ( "fmt" "io/ioutil" "regexp" + "strings" "github.com/nats-io/jwt" "github.com/nats-io/nkeys" @@ -24,11 +25,19 @@ import ( var nscDecoratedRe = regexp.MustCompile(`\s*(?:(?:[-]{3,}[^\n]*[-]{3,}\n)(.+)(?:\n\s*[-]{3,}[^\n]*[-]{3,}[\n]*))`) +// All JWTs once encoded start with this +const jwtPrefix = "eyJ" + // readOperatorJWT func readOperatorJWT(jwtfile string) (*jwt.OperatorClaims, error) { contents, err := ioutil.ReadFile(jwtfile) if err != nil { - return nil, err + // Check to see if the JWT has been inlined. + if !strings.HasPrefix(jwtfile, jwtPrefix) { + return nil, err + } + // We may have an inline jwt here. + contents = []byte(jwtfile) } defer wipeSlice(contents) diff --git a/server/monitor.go b/server/monitor.go index e75f12fa..403304aa 100644 --- a/server/monitor.go +++ b/server/monitor.go @@ -1398,7 +1398,7 @@ func createOutboundAccountsGatewayz(opts *GatewayzOptions, gw *gateway) []*Accou // Returns an AccountGatewayz for this gateway outbound connection func createAccountOutboundGatewayz(name string, ei interface{}) *AccountGatewayz { a := &AccountGatewayz{ - Name: name, + Name: name, InterestOnlyThreshold: gatewayMaxRUnsubBeforeSwitch, } if ei != nil { @@ -1481,7 +1481,7 @@ func createInboundAccountsGatewayz(opts *GatewayzOptions, gw *gateway) []*Accoun // Returns an AccountGatewayz for this gateway inbound connection func createInboundAccountGatewayz(name string, e *insie) *AccountGatewayz { a := &AccountGatewayz{ - Name: name, + Name: name, InterestOnlyThreshold: gatewayMaxRUnsubBeforeSwitch, } if e != nil { diff --git a/server/opts.go b/server/opts.go index 2934824a..4308b676 100644 --- a/server/opts.go +++ b/server/opts.go @@ -602,9 +602,7 @@ func (o *Options) ProcessConfigFile(configFile string) error { err := &configErr{tk, fmt.Sprintf("error parsing operators: unsupported type %T", v)} errors = append(errors, err) } - // 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. + // Assume for now these are file names, but they can also be the JWT itself inline. o.TrustedOperators = make([]*jwt.OperatorClaims, 0, len(opFiles)) for _, fname := range opFiles { opc, err := readOperatorJWT(fname) @@ -651,19 +649,26 @@ func (o *Options) ProcessConfigFile(configFile string) error { case "resolver_preload": mp, ok := v.(map[string]interface{}) if !ok { - err := &configErr{tk, fmt.Sprintf("preload should be a map of key:jwt")} + err := &configErr{tk, fmt.Sprintf("preload should be a map of account_public_key:account_jwt")} errors = append(errors, err) continue } o.resolverPreloads = make(map[string]string) for key, val := range mp { tk, val = unwrapValue(val) - if jwt, ok := val.(string); !ok { - err := &configErr{tk, fmt.Sprintf("preload map value should be a string jwt")} + if jwtstr, ok := val.(string); !ok { + err := &configErr{tk, fmt.Sprintf("preload map value should be a string JWT")} errors = append(errors, err) continue } else { - o.resolverPreloads[key] = jwt + // Make sure this is a valid account JWT, that is a config error. + // We will warn of expirations, etc later. + if _, err := jwt.DecodeAccountClaims(jwtstr); err != nil { + err := &configErr{tk, fmt.Sprintf("invalid account JWT")} + errors = append(errors, err) + continue + } + o.resolverPreloads[key] = jwtstr } } case "system_account", "system": diff --git a/server/reload.go b/server/reload.go index b37439fb..1511fcfd 100644 --- a/server/reload.go +++ b/server/reload.go @@ -882,6 +882,8 @@ func (s *Server) reloadAuthorization() { } else if s.opts.AccountResolver != nil { s.configureResolver() if _, ok := s.accResolver.(*MemAccResolver); ok { + // Check preloads so we can issue warnings etc if needed. + s.checkResolvePreloads() // With a memory resolver we want to do something similar to configured accounts. // We will walk the accounts and delete them if they are no longer present via fetch. // If they are present we will force a claim update to process changes. diff --git a/server/server.go b/server/server.go index cd021f2a..681fdccf 100644 --- a/server/server.go +++ b/server/server.go @@ -423,16 +423,19 @@ func (s *Server) configureAccounts() error { return nil } +// Setup the memory resolver, make sure the JWTs are properly formed but do not +// enforce expiration etc. func (s *Server) configureResolver() error { opts := s.opts s.accResolver = opts.AccountResolver if opts.AccountResolver != nil && len(opts.resolverPreloads) > 0 { if _, ok := s.accResolver.(*MemAccResolver); !ok { - return fmt.Errorf("resolver preloads only available for MemAccResolver") + return fmt.Errorf("resolver preloads only available for resolver type MEM") } for k, v := range opts.resolverPreloads { - if _, _, err := s.verifyAccountClaims(v); err != nil { - return fmt.Errorf("preloaded Account: %v", err) + _, err := jwt.DecodeAccountClaims(v) + if err != nil { + return fmt.Errorf("preload account error for %q: %v", k, err) } s.accResolver.Store(k, v) } @@ -440,6 +443,28 @@ func (s *Server) configureResolver() error { return nil } +// This will check preloads for validation issues. +func (s *Server) checkResolvePreloads() { + opts := s.getOpts() + // We can just check the read-only opts versions here, that way we do not need + // to grab server lock or access s.accResolver. + for k, v := range opts.resolverPreloads { + claims, err := jwt.DecodeAccountClaims(v) + if err != nil { + s.Errorf("Preloaded account [%s] not valid", k) + } + // Check if it is expired. + vr := jwt.CreateValidationResults() + claims.Validate(vr) + if vr.IsBlocking(true) { + s.Warnf("Account [%s] has validation issues:", k) + for _, v := range vr.Issues { + s.Warnf(" - %s", v.Description) + } + } + } +} + func (s *Server) generateRouteInfoJSON() { // New proto wants a nonce. var raw [nonceLen]byte @@ -955,6 +980,12 @@ func (s *Server) Start() { s.Warnf("Trusted Operators should utilize a System Account") } + // If we have a memory resolver, check the accounts here for validation exceptions. + // This allows them to be logged right away vs when they are accessed via a client. + if hasOperators && len(opts.resolverPreloads) > 0 { + s.checkResolvePreloads() + } + // Log the pid to a file if opts.PidFile != _EMPTY_ { if err := s.logPid(); err != nil { diff --git a/test/configs/operator_inline.conf b/test/configs/operator_inline.conf new file mode 100644 index 00000000..dc9ccb26 --- /dev/null +++ b/test/configs/operator_inline.conf @@ -0,0 +1,14 @@ +# Server that loads an operator JWT + +listen: 127.0.0.1:22222 + +# This example is a single inline JWT. +operator = "eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJhdWQiOiJURVNUUyIsImV4cCI6MTg1OTEyMTI3NSwianRpIjoiWE5MWjZYWVBIVE1ESlFSTlFPSFVPSlFHV0NVN01JNVc1SlhDWk5YQllVS0VRVzY3STI1USIsImlhdCI6MTU0Mzc2MTI3NSwiaXNzIjoiT0NBVDMzTVRWVTJWVU9JTUdOR1VOWEo2NkFIMlJMU0RBRjNNVUJDWUFZNVFNSUw2NU5RTTZYUUciLCJuYW1lIjoiU3luYWRpYSBDb21tdW5pY2F0aW9ucyBJbmMuIiwibmJmIjoxNTQzNzYxMjc1LCJzdWIiOiJPQ0FUMzNNVFZVMlZVT0lNR05HVU5YSjY2QUgyUkxTREFGM01VQkNZQVk1UU1JTDY1TlFNNlhRRyIsInR5cGUiOiJvcGVyYXRvciIsIm5hdHMiOnsic2lnbmluZ19rZXlzIjpbIk9EU0tSN01ZRlFaNU1NQUo2RlBNRUVUQ1RFM1JJSE9GTFRZUEpSTUFWVk40T0xWMllZQU1IQ0FDIiwiT0RTS0FDU1JCV1A1MzdEWkRSVko2NTdKT0lHT1BPUTZLRzdUNEhONk9LNEY2SUVDR1hEQUhOUDIiLCJPRFNLSTM2TFpCNDRPWTVJVkNSNlA1MkZaSlpZTVlXWlZXTlVEVExFWjVUSzJQTjNPRU1SVEFCUiJdfX0.hyfz6E39BMUh0GLzovFfk3wT4OfualftjdJ_eYkLfPvu5tZubYQ_Pn9oFYGCV_6yKy3KMGhWGUCyCdHaPhalBw" + +# 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 diff --git a/test/operator_test.go b/test/operator_test.go index 2d5caa19..197958e4 100644 --- a/test/operator_test.go +++ b/test/operator_test.go @@ -28,7 +28,10 @@ import ( "github.com/nats-io/nkeys" ) -const testOpConfig = "./configs/operator.conf" +const ( + testOpConfig = "./configs/operator.conf" + testOpInlineConfig = "./configs/operator_inline.conf" +) // This matches ./configs/nkeys_jwts/test.seed // Test operator seed. @@ -122,8 +125,26 @@ func TestOperatorConfig(t *testing.T) { 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). + // We should have filled in the public TrustedKeys here. + // Our master public key (issuer) plus the signing keys (3). + checkKeys(t, opts, opts.TrustedOperators[0], 4) +} + +func TestOperatorConfigInline(t *testing.T) { + opts, err := server.ProcessConfigFile(testOpInlineConfig) + 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 public TrustedKeys here. + // Our master public key (issuer) plus the signing keys (3). checkKeys(t, opts, opts.TrustedOperators[0], 4) } @@ -488,3 +509,72 @@ func TestReloadDoesUpdatesAccountsWithMemoryResolver(t *testing.T) { t.Fatalf("Expected error looking up old account") } } + +func TestReloadFailsWithBadAccountsWithMemoryResolver(t *testing.T) { + // Create two accounts, system and normal account. + sysJWT, sysKP := createAccountForConfig(t) + sysPub, _ := sysKP.PublicKey() + + // Create an expired account by hand here. We want to make sure we start up correctly + // with expired or otherwise accounts with validation issues. + okp, _ := nkeys.FromSeed(oSeed) + + akp, _ := nkeys.CreateAccount() + apub, _ := akp.PublicKey() + nac := jwt.NewAccountClaims(apub) + nac.IssuedAt = time.Now().Add(-10 * time.Second).Unix() + nac.Expires = time.Now().Add(-2 * time.Second).Unix() + ajwt, err := nac.Encode(okp) + if err != nil { + t.Fatalf("Error generating account JWT: %v", err) + } + + cf := ` + listen: 127.0.0.1:-1 + cluster { + listen: 127.0.0.1:-1 + authorization { + timeout: 2.2 + } %s + } + + operator = "./configs/nkeys/op.jwt" + system_account = "%s" + + resolver = MEMORY + resolver_preload = { + %s : "%s" + %s : "%s" + } + ` + contents := strings.Replace(fmt.Sprintf(cf, "", sysPub, sysPub, sysJWT, apub, ajwt), "\n\t", "\n", -1) + conf := createConfFile(t, []byte(contents)) + defer os.Remove(conf) + + s, _ := RunServerWithConfig(conf) + defer s.Shutdown() + + // Now add in bogus account for second item and make sure reload fails. + contents = strings.Replace(fmt.Sprintf(cf, "", sysPub, sysPub, sysJWT, "foo", "bar"), "\n\t", "\n", -1) + err = ioutil.WriteFile(conf, []byte(contents), 0644) + if err != nil { + t.Fatal(err) + } + + if err := s.Reload(); err == nil { + t.Fatalf("Expected fatal error with bad account on reload") + } + + // Put it back with a normal account and reload should succeed. + accJWT, accKP := createAccountForConfig(t) + accPub, _ := accKP.PublicKey() + + contents = strings.Replace(fmt.Sprintf(cf, "", sysPub, sysPub, sysJWT, accPub, accJWT), "\n\t", "\n", -1) + err = ioutil.WriteFile(conf, []byte(contents), 0644) + if err != nil { + t.Fatal(err) + } + if err := s.Reload(); err != nil { + t.Fatalf("Got unexpected error on reload: %v", err) + } +}