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 <derek@nats.io>
This commit is contained in:
Derek Collison
2019-06-24 16:41:01 -07:00
parent 6f49f76efb
commit 5b42b99dc1
7 changed files with 167 additions and 16 deletions

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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":

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)
}
}