mirror of
https://github.com/gogrlx/nats-server.git
synced 2026-04-02 03:38:42 -07:00
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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
14
test/configs/operator_inline.conf
Normal file
14
test/configs/operator_inline.conf
Normal 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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user