mirror of
https://github.com/gogrlx/nats-server.git
synced 2026-04-02 11:48:43 -07:00
5764 lines
172 KiB
Go
5764 lines
172 KiB
Go
// Copyright 2018-2020 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 (
|
|
"bufio"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/jwt/v2"
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/nats-io/nkeys"
|
|
)
|
|
|
|
var (
|
|
// This matches ./configs/nkeys_jwts/test.seed
|
|
oSeed = []byte("SOAFYNORQLQFJYBYNUGC5D7SH2MXMUX5BFEWWGHN3EK4VGG5TPT5DZP7QU")
|
|
// This matches ./configs/nkeys/op.jwt
|
|
ojwt = "eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJhdWQiOiJURVNUUyIsImV4cCI6MTg1OTEyMTI3NSwianRpIjoiWE5MWjZYWVBIVE1ESlFSTlFPSFVPSlFHV0NVN01JNVc1SlhDWk5YQllVS0VRVzY3STI1USIsImlhdCI6MTU0Mzc2MTI3NSwiaXNzIjoiT0NBVDMzTVRWVTJWVU9JTUdOR1VOWEo2NkFIMlJMU0RBRjNNVUJDWUFZNVFNSUw2NU5RTTZYUUciLCJuYW1lIjoiU3luYWRpYSBDb21tdW5pY2F0aW9ucyBJbmMuIiwibmJmIjoxNTQzNzYxMjc1LCJzdWIiOiJPQ0FUMzNNVFZVMlZVT0lNR05HVU5YSjY2QUgyUkxTREFGM01VQkNZQVk1UU1JTDY1TlFNNlhRRyIsInR5cGUiOiJvcGVyYXRvciIsIm5hdHMiOnsic2lnbmluZ19rZXlzIjpbIk9EU0tSN01ZRlFaNU1NQUo2RlBNRUVUQ1RFM1JJSE9GTFRZUEpSTUFWVk40T0xWMllZQU1IQ0FDIiwiT0RTS0FDU1JCV1A1MzdEWkRSVko2NTdKT0lHT1BPUTZLRzdUNEhONk9LNEY2SUVDR1hEQUhOUDIiLCJPRFNLSTM2TFpCNDRPWTVJVkNSNlA1MkZaSlpZTVlXWlZXTlVEVExFWjVUSzJQTjNPRU1SVEFCUiJdfX0.hyfz6E39BMUh0GLzovFfk3wT4OfualftjdJ_eYkLfPvu5tZubYQ_Pn9oFYGCV_6yKy3KMGhWGUCyCdHaPhalBw"
|
|
oKp nkeys.KeyPair
|
|
|
|
tempRoot = filepath.Join(os.TempDir(), "nats-server")
|
|
)
|
|
|
|
func init() {
|
|
var err error
|
|
oKp, err = nkeys.FromSeed(oSeed)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("Parsing oSeed failed with: %v", err))
|
|
}
|
|
}
|
|
|
|
func chanRecv(t *testing.T, recvChan <-chan struct{}, limit time.Duration) {
|
|
t.Helper()
|
|
select {
|
|
case <-recvChan:
|
|
case <-time.After(limit):
|
|
t.Fatal("Should have received from channel")
|
|
}
|
|
}
|
|
|
|
func opTrustBasicSetup() *Server {
|
|
kp, _ := nkeys.FromSeed(oSeed)
|
|
pub, _ := kp.PublicKey()
|
|
opts := defaultServerOptions
|
|
opts.TrustedKeys = []string{pub}
|
|
s, c, _, _ := rawSetup(opts)
|
|
c.close()
|
|
return s
|
|
}
|
|
|
|
func buildMemAccResolver(s *Server) {
|
|
mr := &MemAccResolver{}
|
|
s.SetAccountResolver(mr)
|
|
}
|
|
|
|
func addAccountToMemResolver(s *Server, pub, jwtclaim string) {
|
|
s.AccountResolver().Store(pub, jwtclaim)
|
|
}
|
|
|
|
func createClient(t *testing.T, s *Server, akp nkeys.KeyPair) (*testAsyncClient, *bufio.Reader, string) {
|
|
return createClientWithIssuer(t, s, akp, "")
|
|
}
|
|
|
|
func createClientWithIssuer(t *testing.T, s *Server, akp nkeys.KeyPair, optIssuerAccount string) (*testAsyncClient, *bufio.Reader, string) {
|
|
t.Helper()
|
|
nkp, _ := nkeys.CreateUser()
|
|
pub, _ := nkp.PublicKey()
|
|
nuc := jwt.NewUserClaims(pub)
|
|
if optIssuerAccount != "" {
|
|
nuc.IssuerAccount = optIssuerAccount
|
|
}
|
|
ujwt, err := nuc.Encode(akp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating user JWT: %v", err)
|
|
}
|
|
c, cr, l := newClientForServer(s)
|
|
|
|
// Sign Nonce
|
|
var info nonceInfo
|
|
json.Unmarshal([]byte(l[5:]), &info)
|
|
sigraw, _ := nkp.Sign([]byte(info.Nonce))
|
|
sig := base64.RawURLEncoding.EncodeToString(sigraw)
|
|
|
|
cs := fmt.Sprintf("CONNECT {\"jwt\":%q,\"sig\":\"%s\"}\r\nPING\r\n", ujwt, sig)
|
|
return c, cr, cs
|
|
}
|
|
|
|
func setupJWTTestWithClaims(t *testing.T, nac *jwt.AccountClaims, nuc *jwt.UserClaims, expected string) (*Server, nkeys.KeyPair, *testAsyncClient, *bufio.Reader) {
|
|
t.Helper()
|
|
|
|
akp, _ := nkeys.CreateAccount()
|
|
apub, _ := akp.PublicKey()
|
|
if nac == nil {
|
|
nac = jwt.NewAccountClaims(apub)
|
|
} else {
|
|
nac.Subject = apub
|
|
}
|
|
ajwt, err := nac.Encode(oKp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
nkp, _ := nkeys.CreateUser()
|
|
pub, _ := nkp.PublicKey()
|
|
if nuc == nil {
|
|
nuc = jwt.NewUserClaims(pub)
|
|
} else {
|
|
nuc.Subject = pub
|
|
}
|
|
jwt, err := nuc.Encode(akp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating user JWT: %v", err)
|
|
}
|
|
|
|
s := opTrustBasicSetup()
|
|
buildMemAccResolver(s)
|
|
addAccountToMemResolver(s, apub, ajwt)
|
|
|
|
c, cr, l := newClientForServer(s)
|
|
|
|
// Sign Nonce
|
|
var info nonceInfo
|
|
json.Unmarshal([]byte(l[5:]), &info)
|
|
sigraw, _ := nkp.Sign([]byte(info.Nonce))
|
|
sig := base64.RawURLEncoding.EncodeToString(sigraw)
|
|
|
|
// PING needed to flush the +OK/-ERR to us.
|
|
cs := fmt.Sprintf("CONNECT {\"jwt\":%q,\"sig\":\"%s\",\"verbose\":true,\"pedantic\":true}\r\nPING\r\n", jwt, sig)
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(1)
|
|
go func() {
|
|
c.parse([]byte(cs))
|
|
wg.Done()
|
|
}()
|
|
l, _ = cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, expected) {
|
|
t.Fatalf("Expected %q, got %q", expected, l)
|
|
}
|
|
wg.Wait()
|
|
|
|
return s, akp, c, cr
|
|
}
|
|
|
|
func setupJWTTestWitAccountClaims(t *testing.T, nac *jwt.AccountClaims, expected string) (*Server, nkeys.KeyPair, *testAsyncClient, *bufio.Reader) {
|
|
t.Helper()
|
|
return setupJWTTestWithClaims(t, nac, nil, expected)
|
|
}
|
|
|
|
// This is used in test to create account claims and pass it
|
|
// to setupJWTTestWitAccountClaims.
|
|
func newJWTTestAccountClaims() *jwt.AccountClaims {
|
|
// We call NewAccountClaims() because it sets some defaults.
|
|
// However, this call needs a subject, but the real subject will
|
|
// be set in setupJWTTestWitAccountClaims(). Use some temporary one
|
|
// here.
|
|
return jwt.NewAccountClaims("temp")
|
|
}
|
|
|
|
func setupJWTTestWithUserClaims(t *testing.T, nuc *jwt.UserClaims, expected string) (*Server, *testAsyncClient, *bufio.Reader) {
|
|
t.Helper()
|
|
s, _, c, cr := setupJWTTestWithClaims(t, nil, nuc, expected)
|
|
return s, c, cr
|
|
}
|
|
|
|
// This is used in test to create user claims and pass it
|
|
// to setupJWTTestWithUserClaims.
|
|
func newJWTTestUserClaims() *jwt.UserClaims {
|
|
// As of now, tests could simply do &jwt.UserClaims{}, but in
|
|
// case some defaults are later added, we call NewUserClaims().
|
|
// However, this call needs a subject, but the real subject will
|
|
// be set in setupJWTTestWithUserClaims(). Use some temporary one
|
|
// here.
|
|
return jwt.NewUserClaims("temp")
|
|
}
|
|
|
|
func TestJWTUser(t *testing.T) {
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
|
|
// Check to make sure we would have an authTimer
|
|
if !s.info.AuthRequired {
|
|
t.Fatalf("Expect the server to require auth")
|
|
}
|
|
|
|
c, cr, _ := newClientForServer(s)
|
|
defer c.close()
|
|
|
|
// Don't send jwt field, should fail.
|
|
c.parseAsync("CONNECT {\"verbose\":true,\"pedantic\":true}\r\nPING\r\n")
|
|
l, _ := cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "-ERR ") {
|
|
t.Fatalf("Expected an error")
|
|
}
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
|
|
// Create an account that will be expired.
|
|
akp, _ := nkeys.CreateAccount()
|
|
apub, _ := akp.PublicKey()
|
|
nac := jwt.NewAccountClaims(apub)
|
|
ajwt, err := nac.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
c, cr, cs := createClient(t, s, akp)
|
|
defer c.close()
|
|
|
|
// PING needed to flush the +OK/-ERR to us.
|
|
// This should fail too since no account resolver is defined.
|
|
c.parseAsync(cs)
|
|
l, _ = cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "-ERR ") {
|
|
t.Fatalf("Expected an error")
|
|
}
|
|
|
|
// Ok now let's walk through and make sure all is good.
|
|
// We will set the account resolver by hand to a memory resolver.
|
|
buildMemAccResolver(s)
|
|
addAccountToMemResolver(s, apub, ajwt)
|
|
|
|
c, cr, cs = createClient(t, s, akp)
|
|
defer c.close()
|
|
|
|
c.parseAsync(cs)
|
|
l, _ = cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "PONG") {
|
|
t.Fatalf("Expected a PONG, got %q", l)
|
|
}
|
|
}
|
|
|
|
func TestJWTUserBadTrusted(t *testing.T) {
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
|
|
// Check to make sure we would have an authTimer
|
|
if !s.info.AuthRequired {
|
|
t.Fatalf("Expect the server to require auth")
|
|
}
|
|
// Now place bad trusted key
|
|
s.mu.Lock()
|
|
s.trustedKeys = []string{"bad"}
|
|
s.mu.Unlock()
|
|
|
|
buildMemAccResolver(s)
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
|
|
// Create an account that will be expired.
|
|
akp, _ := nkeys.CreateAccount()
|
|
apub, _ := akp.PublicKey()
|
|
nac := jwt.NewAccountClaims(apub)
|
|
ajwt, err := nac.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, apub, ajwt)
|
|
|
|
c, cr, cs := createClient(t, s, akp)
|
|
defer c.close()
|
|
c.parseAsync(cs)
|
|
l, _ := cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "-ERR ") {
|
|
t.Fatalf("Expected an error")
|
|
}
|
|
}
|
|
|
|
// Test that if a user tries to connect with an expired user JWT we do the right thing.
|
|
func TestJWTUserExpired(t *testing.T) {
|
|
nuc := newJWTTestUserClaims()
|
|
nuc.IssuedAt = time.Now().Add(-10 * time.Second).Unix()
|
|
nuc.Expires = time.Now().Add(-2 * time.Second).Unix()
|
|
s, c, _ := setupJWTTestWithUserClaims(t, nuc, "-ERR ")
|
|
c.close()
|
|
s.Shutdown()
|
|
}
|
|
|
|
func TestJWTUserExpiresAfterConnect(t *testing.T) {
|
|
nuc := newJWTTestUserClaims()
|
|
nuc.IssuedAt = time.Now().Unix()
|
|
nuc.Expires = time.Now().Add(time.Second).Unix()
|
|
s, c, cr := setupJWTTestWithUserClaims(t, nuc, "+OK")
|
|
defer s.Shutdown()
|
|
defer c.close()
|
|
l, err := cr.ReadString('\n')
|
|
if err != nil {
|
|
t.Fatalf("Received %v", err)
|
|
}
|
|
if !strings.HasPrefix(l, "PONG") {
|
|
t.Fatalf("Expected a PONG")
|
|
}
|
|
|
|
// Now we should expire after 1 second or so.
|
|
time.Sleep(1250 * time.Millisecond)
|
|
|
|
l, err = cr.ReadString('\n')
|
|
if err != nil {
|
|
t.Fatalf("Received %v", err)
|
|
}
|
|
if !strings.HasPrefix(l, "-ERR ") {
|
|
t.Fatalf("Expected an error")
|
|
}
|
|
if !strings.Contains(l, "Expired") {
|
|
t.Fatalf("Expected 'Expired' to be in the error")
|
|
}
|
|
}
|
|
|
|
func TestJWTUserPermissionClaims(t *testing.T) {
|
|
nuc := newJWTTestUserClaims()
|
|
nuc.Permissions.Pub.Allow.Add("foo")
|
|
nuc.Permissions.Pub.Allow.Add("bar")
|
|
nuc.Permissions.Pub.Deny.Add("baz")
|
|
nuc.Permissions.Sub.Allow.Add("foo")
|
|
nuc.Permissions.Sub.Allow.Add("bar")
|
|
nuc.Permissions.Sub.Deny.Add("baz")
|
|
|
|
s, c, _ := setupJWTTestWithUserClaims(t, nuc, "+OK")
|
|
defer s.Shutdown()
|
|
defer c.close()
|
|
|
|
// Now check client to make sure permissions transferred.
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if c.perms == nil {
|
|
t.Fatalf("Expected client permissions to be set")
|
|
}
|
|
|
|
if lpa := c.perms.pub.allow.Count(); lpa != 2 {
|
|
t.Fatalf("Expected 2 publish allow subjects, got %d", lpa)
|
|
}
|
|
if lpd := c.perms.pub.deny.Count(); lpd != 1 {
|
|
t.Fatalf("Expected 1 publish deny subjects, got %d", lpd)
|
|
}
|
|
if lsa := c.perms.sub.allow.Count(); lsa != 2 {
|
|
t.Fatalf("Expected 2 subscribe allow subjects, got %d", lsa)
|
|
}
|
|
if lsd := c.perms.sub.deny.Count(); lsd != 1 {
|
|
t.Fatalf("Expected 1 subscribe deny subjects, got %d", lsd)
|
|
}
|
|
}
|
|
|
|
func TestJWTUserResponsePermissionClaims(t *testing.T) {
|
|
nuc := newJWTTestUserClaims()
|
|
nuc.Permissions.Resp = &jwt.ResponsePermission{
|
|
MaxMsgs: 22,
|
|
Expires: 100 * time.Millisecond,
|
|
}
|
|
s, c, _ := setupJWTTestWithUserClaims(t, nuc, "+OK")
|
|
defer s.Shutdown()
|
|
defer c.close()
|
|
|
|
// Now check client to make sure permissions transferred.
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if c.perms == nil {
|
|
t.Fatalf("Expected client permissions to be set")
|
|
}
|
|
if c.perms.pub.allow == nil {
|
|
t.Fatalf("Expected client perms for pub allow to be non-nil")
|
|
}
|
|
if lpa := c.perms.pub.allow.Count(); lpa != 0 {
|
|
t.Fatalf("Expected 0 publish allow subjects, got %d", lpa)
|
|
}
|
|
if c.perms.resp == nil {
|
|
t.Fatalf("Expected client perms for response permissions to be non-nil")
|
|
}
|
|
if c.perms.resp.MaxMsgs != nuc.Permissions.Resp.MaxMsgs {
|
|
t.Fatalf("Expected client perms for response permissions MaxMsgs to be same as jwt: %d vs %d",
|
|
c.perms.resp.MaxMsgs, nuc.Permissions.Resp.MaxMsgs)
|
|
}
|
|
if c.perms.resp.Expires != nuc.Permissions.Resp.Expires {
|
|
t.Fatalf("Expected client perms for response permissions Expires to be same as jwt: %v vs %v",
|
|
c.perms.resp.Expires, nuc.Permissions.Resp.Expires)
|
|
}
|
|
}
|
|
|
|
func TestJWTUserResponsePermissionClaimsDefaultValues(t *testing.T) {
|
|
nuc := newJWTTestUserClaims()
|
|
nuc.Permissions.Resp = &jwt.ResponsePermission{}
|
|
s, c, _ := setupJWTTestWithUserClaims(t, nuc, "+OK")
|
|
defer s.Shutdown()
|
|
defer c.close()
|
|
|
|
// Now check client to make sure permissions transferred
|
|
// and defaults are set.
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if c.perms == nil {
|
|
t.Fatalf("Expected client permissions to be set")
|
|
}
|
|
if c.perms.pub.allow == nil {
|
|
t.Fatalf("Expected client perms for pub allow to be non-nil")
|
|
}
|
|
if lpa := c.perms.pub.allow.Count(); lpa != 0 {
|
|
t.Fatalf("Expected 0 publish allow subjects, got %d", lpa)
|
|
}
|
|
if c.perms.resp == nil {
|
|
t.Fatalf("Expected client perms for response permissions to be non-nil")
|
|
}
|
|
if c.perms.resp.MaxMsgs != DEFAULT_ALLOW_RESPONSE_MAX_MSGS {
|
|
t.Fatalf("Expected client perms for response permissions MaxMsgs to be default %v, got %v",
|
|
DEFAULT_ALLOW_RESPONSE_MAX_MSGS, c.perms.resp.MaxMsgs)
|
|
}
|
|
if c.perms.resp.Expires != DEFAULT_ALLOW_RESPONSE_EXPIRATION {
|
|
t.Fatalf("Expected client perms for response permissions Expires to be default %v, got %v",
|
|
DEFAULT_ALLOW_RESPONSE_EXPIRATION, c.perms.resp.Expires)
|
|
}
|
|
}
|
|
|
|
func TestJWTUserResponsePermissionClaimsNegativeValues(t *testing.T) {
|
|
nuc := newJWTTestUserClaims()
|
|
nuc.Permissions.Resp = &jwt.ResponsePermission{
|
|
MaxMsgs: -1,
|
|
Expires: -1 * time.Second,
|
|
}
|
|
s, c, _ := setupJWTTestWithUserClaims(t, nuc, "+OK")
|
|
defer s.Shutdown()
|
|
defer c.close()
|
|
|
|
// Now check client to make sure permissions transferred
|
|
// and negative values are transferred.
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if c.perms == nil {
|
|
t.Fatalf("Expected client permissions to be set")
|
|
}
|
|
if c.perms.pub.allow == nil {
|
|
t.Fatalf("Expected client perms for pub allow to be non-nil")
|
|
}
|
|
if lpa := c.perms.pub.allow.Count(); lpa != 0 {
|
|
t.Fatalf("Expected 0 publish allow subjects, got %d", lpa)
|
|
}
|
|
if c.perms.resp == nil {
|
|
t.Fatalf("Expected client perms for response permissions to be non-nil")
|
|
}
|
|
if c.perms.resp.MaxMsgs != -1 {
|
|
t.Fatalf("Expected client perms for response permissions MaxMsgs to be %v, got %v",
|
|
-1, c.perms.resp.MaxMsgs)
|
|
}
|
|
if c.perms.resp.Expires != -1*time.Second {
|
|
t.Fatalf("Expected client perms for response permissions Expires to be %v, got %v",
|
|
-1*time.Second, c.perms.resp.Expires)
|
|
}
|
|
}
|
|
|
|
func TestJWTAccountExpired(t *testing.T) {
|
|
nac := newJWTTestAccountClaims()
|
|
nac.IssuedAt = time.Now().Add(-10 * time.Second).Unix()
|
|
nac.Expires = time.Now().Add(-2 * time.Second).Unix()
|
|
s, _, c, _ := setupJWTTestWitAccountClaims(t, nac, "-ERR ")
|
|
defer s.Shutdown()
|
|
defer c.close()
|
|
}
|
|
|
|
func TestJWTAccountExpiresAfterConnect(t *testing.T) {
|
|
nac := newJWTTestAccountClaims()
|
|
now := time.Now()
|
|
nac.IssuedAt = now.Add(-10 * time.Second).Unix()
|
|
nac.Expires = now.Round(time.Second).Add(time.Second).Unix()
|
|
s, akp, c, cr := setupJWTTestWitAccountClaims(t, nac, "+OK")
|
|
defer s.Shutdown()
|
|
defer c.close()
|
|
|
|
apub, _ := akp.PublicKey()
|
|
acc, err := s.LookupAccount(apub)
|
|
if acc == nil || err != nil {
|
|
t.Fatalf("Expected to retrieve the account")
|
|
}
|
|
|
|
if l, _ := cr.ReadString('\n'); !strings.HasPrefix(l, "PONG") {
|
|
t.Fatalf("Expected PONG, got %q", l)
|
|
}
|
|
|
|
// Wait for the account to be expired.
|
|
checkFor(t, 3*time.Second, 100*time.Millisecond, func() error {
|
|
if acc.IsExpired() {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Account not expired yet")
|
|
})
|
|
|
|
l, _ := cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "-ERR ") {
|
|
t.Fatalf("Expected an error, got %q", l)
|
|
}
|
|
if !strings.Contains(l, "Expired") {
|
|
t.Fatalf("Expected 'Expired' to be in the error")
|
|
}
|
|
|
|
// Now make sure that accounts that have expired return an error.
|
|
c, cr, cs := createClient(t, s, akp)
|
|
defer c.close()
|
|
c.parseAsync(cs)
|
|
l, _ = cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "-ERR ") {
|
|
t.Fatalf("Expected an error")
|
|
}
|
|
}
|
|
|
|
func TestJWTAccountRenew(t *testing.T) {
|
|
nac := newJWTTestAccountClaims()
|
|
// Create an account that has expired.
|
|
nac.IssuedAt = time.Now().Add(-10 * time.Second).Unix()
|
|
nac.Expires = time.Now().Add(-2 * time.Second).Unix()
|
|
// Expect an error
|
|
s, akp, c, _ := setupJWTTestWitAccountClaims(t, nac, "-ERR ")
|
|
defer s.Shutdown()
|
|
defer c.close()
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
apub, _ := akp.PublicKey()
|
|
|
|
// Now update with new expiration
|
|
nac.IssuedAt = time.Now().Unix()
|
|
nac.Expires = time.Now().Add(5 * time.Second).Unix()
|
|
ajwt, err := nac.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
// Update the account
|
|
addAccountToMemResolver(s, apub, ajwt)
|
|
acc, _ := s.LookupAccount(apub)
|
|
if acc == nil {
|
|
t.Fatalf("Expected to retrieve the account")
|
|
}
|
|
s.UpdateAccountClaims(acc, nac)
|
|
|
|
// Now make sure we can connect.
|
|
c, cr, cs := createClient(t, s, akp)
|
|
defer c.close()
|
|
c.parseAsync(cs)
|
|
if l, _ := cr.ReadString('\n'); !strings.HasPrefix(l, "PONG") {
|
|
t.Fatalf("Expected a PONG, got: %q", l)
|
|
}
|
|
}
|
|
|
|
func TestJWTAccountRenewFromResolver(t *testing.T) {
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
|
|
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(time.Second).Unix()
|
|
ajwt, err := nac.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
addAccountToMemResolver(s, apub, ajwt)
|
|
// Force it to be loaded by the server and start the expiration timer.
|
|
acc, _ := s.LookupAccount(apub)
|
|
if acc == nil {
|
|
t.Fatalf("Could not retrieve account for %q", apub)
|
|
}
|
|
|
|
// Create a new user
|
|
c, cr, cs := createClient(t, s, akp)
|
|
defer c.close()
|
|
// Wait for expiration.
|
|
time.Sleep(1250 * time.Millisecond)
|
|
|
|
c.parseAsync(cs)
|
|
l, _ := cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "-ERR ") {
|
|
t.Fatalf("Expected an error")
|
|
}
|
|
|
|
// Now update with new expiration
|
|
nac.IssuedAt = time.Now().Unix()
|
|
nac.Expires = time.Now().Add(5 * time.Second).Unix()
|
|
ajwt, err = nac.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
// Update the account
|
|
addAccountToMemResolver(s, apub, ajwt)
|
|
// Make sure the too quick update suppression does not bite us.
|
|
acc.mu.Lock()
|
|
acc.updated = time.Now().UTC().Add(-1 * time.Hour)
|
|
acc.mu.Unlock()
|
|
|
|
// Do not update the account directly. The resolver should
|
|
// happen automatically.
|
|
|
|
// Now make sure we can connect.
|
|
c, cr, cs = createClient(t, s, akp)
|
|
defer c.close()
|
|
c.parseAsync(cs)
|
|
l, _ = cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "PONG") {
|
|
t.Fatalf("Expected a PONG, got: %q", l)
|
|
}
|
|
}
|
|
|
|
func TestJWTAccountBasicImportExport(t *testing.T) {
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
|
|
// Create accounts and imports/exports.
|
|
fooKP, _ := nkeys.CreateAccount()
|
|
fooPub, _ := fooKP.PublicKey()
|
|
fooAC := jwt.NewAccountClaims(fooPub)
|
|
|
|
// Now create Exports.
|
|
streamExport := &jwt.Export{Subject: "foo", Type: jwt.Stream}
|
|
streamExport2 := &jwt.Export{Subject: "private", Type: jwt.Stream, TokenReq: true}
|
|
serviceExport := &jwt.Export{Subject: "req.echo", Type: jwt.Service, TokenReq: true}
|
|
serviceExport2 := &jwt.Export{Subject: "req.add", Type: jwt.Service, TokenReq: true}
|
|
|
|
fooAC.Exports.Add(streamExport, streamExport2, serviceExport, serviceExport2)
|
|
fooJWT, err := fooAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
addAccountToMemResolver(s, fooPub, fooJWT)
|
|
|
|
acc, _ := s.LookupAccount(fooPub)
|
|
if acc == nil {
|
|
t.Fatalf("Expected to retrieve the account")
|
|
}
|
|
|
|
// Check to make sure exports transferred over.
|
|
if les := len(acc.exports.streams); les != 2 {
|
|
t.Fatalf("Expected exports streams len of 2, got %d", les)
|
|
}
|
|
if les := len(acc.exports.services); les != 2 {
|
|
t.Fatalf("Expected exports services len of 2, got %d", les)
|
|
}
|
|
_, ok := acc.exports.streams["foo"]
|
|
if !ok {
|
|
t.Fatalf("Expected to map a stream export")
|
|
}
|
|
se, ok := acc.exports.services["req.echo"]
|
|
if !ok || se == nil {
|
|
t.Fatalf("Expected to map a service export")
|
|
}
|
|
if !se.tokenReq {
|
|
t.Fatalf("Expected the service export to require tokens")
|
|
}
|
|
|
|
barKP, _ := nkeys.CreateAccount()
|
|
barPub, _ := barKP.PublicKey()
|
|
barAC := jwt.NewAccountClaims(barPub)
|
|
|
|
streamImport := &jwt.Import{Account: fooPub, Subject: "foo", To: "import.foo", Type: jwt.Stream}
|
|
serviceImport := &jwt.Import{Account: fooPub, Subject: "req.echo", Type: jwt.Service}
|
|
barAC.Imports.Add(streamImport, serviceImport)
|
|
barJWT, err := barAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, barPub, barJWT)
|
|
|
|
acc, _ = s.LookupAccount(barPub)
|
|
if acc == nil {
|
|
t.Fatalf("Expected to retrieve the account")
|
|
}
|
|
if les := len(acc.imports.streams); les != 1 {
|
|
t.Fatalf("Expected imports streams len of 1, got %d", les)
|
|
}
|
|
// Our service import should have failed without a token.
|
|
if les := len(acc.imports.services); les != 0 {
|
|
t.Fatalf("Expected imports services len of 0, got %d", les)
|
|
}
|
|
|
|
// Now add in a bad activation token.
|
|
barAC = jwt.NewAccountClaims(barPub)
|
|
serviceImport = &jwt.Import{Account: fooPub, Subject: "req.echo", Token: "not a token", Type: jwt.Service}
|
|
barAC.Imports.Add(serviceImport)
|
|
barJWT, err = barAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, barPub, barJWT)
|
|
|
|
s.UpdateAccountClaims(acc, barAC)
|
|
|
|
// Our service import should have failed with a bad token.
|
|
if les := len(acc.imports.services); les != 0 {
|
|
t.Fatalf("Expected imports services len of 0, got %d", les)
|
|
}
|
|
|
|
// Now make a correct one.
|
|
barAC = jwt.NewAccountClaims(barPub)
|
|
serviceImport = &jwt.Import{Account: fooPub, Subject: "req.echo", Type: jwt.Service}
|
|
|
|
activation := jwt.NewActivationClaims(barPub)
|
|
activation.ImportSubject = "req.echo"
|
|
activation.ImportType = jwt.Service
|
|
actJWT, err := activation.Encode(fooKP)
|
|
if err != nil {
|
|
t.Fatalf("Error generating activation token: %v", err)
|
|
}
|
|
serviceImport.Token = actJWT
|
|
barAC.Imports.Add(serviceImport)
|
|
barJWT, err = barAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
vr := jwt.ValidationResults{}
|
|
barAC.Validate(&vr)
|
|
if vr.IsBlocking(true) {
|
|
t.Fatalf("Error generating account JWT: %v", vr)
|
|
}
|
|
|
|
addAccountToMemResolver(s, barPub, barJWT)
|
|
s.UpdateAccountClaims(acc, barAC)
|
|
// Our service import should have succeeded.
|
|
if les := len(acc.imports.services); les != 1 {
|
|
t.Fatalf("Expected imports services len of 1, got %d", les)
|
|
}
|
|
|
|
// Now streams
|
|
barAC = jwt.NewAccountClaims(barPub)
|
|
streamImport = &jwt.Import{Account: fooPub, Subject: "private", To: "import.private", Type: jwt.Stream}
|
|
|
|
barAC.Imports.Add(streamImport)
|
|
barJWT, err = barAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, barPub, barJWT)
|
|
s.UpdateAccountClaims(acc, barAC)
|
|
// Our stream import should have not succeeded.
|
|
if les := len(acc.imports.streams); les != 0 {
|
|
t.Fatalf("Expected imports services len of 0, got %d", les)
|
|
}
|
|
|
|
// Now add in activation.
|
|
barAC = jwt.NewAccountClaims(barPub)
|
|
streamImport = &jwt.Import{Account: fooPub, Subject: "private", To: "import.private", Type: jwt.Stream}
|
|
|
|
activation = jwt.NewActivationClaims(barPub)
|
|
activation.ImportSubject = "private"
|
|
activation.ImportType = jwt.Stream
|
|
actJWT, err = activation.Encode(fooKP)
|
|
if err != nil {
|
|
t.Fatalf("Error generating activation token: %v", err)
|
|
}
|
|
streamImport.Token = actJWT
|
|
barAC.Imports.Add(streamImport)
|
|
barJWT, err = barAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, barPub, barJWT)
|
|
s.UpdateAccountClaims(acc, barAC)
|
|
// Our stream import should have not succeeded.
|
|
if les := len(acc.imports.streams); les != 1 {
|
|
t.Fatalf("Expected imports services len of 1, got %d", les)
|
|
}
|
|
}
|
|
|
|
func TestJWTAccountExportWithResponseType(t *testing.T) {
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
|
|
// Create accounts and imports/exports.
|
|
fooKP, _ := nkeys.CreateAccount()
|
|
fooPub, _ := fooKP.PublicKey()
|
|
fooAC := jwt.NewAccountClaims(fooPub)
|
|
|
|
// Now create Exports.
|
|
serviceStreamExport := &jwt.Export{Subject: "test.stream", Type: jwt.Service, ResponseType: jwt.ResponseTypeStream, TokenReq: false}
|
|
serviceChunkExport := &jwt.Export{Subject: "test.chunk", Type: jwt.Service, ResponseType: jwt.ResponseTypeChunked, TokenReq: false}
|
|
serviceSingletonExport := &jwt.Export{Subject: "test.single", Type: jwt.Service, ResponseType: jwt.ResponseTypeSingleton, TokenReq: true}
|
|
serviceDefExport := &jwt.Export{Subject: "test.def", Type: jwt.Service, TokenReq: true}
|
|
serviceOldExport := &jwt.Export{Subject: "test.old", Type: jwt.Service, TokenReq: false}
|
|
|
|
fooAC.Exports.Add(serviceStreamExport, serviceSingletonExport, serviceChunkExport, serviceDefExport, serviceOldExport)
|
|
fooJWT, err := fooAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
addAccountToMemResolver(s, fooPub, fooJWT)
|
|
|
|
fooAcc, _ := s.LookupAccount(fooPub)
|
|
if fooAcc == nil {
|
|
t.Fatalf("Expected to retrieve the account")
|
|
}
|
|
|
|
services := fooAcc.exports.services
|
|
|
|
if len(services) != 5 {
|
|
t.Fatalf("Expected 4 services")
|
|
}
|
|
|
|
se, ok := services["test.stream"]
|
|
if !ok || se == nil {
|
|
t.Fatalf("Expected to map a service export")
|
|
}
|
|
if se.tokenReq {
|
|
t.Fatalf("Expected the service export to not require tokens")
|
|
}
|
|
if se.respType != Streamed {
|
|
t.Fatalf("Expected the service export to respond with a stream")
|
|
}
|
|
|
|
se, ok = services["test.chunk"]
|
|
if !ok || se == nil {
|
|
t.Fatalf("Expected to map a service export")
|
|
}
|
|
if se.tokenReq {
|
|
t.Fatalf("Expected the service export to not require tokens")
|
|
}
|
|
if se.respType != Chunked {
|
|
t.Fatalf("Expected the service export to respond with a stream")
|
|
}
|
|
|
|
se, ok = services["test.def"]
|
|
if !ok || se == nil {
|
|
t.Fatalf("Expected to map a service export")
|
|
}
|
|
if !se.tokenReq {
|
|
t.Fatalf("Expected the service export to not require tokens")
|
|
}
|
|
if se.respType != Singleton {
|
|
t.Fatalf("Expected the service export to respond with a stream")
|
|
}
|
|
|
|
se, ok = services["test.single"]
|
|
if !ok || se == nil {
|
|
t.Fatalf("Expected to map a service export")
|
|
}
|
|
if !se.tokenReq {
|
|
t.Fatalf("Expected the service export to not require tokens")
|
|
}
|
|
if se.respType != Singleton {
|
|
t.Fatalf("Expected the service export to respond with a stream")
|
|
}
|
|
|
|
se, ok = services["test.old"]
|
|
if !ok || se == nil || len(se.approved) > 0 {
|
|
t.Fatalf("Service with a singleton response and no tokens should not be nil and have no approvals")
|
|
}
|
|
}
|
|
|
|
func expectPong(t *testing.T, cr *bufio.Reader) {
|
|
t.Helper()
|
|
l, _ := cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "PONG") {
|
|
t.Fatalf("Expected a PONG, got %q", l)
|
|
}
|
|
}
|
|
|
|
func expectMsg(t *testing.T, cr *bufio.Reader, sub, payload string) {
|
|
t.Helper()
|
|
l, _ := cr.ReadString('\n')
|
|
expected := "MSG " + sub
|
|
if !strings.HasPrefix(l, expected) {
|
|
t.Fatalf("Expected %q, got %q", expected, l)
|
|
}
|
|
l, _ = cr.ReadString('\n')
|
|
if l != payload+"\r\n" {
|
|
t.Fatalf("Expected %q, got %q", payload, l)
|
|
}
|
|
expectPong(t, cr)
|
|
}
|
|
|
|
func TestJWTAccountImportExportUpdates(t *testing.T) {
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
|
|
// Create accounts and imports/exports.
|
|
fooKP, _ := nkeys.CreateAccount()
|
|
fooPub, _ := fooKP.PublicKey()
|
|
fooAC := jwt.NewAccountClaims(fooPub)
|
|
streamExport := &jwt.Export{Subject: "foo", Type: jwt.Stream}
|
|
|
|
fooAC.Exports.Add(streamExport)
|
|
fooJWT, err := fooAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, fooPub, fooJWT)
|
|
|
|
barKP, _ := nkeys.CreateAccount()
|
|
barPub, _ := barKP.PublicKey()
|
|
barAC := jwt.NewAccountClaims(barPub)
|
|
streamImport := &jwt.Import{Account: fooPub, Subject: "foo", To: "import", Type: jwt.Stream}
|
|
|
|
barAC.Imports.Add(streamImport)
|
|
barJWT, err := barAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, barPub, barJWT)
|
|
|
|
// Create a client.
|
|
c, cr, cs := createClient(t, s, barKP)
|
|
defer c.close()
|
|
|
|
c.parseAsync(cs)
|
|
expectPong(t, cr)
|
|
|
|
c.parseAsync("SUB import.foo 1\r\nPING\r\n")
|
|
expectPong(t, cr)
|
|
|
|
checkShadow := func(expected int) {
|
|
t.Helper()
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
sub := c.subs["1"]
|
|
if ls := len(sub.shadow); ls != expected {
|
|
t.Fatalf("Expected shadows to be %d, got %d", expected, ls)
|
|
}
|
|
}
|
|
|
|
// We created a SUB on foo which should create a shadow subscription.
|
|
checkShadow(1)
|
|
|
|
// Now update bar and remove the import which should make the shadow go away.
|
|
barAC = jwt.NewAccountClaims(barPub)
|
|
barJWT, _ = barAC.Encode(okp)
|
|
addAccountToMemResolver(s, barPub, barJWT)
|
|
acc, _ := s.LookupAccount(barPub)
|
|
s.UpdateAccountClaims(acc, barAC)
|
|
|
|
checkShadow(0)
|
|
|
|
// Now add it back and make sure the shadow comes back.
|
|
streamImport = &jwt.Import{Account: string(fooPub), Subject: "foo", To: "import", Type: jwt.Stream}
|
|
barAC.Imports.Add(streamImport)
|
|
barJWT, _ = barAC.Encode(okp)
|
|
addAccountToMemResolver(s, barPub, barJWT)
|
|
s.UpdateAccountClaims(acc, barAC)
|
|
|
|
checkShadow(1)
|
|
|
|
// Now change export and make sure it goes away as well. So no exports anymore.
|
|
fooAC = jwt.NewAccountClaims(fooPub)
|
|
fooJWT, _ = fooAC.Encode(okp)
|
|
addAccountToMemResolver(s, fooPub, fooJWT)
|
|
acc, _ = s.LookupAccount(fooPub)
|
|
s.UpdateAccountClaims(acc, fooAC)
|
|
checkShadow(0)
|
|
|
|
// Now add it in but with permission required.
|
|
streamExport = &jwt.Export{Subject: "foo", Type: jwt.Stream, TokenReq: true}
|
|
fooAC.Exports.Add(streamExport)
|
|
fooJWT, _ = fooAC.Encode(okp)
|
|
addAccountToMemResolver(s, fooPub, fooJWT)
|
|
s.UpdateAccountClaims(acc, fooAC)
|
|
|
|
checkShadow(0)
|
|
|
|
// Now put it back as normal.
|
|
fooAC = jwt.NewAccountClaims(fooPub)
|
|
streamExport = &jwt.Export{Subject: "foo", Type: jwt.Stream}
|
|
fooAC.Exports.Add(streamExport)
|
|
fooJWT, _ = fooAC.Encode(okp)
|
|
addAccountToMemResolver(s, fooPub, fooJWT)
|
|
s.UpdateAccountClaims(acc, fooAC)
|
|
|
|
checkShadow(1)
|
|
}
|
|
|
|
func TestJWTAccountImportActivationExpires(t *testing.T) {
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
|
|
// Create accounts and imports/exports.
|
|
fooKP, _ := nkeys.CreateAccount()
|
|
fooPub, _ := fooKP.PublicKey()
|
|
fooAC := jwt.NewAccountClaims(fooPub)
|
|
streamExport := &jwt.Export{Subject: "foo", Type: jwt.Stream, TokenReq: true}
|
|
fooAC.Exports.Add(streamExport)
|
|
|
|
fooJWT, err := fooAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
addAccountToMemResolver(s, fooPub, fooJWT)
|
|
acc, _ := s.LookupAccount(fooPub)
|
|
if acc == nil {
|
|
t.Fatalf("Expected to retrieve the account")
|
|
}
|
|
|
|
barKP, _ := nkeys.CreateAccount()
|
|
barPub, _ := barKP.PublicKey()
|
|
barAC := jwt.NewAccountClaims(barPub)
|
|
streamImport := &jwt.Import{Account: fooPub, Subject: "foo", To: "import.", Type: jwt.Stream}
|
|
|
|
activation := jwt.NewActivationClaims(barPub)
|
|
activation.ImportSubject = "foo"
|
|
activation.ImportType = jwt.Stream
|
|
now := time.Now()
|
|
activation.IssuedAt = now.Add(-10 * time.Second).Unix()
|
|
// These are second resolution. So round up before adding a second.
|
|
activation.Expires = now.Round(time.Second).Add(time.Second).Unix()
|
|
actJWT, err := activation.Encode(fooKP)
|
|
if err != nil {
|
|
t.Fatalf("Error generating activation token: %v", err)
|
|
}
|
|
streamImport.Token = actJWT
|
|
barAC.Imports.Add(streamImport)
|
|
barJWT, err := barAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, barPub, barJWT)
|
|
if acc, _ := s.LookupAccount(barPub); acc == nil {
|
|
t.Fatalf("Expected to retrieve the account")
|
|
}
|
|
|
|
// Create a client.
|
|
c, cr, cs := createClient(t, s, barKP)
|
|
defer c.close()
|
|
|
|
c.parseAsync(cs)
|
|
expectPong(t, cr)
|
|
|
|
c.parseAsync("SUB import.foo 1\r\nPING\r\n")
|
|
expectPong(t, cr)
|
|
|
|
checkShadow := func(t *testing.T, expected int) {
|
|
t.Helper()
|
|
checkFor(t, 3*time.Second, 15*time.Millisecond, func() error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
sub := c.subs["1"]
|
|
if ls := len(sub.shadow); ls != expected {
|
|
return fmt.Errorf("Expected shadows to be %d, got %d", expected, ls)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// We created a SUB on foo which should create a shadow subscription.
|
|
checkShadow(t, 1)
|
|
|
|
time.Sleep(1250 * time.Millisecond)
|
|
|
|
// Should have expired and been removed.
|
|
checkShadow(t, 0)
|
|
}
|
|
|
|
func TestJWTAccountLimitsSubs(t *testing.T) {
|
|
fooAC := newJWTTestAccountClaims()
|
|
fooAC.Limits.Subs = 10
|
|
s, fooKP, c, _ := setupJWTTestWitAccountClaims(t, fooAC, "+OK")
|
|
defer s.Shutdown()
|
|
defer c.close()
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
fooPub, _ := fooKP.PublicKey()
|
|
|
|
// Create a client.
|
|
c, cr, cs := createClient(t, s, fooKP)
|
|
defer c.close()
|
|
|
|
c.parseAsync(cs)
|
|
expectPong(t, cr)
|
|
|
|
// Check to make sure we have the limit set.
|
|
// Account first
|
|
fooAcc, _ := s.LookupAccount(fooPub)
|
|
fooAcc.mu.RLock()
|
|
if fooAcc.msubs != 10 {
|
|
fooAcc.mu.RUnlock()
|
|
t.Fatalf("Expected account to have msubs of 10, got %d", fooAcc.msubs)
|
|
}
|
|
fooAcc.mu.RUnlock()
|
|
// Now test that the client has limits too.
|
|
c.mu.Lock()
|
|
if c.msubs != 10 {
|
|
c.mu.Unlock()
|
|
t.Fatalf("Expected client msubs to be 10, got %d", c.msubs)
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
// Now make sure its enforced.
|
|
/// These should all work ok.
|
|
for i := 0; i < 10; i++ {
|
|
c.parseAsync(fmt.Sprintf("SUB foo %d\r\nPING\r\n", i))
|
|
expectPong(t, cr)
|
|
}
|
|
|
|
// This one should fail.
|
|
c.parseAsync("SUB foo 22\r\n")
|
|
l, _ := cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "-ERR") {
|
|
t.Fatalf("Expected an ERR, got: %v", l)
|
|
}
|
|
if !strings.Contains(l, "maximum subscriptions exceeded") {
|
|
t.Fatalf("Expected an ERR for max subscriptions exceeded, got: %v", l)
|
|
}
|
|
|
|
// Now update the claims and expect if max is lower to be disconnected.
|
|
fooAC.Limits.Subs = 5
|
|
fooJWT, err := fooAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, fooPub, fooJWT)
|
|
s.UpdateAccountClaims(fooAcc, fooAC)
|
|
l, _ = cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "-ERR") {
|
|
t.Fatalf("Expected an ERR, got: %v", l)
|
|
}
|
|
if !strings.Contains(l, "maximum subscriptions exceeded") {
|
|
t.Fatalf("Expected an ERR for max subscriptions exceeded, got: %v", l)
|
|
}
|
|
}
|
|
|
|
func TestJWTAccountLimitsSubsButServerOverrides(t *testing.T) {
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
|
|
// override with server setting of 2.
|
|
opts := s.getOpts()
|
|
opts.MaxSubs = 2
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
|
|
// Create accounts and imports/exports.
|
|
fooKP, _ := nkeys.CreateAccount()
|
|
fooPub, _ := fooKP.PublicKey()
|
|
fooAC := jwt.NewAccountClaims(fooPub)
|
|
fooAC.Limits.Subs = 10
|
|
fooJWT, err := fooAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, fooPub, fooJWT)
|
|
fooAcc, _ := s.LookupAccount(fooPub)
|
|
fooAcc.mu.RLock()
|
|
if fooAcc.msubs != 10 {
|
|
fooAcc.mu.RUnlock()
|
|
t.Fatalf("Expected account to have msubs of 10, got %d", fooAcc.msubs)
|
|
}
|
|
fooAcc.mu.RUnlock()
|
|
|
|
// Create a client.
|
|
c, cr, cs := createClient(t, s, fooKP)
|
|
defer c.close()
|
|
|
|
c.parseAsync(cs)
|
|
expectPong(t, cr)
|
|
|
|
c.parseAsync("SUB foo 1\r\nSUB bar 2\r\nSUB baz 3\r\nPING\r\n")
|
|
l, _ := cr.ReadString('\n')
|
|
|
|
if !strings.HasPrefix(l, "-ERR ") {
|
|
t.Fatalf("Expected an error")
|
|
}
|
|
if !strings.Contains(l, "maximum subscriptions exceeded") {
|
|
t.Fatalf("Expected an ERR for max subscriptions exceeded, got: %v", l)
|
|
}
|
|
// Read last PONG so does not hold up test.
|
|
cr.ReadString('\n')
|
|
}
|
|
|
|
func TestJWTAccountLimitsMaxPayload(t *testing.T) {
|
|
fooAC := newJWTTestAccountClaims()
|
|
fooAC.Limits.Payload = 8
|
|
s, fooKP, c, _ := setupJWTTestWitAccountClaims(t, fooAC, "+OK")
|
|
defer s.Shutdown()
|
|
defer c.close()
|
|
|
|
fooPub, _ := fooKP.PublicKey()
|
|
|
|
// Create a client.
|
|
c, cr, cs := createClient(t, s, fooKP)
|
|
defer c.close()
|
|
|
|
c.parseAsync(cs)
|
|
expectPong(t, cr)
|
|
|
|
// Check to make sure we have the limit set.
|
|
// Account first
|
|
fooAcc, _ := s.LookupAccount(fooPub)
|
|
fooAcc.mu.RLock()
|
|
if fooAcc.mpay != 8 {
|
|
fooAcc.mu.RUnlock()
|
|
t.Fatalf("Expected account to have mpay of 8, got %d", fooAcc.mpay)
|
|
}
|
|
fooAcc.mu.RUnlock()
|
|
// Now test that the client has limits too.
|
|
c.mu.Lock()
|
|
if c.mpay != 8 {
|
|
c.mu.Unlock()
|
|
t.Fatalf("Expected client to have mpay of 10, got %d", c.mpay)
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
c.parseAsync("PUB foo 4\r\nXXXX\r\nPING\r\n")
|
|
expectPong(t, cr)
|
|
|
|
c.parseAsync("PUB foo 10\r\nXXXXXXXXXX\r\nPING\r\n")
|
|
l, _ := cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "-ERR ") {
|
|
t.Fatalf("Expected an error")
|
|
}
|
|
if !strings.Contains(l, "Maximum Payload") {
|
|
t.Fatalf("Expected an ERR for max payload violation, got: %v", l)
|
|
}
|
|
}
|
|
|
|
func TestJWTAccountLimitsMaxPayloadButServerOverrides(t *testing.T) {
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
|
|
// override with server setting of 4.
|
|
opts := s.getOpts()
|
|
opts.MaxPayload = 4
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
|
|
// Create accounts and imports/exports.
|
|
fooKP, _ := nkeys.CreateAccount()
|
|
fooPub, _ := fooKP.PublicKey()
|
|
fooAC := jwt.NewAccountClaims(fooPub)
|
|
fooAC.Limits.Payload = 8
|
|
fooJWT, err := fooAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, fooPub, fooJWT)
|
|
|
|
// Create a client.
|
|
c, cr, cs := createClient(t, s, fooKP)
|
|
defer c.close()
|
|
|
|
c.parseAsync(cs)
|
|
expectPong(t, cr)
|
|
|
|
c.parseAsync("PUB foo 6\r\nXXXXXX\r\nPING\r\n")
|
|
l, _ := cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "-ERR ") {
|
|
t.Fatalf("Expected an error")
|
|
}
|
|
if !strings.Contains(l, "Maximum Payload") {
|
|
t.Fatalf("Expected an ERR for max payload violation, got: %v", l)
|
|
}
|
|
}
|
|
|
|
func TestJWTAccountLimitsMaxConns(t *testing.T) {
|
|
fooAC := newJWTTestAccountClaims()
|
|
fooAC.Limits.Conn = 8
|
|
s, fooKP, c, _ := setupJWTTestWitAccountClaims(t, fooAC, "+OK")
|
|
defer s.Shutdown()
|
|
defer c.close()
|
|
|
|
newClient := func(expPre string) *testAsyncClient {
|
|
t.Helper()
|
|
// Create a client.
|
|
c, cr, cs := createClient(t, s, fooKP)
|
|
c.parseAsync(cs)
|
|
l, _ := cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, expPre) {
|
|
t.Fatalf("Expected a response starting with %q, got %q", expPre, l)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// A connection is created in setupJWTTestWitAccountClaims(), so limit
|
|
// to 7 here (8 total).
|
|
for i := 0; i < 7; i++ {
|
|
c := newClient("PONG")
|
|
defer c.close()
|
|
}
|
|
// Now this one should fail.
|
|
c = newClient("-ERR ")
|
|
c.close()
|
|
}
|
|
|
|
// This will test that we can switch from a public export to a private
|
|
// one and back with export claims to make sure the claim update mechanism
|
|
// is working properly.
|
|
func TestJWTAccountServiceImportAuthSwitch(t *testing.T) {
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
|
|
// Create accounts and imports/exports.
|
|
fooKP, _ := nkeys.CreateAccount()
|
|
fooPub, _ := fooKP.PublicKey()
|
|
fooAC := jwt.NewAccountClaims(fooPub)
|
|
serviceExport := &jwt.Export{Subject: "ngs.usage.*", Type: jwt.Service}
|
|
fooAC.Exports.Add(serviceExport)
|
|
fooJWT, err := fooAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, fooPub, fooJWT)
|
|
|
|
barKP, _ := nkeys.CreateAccount()
|
|
barPub, _ := barKP.PublicKey()
|
|
barAC := jwt.NewAccountClaims(barPub)
|
|
serviceImport := &jwt.Import{Account: fooPub, Subject: "ngs.usage", To: "ngs.usage.DEREK", Type: jwt.Service}
|
|
barAC.Imports.Add(serviceImport)
|
|
barJWT, err := barAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, barPub, barJWT)
|
|
|
|
// Create a client that will send the request
|
|
ca, cra, csa := createClient(t, s, barKP)
|
|
defer ca.close()
|
|
ca.parseAsync(csa)
|
|
expectPong(t, cra)
|
|
|
|
// Create the client that will respond to the requests.
|
|
cb, crb, csb := createClient(t, s, fooKP)
|
|
defer cb.close()
|
|
cb.parseAsync(csb)
|
|
expectPong(t, crb)
|
|
|
|
// Create Subscriber.
|
|
cb.parseAsync("SUB ngs.usage.* 1\r\nPING\r\n")
|
|
expectPong(t, crb)
|
|
|
|
// Send Request
|
|
ca.parseAsync("PUB ngs.usage 2\r\nhi\r\nPING\r\n")
|
|
expectPong(t, cra)
|
|
|
|
// We should receive the request mapped into our account. PING needed to flush.
|
|
cb.parseAsync("PING\r\n")
|
|
expectMsg(t, crb, "ngs.usage.DEREK", "hi")
|
|
|
|
// Now update to make the export private.
|
|
fooACPrivate := jwt.NewAccountClaims(fooPub)
|
|
serviceExport = &jwt.Export{Subject: "ngs.usage.*", Type: jwt.Service, TokenReq: true}
|
|
fooACPrivate.Exports.Add(serviceExport)
|
|
fooJWTPrivate, err := fooACPrivate.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, fooPub, fooJWTPrivate)
|
|
acc, _ := s.LookupAccount(fooPub)
|
|
s.UpdateAccountClaims(acc, fooACPrivate)
|
|
|
|
// Send Another Request
|
|
ca.parseAsync("PUB ngs.usage 2\r\nhi\r\nPING\r\n")
|
|
expectPong(t, cra)
|
|
|
|
// We should not receive the request this time.
|
|
cb.parseAsync("PING\r\n")
|
|
expectPong(t, crb)
|
|
|
|
// Now put it back again to public and make sure it works again.
|
|
addAccountToMemResolver(s, fooPub, fooJWT)
|
|
s.UpdateAccountClaims(acc, fooAC)
|
|
|
|
// Send Request
|
|
ca.parseAsync("PUB ngs.usage 2\r\nhi\r\nPING\r\n")
|
|
expectPong(t, cra)
|
|
|
|
// We should receive the request mapped into our account. PING needed to flush.
|
|
cb.parseAsync("PING\r\n")
|
|
expectMsg(t, crb, "ngs.usage.DEREK", "hi")
|
|
}
|
|
|
|
func TestJWTAccountServiceImportExpires(t *testing.T) {
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
|
|
// Create accounts and imports/exports.
|
|
fooKP, _ := nkeys.CreateAccount()
|
|
fooPub, _ := fooKP.PublicKey()
|
|
fooAC := jwt.NewAccountClaims(fooPub)
|
|
serviceExport := &jwt.Export{Subject: "foo", Type: jwt.Service}
|
|
|
|
fooAC.Exports.Add(serviceExport)
|
|
fooJWT, err := fooAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, fooPub, fooJWT)
|
|
|
|
barKP, _ := nkeys.CreateAccount()
|
|
barPub, _ := barKP.PublicKey()
|
|
barAC := jwt.NewAccountClaims(barPub)
|
|
serviceImport := &jwt.Import{Account: fooPub, Subject: "foo", Type: jwt.Service}
|
|
|
|
barAC.Imports.Add(serviceImport)
|
|
barJWT, err := barAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, barPub, barJWT)
|
|
|
|
// Create a client that will send the request
|
|
ca, cra, csa := createClient(t, s, barKP)
|
|
defer ca.close()
|
|
ca.parseAsync(csa)
|
|
expectPong(t, cra)
|
|
|
|
// Create the client that will respond to the requests.
|
|
cb, crb, csb := createClient(t, s, fooKP)
|
|
defer cb.close()
|
|
cb.parseAsync(csb)
|
|
expectPong(t, crb)
|
|
|
|
// Create Subscriber.
|
|
cb.parseAsync("SUB foo 1\r\nPING\r\n")
|
|
expectPong(t, crb)
|
|
|
|
// Send Request
|
|
ca.parseAsync("PUB foo 2\r\nhi\r\nPING\r\n")
|
|
expectPong(t, cra)
|
|
|
|
// We should receive the request. PING needed to flush.
|
|
cb.parseAsync("PING\r\n")
|
|
expectMsg(t, crb, "foo", "hi")
|
|
|
|
// Now update the exported service to require auth.
|
|
fooAC = jwt.NewAccountClaims(fooPub)
|
|
serviceExport = &jwt.Export{Subject: "foo", Type: jwt.Service, TokenReq: true}
|
|
|
|
fooAC.Exports.Add(serviceExport)
|
|
fooJWT, err = fooAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, fooPub, fooJWT)
|
|
acc, _ := s.LookupAccount(fooPub)
|
|
s.UpdateAccountClaims(acc, fooAC)
|
|
|
|
// Send Another Request
|
|
ca.parseAsync("PUB foo 2\r\nhi\r\nPING\r\n")
|
|
expectPong(t, cra)
|
|
|
|
// We should not receive the request this time.
|
|
cb.parseAsync("PING\r\n")
|
|
expectPong(t, crb)
|
|
|
|
// Now get an activation token such that it will work, but will expire.
|
|
barAC = jwt.NewAccountClaims(barPub)
|
|
serviceImport = &jwt.Import{Account: fooPub, Subject: "foo", Type: jwt.Service}
|
|
|
|
now := time.Now()
|
|
activation := jwt.NewActivationClaims(barPub)
|
|
activation.ImportSubject = "foo"
|
|
activation.ImportType = jwt.Service
|
|
activation.IssuedAt = now.Add(-10 * time.Second).Unix()
|
|
activation.Expires = now.Add(time.Second).Round(time.Second).Unix()
|
|
actJWT, err := activation.Encode(fooKP)
|
|
if err != nil {
|
|
t.Fatalf("Error generating activation token: %v", err)
|
|
}
|
|
serviceImport.Token = actJWT
|
|
|
|
barAC.Imports.Add(serviceImport)
|
|
barJWT, err = barAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, barPub, barJWT)
|
|
acc, _ = s.LookupAccount(barPub)
|
|
s.UpdateAccountClaims(acc, barAC)
|
|
|
|
// Now it should work again.
|
|
// Send Another Request
|
|
ca.parseAsync("PUB foo 3\r\nhi2\r\nPING\r\n")
|
|
expectPong(t, cra)
|
|
|
|
// We should receive the request. PING needed to flush.
|
|
cb.parseAsync("PING\r\n")
|
|
expectMsg(t, crb, "foo", "hi2")
|
|
|
|
// Now wait for it to expire, then retry.
|
|
waitTime := time.Duration(activation.Expires-time.Now().Unix()) * time.Second
|
|
time.Sleep(waitTime + 250*time.Millisecond)
|
|
|
|
// Send Another Request
|
|
ca.parseAsync("PUB foo 3\r\nhi3\r\nPING\r\n")
|
|
expectPong(t, cra)
|
|
|
|
// We should NOT receive the request. PING needed to flush.
|
|
cb.parseAsync("PING\r\n")
|
|
expectPong(t, crb)
|
|
}
|
|
|
|
func TestAccountURLResolver(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
useTLS bool
|
|
}{
|
|
{"plain", false},
|
|
{"tls", true},
|
|
} {
|
|
t.Run(test.name, func(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)
|
|
}
|
|
|
|
hf := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(ajwt))
|
|
})
|
|
var ts *httptest.Server
|
|
if test.useTLS {
|
|
ts = httptest.NewTLSServer(hf)
|
|
} else {
|
|
ts = httptest.NewServer(hf)
|
|
}
|
|
defer ts.Close()
|
|
|
|
confTemplate := `
|
|
operator: %s
|
|
listen: -1
|
|
resolver: URL("%s/ngs/v1/accounts/jwt/")
|
|
resolver_tls {
|
|
insecure: true
|
|
}
|
|
`
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(confTemplate, ojwt, ts.URL)))
|
|
defer removeFile(t, 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")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAccountURLResolverTimeout(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)
|
|
}
|
|
|
|
basePath := "/ngs/v1/accounts/jwt/"
|
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == basePath {
|
|
w.Write([]byte("ok"))
|
|
return
|
|
}
|
|
// Purposely be slow on account lookup.
|
|
time.Sleep(200 * time.Millisecond)
|
|
w.Write([]byte(ajwt))
|
|
}))
|
|
defer ts.Close()
|
|
|
|
confTemplate := `
|
|
listen: -1
|
|
resolver: URL("%s%s")
|
|
`
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(confTemplate, ts.URL, basePath)))
|
|
defer removeFile(t, conf)
|
|
|
|
s, opts := RunServerWithConfig(conf)
|
|
pub, _ := kp.PublicKey()
|
|
opts.TrustedKeys = []string{pub}
|
|
defer s.Shutdown()
|
|
|
|
// Lower default timeout to speed-up test
|
|
s.AccountResolver().(*URLAccResolver).c.Timeout = 50 * time.Millisecond
|
|
|
|
acc, _ := s.LookupAccount(apub)
|
|
if acc != nil {
|
|
t.Fatalf("Expected to not receive an account due to timeout")
|
|
}
|
|
}
|
|
|
|
func TestAccountURLResolverNoFetchOnReload(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 := `
|
|
operator: %s
|
|
listen: -1
|
|
resolver: URL("%s/ngs/v1/accounts/jwt/")
|
|
`
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(confTemplate, ojwt, ts.URL)))
|
|
defer removeFile(t, conf)
|
|
|
|
s, _ := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
acc, _ := s.LookupAccount(apub)
|
|
if acc == nil {
|
|
t.Fatalf("Expected to receive an account")
|
|
}
|
|
|
|
// Reload would produce a DATA race during the DeepEqual check for the account resolver,
|
|
// so close the current one and we will create a new one that keeps track of fetch calls.
|
|
ts.Close()
|
|
|
|
fetch := int32(0)
|
|
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
atomic.AddInt32(&fetch, 1)
|
|
w.Write([]byte(ajwt))
|
|
}))
|
|
defer ts.Close()
|
|
|
|
changeCurrentConfigContentWithNewContent(t, conf, []byte(fmt.Sprintf(confTemplate, ojwt, ts.URL)))
|
|
|
|
if err := s.Reload(); err != nil {
|
|
t.Fatalf("Error on reload: %v", err)
|
|
}
|
|
if atomic.LoadInt32(&fetch) != 0 {
|
|
t.Fatalf("Fetch invoked during reload")
|
|
}
|
|
|
|
// Now stop the resolver and make sure that on startup, we report URL resolver failure
|
|
s.Shutdown()
|
|
s = nil
|
|
ts.Close()
|
|
|
|
opts := LoadConfig(conf)
|
|
if s, err := NewServer(opts); err == nil || !strings.Contains(err.Error(), "could not fetch") {
|
|
if s != nil {
|
|
s.Shutdown()
|
|
}
|
|
t.Fatalf("Expected error regarding account resolver, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAccountURLResolverFetchFailureInServer1(t *testing.T) {
|
|
const subj = "test"
|
|
const crossAccSubj = "test"
|
|
// Create Exporting Account
|
|
expkp, _ := nkeys.CreateAccount()
|
|
exppub, _ := expkp.PublicKey()
|
|
expac := jwt.NewAccountClaims(exppub)
|
|
expac.Exports.Add(&jwt.Export{
|
|
Subject: crossAccSubj,
|
|
Type: jwt.Stream,
|
|
})
|
|
expjwt, err := expac.Encode(oKp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
// Create importing Account
|
|
impkp, _ := nkeys.CreateAccount()
|
|
imppub, _ := impkp.PublicKey()
|
|
impac := jwt.NewAccountClaims(imppub)
|
|
impac.Imports.Add(&jwt.Import{
|
|
Account: exppub,
|
|
Subject: crossAccSubj,
|
|
Type: jwt.Stream,
|
|
})
|
|
impjwt, err := impac.Encode(oKp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
// Simulate an account server that drops the first request to exppub
|
|
chanImpA := make(chan struct{}, 10)
|
|
defer close(chanImpA)
|
|
chanExpS := make(chan struct{}, 10)
|
|
defer close(chanExpS)
|
|
chanExpF := make(chan struct{}, 1)
|
|
defer close(chanExpF)
|
|
failureCnt := int32(0)
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/A/" {
|
|
// Server startup
|
|
w.Write(nil)
|
|
chanImpA <- struct{}{}
|
|
} else if r.URL.Path == "/A/"+imppub {
|
|
w.Write([]byte(impjwt))
|
|
chanImpA <- struct{}{}
|
|
} else if r.URL.Path == "/A/"+exppub {
|
|
if atomic.AddInt32(&failureCnt, 1) <= 1 {
|
|
// skip the write to simulate the failure
|
|
chanExpF <- struct{}{}
|
|
} else {
|
|
w.Write([]byte(expjwt))
|
|
chanExpS <- struct{}{}
|
|
}
|
|
} else {
|
|
t.Fatal("not expected")
|
|
}
|
|
}))
|
|
defer ts.Close()
|
|
// Create server
|
|
confA := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
operator: %s
|
|
resolver: URL("%s/A/")
|
|
`, ojwt, ts.URL)))
|
|
defer removeFile(t, confA)
|
|
sA := RunServer(LoadConfig(confA))
|
|
defer sA.Shutdown()
|
|
// server observed one fetch on startup
|
|
chanRecv(t, chanImpA, 10*time.Second)
|
|
// Create first client
|
|
ncA := natsConnect(t, sA.ClientURL(), createUserCreds(t, nil, impkp))
|
|
defer ncA.Close()
|
|
// create a test subscription
|
|
subA, err := ncA.SubscribeSync(subj)
|
|
if err != nil {
|
|
t.Fatalf("Expected no error during subscribe: %v", err)
|
|
}
|
|
defer subA.Unsubscribe()
|
|
// Connect of client triggered a fetch of both accounts
|
|
// the fetch for the imported account will fail
|
|
chanRecv(t, chanImpA, 10*time.Second)
|
|
chanRecv(t, chanExpF, 10*time.Second)
|
|
// create second client for user exporting
|
|
ncB := natsConnect(t, sA.ClientURL(), createUserCreds(t, nil, expkp))
|
|
defer ncB.Close()
|
|
chanRecv(t, chanExpS, 10*time.Second)
|
|
// Connect of client triggered another fetch, this time passing
|
|
checkSubInterest(t, sA, imppub, subj, 10*time.Second)
|
|
checkSubInterest(t, sA, exppub, crossAccSubj, 10*time.Second) // Will fail as a result of this issue
|
|
}
|
|
|
|
func TestAccountURLResolverFetchFailurePushReorder(t *testing.T) {
|
|
const subj = "test"
|
|
const crossAccSubj = "test"
|
|
// Create System Account
|
|
syskp, _ := nkeys.CreateAccount()
|
|
syspub, _ := syskp.PublicKey()
|
|
sysAc := jwt.NewAccountClaims(syspub)
|
|
sysjwt, err := sysAc.Encode(oKp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
// Create Exporting Account
|
|
expkp, _ := nkeys.CreateAccount()
|
|
exppub, _ := expkp.PublicKey()
|
|
expac := jwt.NewAccountClaims(exppub)
|
|
expjwt1, err := expac.Encode(oKp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
expac.Exports.Add(&jwt.Export{
|
|
Subject: crossAccSubj,
|
|
Type: jwt.Stream,
|
|
})
|
|
expjwt2, err := expac.Encode(oKp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
// Create importing Account
|
|
impkp, _ := nkeys.CreateAccount()
|
|
imppub, _ := impkp.PublicKey()
|
|
impac := jwt.NewAccountClaims(imppub)
|
|
impac.Imports.Add(&jwt.Import{
|
|
Account: exppub,
|
|
Subject: crossAccSubj,
|
|
Type: jwt.Stream,
|
|
})
|
|
impjwt, err := impac.Encode(oKp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
// Simulate an account server that does not serve the updated jwt for exppub
|
|
chanImpA := make(chan struct{}, 10)
|
|
defer close(chanImpA)
|
|
chanExpS := make(chan struct{}, 10)
|
|
defer close(chanExpS)
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/A/" {
|
|
// Server startup
|
|
w.Write(nil)
|
|
chanImpA <- struct{}{}
|
|
} else if r.URL.Path == "/A/"+imppub {
|
|
w.Write([]byte(impjwt))
|
|
chanImpA <- struct{}{}
|
|
} else if r.URL.Path == "/A/"+exppub {
|
|
// respond with jwt that does not have the export
|
|
// this simulates an ordering issue
|
|
w.Write([]byte(expjwt1))
|
|
chanExpS <- struct{}{}
|
|
} else if r.URL.Path == "/A/"+syspub {
|
|
w.Write([]byte(sysjwt))
|
|
} else {
|
|
t.Fatal("not expected")
|
|
}
|
|
}))
|
|
defer ts.Close()
|
|
confA := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
operator: %s
|
|
resolver: URL("%s/A/")
|
|
system_account: %s
|
|
`, ojwt, ts.URL, syspub)))
|
|
defer removeFile(t, confA)
|
|
sA := RunServer(LoadConfig(confA))
|
|
defer sA.Shutdown()
|
|
// server observed one fetch on startup
|
|
chanRecv(t, chanImpA, 10*time.Second)
|
|
// Create first client
|
|
ncA := natsConnect(t, sA.ClientURL(), createUserCreds(t, nil, impkp))
|
|
defer ncA.Close()
|
|
// create a test subscription
|
|
subA, err := ncA.SubscribeSync(subj)
|
|
if err != nil {
|
|
t.Fatalf("Expected no error during subscribe: %v", err)
|
|
}
|
|
defer subA.Unsubscribe()
|
|
// Connect of client triggered a fetch of both accounts
|
|
// the fetch for the imported account will fail
|
|
chanRecv(t, chanImpA, 10*time.Second)
|
|
chanRecv(t, chanExpS, 10*time.Second)
|
|
// create second client for user exporting
|
|
ncB := natsConnect(t, sA.ClientURL(), createUserCreds(t, nil, expkp))
|
|
defer ncB.Close()
|
|
// update expjwt2, this will correct the import issue
|
|
sysc := natsConnect(t, sA.ClientURL(), createUserCreds(t, nil, syskp))
|
|
defer sysc.Close()
|
|
natsPub(t, sysc, fmt.Sprintf(accUpdateEventSubjNew, exppub), []byte(expjwt2))
|
|
sysc.Flush()
|
|
// updating expjwt should cause this to pass
|
|
checkSubInterest(t, sA, imppub, subj, 10*time.Second)
|
|
checkSubInterest(t, sA, exppub, crossAccSubj, 10*time.Second) // Will fail as a result of this issue
|
|
}
|
|
|
|
type captureDebugLogger struct {
|
|
DummyLogger
|
|
dbgCh chan string
|
|
}
|
|
|
|
func (l *captureDebugLogger) Debugf(format string, v ...interface{}) {
|
|
select {
|
|
case l.dbgCh <- fmt.Sprintf(format, v...):
|
|
default:
|
|
}
|
|
}
|
|
|
|
func TestAccountURLResolverPermanentFetchFailure(t *testing.T) {
|
|
const crossAccSubj = "test"
|
|
expkp, _ := nkeys.CreateAccount()
|
|
exppub, _ := expkp.PublicKey()
|
|
impkp, _ := nkeys.CreateAccount()
|
|
imppub, _ := impkp.PublicKey()
|
|
// Create System Account
|
|
syskp, _ := nkeys.CreateAccount()
|
|
syspub, _ := syskp.PublicKey()
|
|
sysAc := jwt.NewAccountClaims(syspub)
|
|
sysjwt, err := sysAc.Encode(oKp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
// Create 2 Accounts. Each importing from the other, but NO matching export
|
|
expac := jwt.NewAccountClaims(exppub)
|
|
expac.Imports.Add(&jwt.Import{
|
|
Account: imppub,
|
|
Subject: crossAccSubj,
|
|
Type: jwt.Stream,
|
|
})
|
|
expjwt, err := expac.Encode(oKp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
// Create importing Account
|
|
impac := jwt.NewAccountClaims(imppub)
|
|
impac.Imports.Add(&jwt.Import{
|
|
Account: exppub,
|
|
Subject: crossAccSubj,
|
|
Type: jwt.Stream,
|
|
})
|
|
impjwt, err := impac.Encode(oKp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
// Simulate an account server that does not serve the updated jwt for exppub
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/A/" {
|
|
// Server startup
|
|
w.Write(nil)
|
|
} else if r.URL.Path == "/A/"+imppub {
|
|
w.Write([]byte(impjwt))
|
|
} else if r.URL.Path == "/A/"+exppub {
|
|
w.Write([]byte(expjwt))
|
|
} else if r.URL.Path == "/A/"+syspub {
|
|
w.Write([]byte(sysjwt))
|
|
} else {
|
|
t.Fatal("not expected")
|
|
}
|
|
}))
|
|
defer ts.Close()
|
|
confA := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
operator: %s
|
|
resolver: URL("%s/A/")
|
|
system_account: %s
|
|
`, ojwt, ts.URL, syspub)))
|
|
defer removeFile(t, confA)
|
|
o := LoadConfig(confA)
|
|
sA := RunServer(o)
|
|
defer sA.Shutdown()
|
|
l := &captureDebugLogger{dbgCh: make(chan string, 100)} // has enough space to not block
|
|
sA.SetLogger(l, true, false)
|
|
// Create clients
|
|
ncA := natsConnect(t, sA.ClientURL(), createUserCreds(t, nil, impkp))
|
|
defer ncA.Close()
|
|
ncB := natsConnect(t, sA.ClientURL(), createUserCreds(t, nil, expkp))
|
|
defer ncB.Close()
|
|
sysc := natsConnect(t, sA.ClientURL(), createUserCreds(t, nil, syskp))
|
|
defer sysc.Close()
|
|
// push accounts
|
|
natsPub(t, sysc, fmt.Sprintf(accUpdateEventSubjNew, imppub), []byte(impjwt))
|
|
natsPub(t, sysc, fmt.Sprintf(accUpdateEventSubjOld, exppub), []byte(expjwt))
|
|
sysc.Flush()
|
|
importErrCnt := 0
|
|
tmr := time.NewTimer(500 * time.Millisecond)
|
|
defer tmr.Stop()
|
|
for {
|
|
select {
|
|
case line := <-l.dbgCh:
|
|
if strings.HasPrefix(line, "Error adding stream import to account") {
|
|
importErrCnt++
|
|
}
|
|
case <-tmr.C:
|
|
// connecting and updating, each cause 3 traces (2 + 1 on iteration)
|
|
if importErrCnt != 6 {
|
|
t.Fatalf("Expected 6 debug traces, got %d", importErrCnt)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAccountURLResolverFetchFailureInCluster(t *testing.T) {
|
|
assertChanLen := func(x int, chans ...chan struct{}) {
|
|
t.Helper()
|
|
for _, c := range chans {
|
|
if len(c) != x {
|
|
t.Fatalf("length of channel is not %d", x)
|
|
}
|
|
}
|
|
}
|
|
const subj = ">"
|
|
const crossAccSubj = "test"
|
|
// Create Exporting Account
|
|
expkp, _ := nkeys.CreateAccount()
|
|
exppub, _ := expkp.PublicKey()
|
|
expac := jwt.NewAccountClaims(exppub)
|
|
expac.Exports.Add(&jwt.Export{
|
|
Subject: crossAccSubj,
|
|
Type: jwt.Stream,
|
|
})
|
|
expjwt, err := expac.Encode(oKp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
// Create importing Account
|
|
impkp, _ := nkeys.CreateAccount()
|
|
imppub, _ := impkp.PublicKey()
|
|
impac := jwt.NewAccountClaims(imppub)
|
|
impac.Imports.Add(&jwt.Import{
|
|
Account: exppub,
|
|
Subject: crossAccSubj,
|
|
Type: jwt.Stream,
|
|
})
|
|
impac.Exports.Add(&jwt.Export{
|
|
Subject: "srvc",
|
|
Type: jwt.Service,
|
|
})
|
|
impjwt, err := impac.Encode(oKp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
// Create User
|
|
nkp, _ := nkeys.CreateUser()
|
|
uSeed, _ := nkp.Seed()
|
|
upub, _ := nkp.PublicKey()
|
|
nuc := newJWTTestUserClaims()
|
|
nuc.Subject = upub
|
|
uJwt, err := nuc.Encode(impkp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating user JWT: %v", err)
|
|
}
|
|
creds := genCredsFile(t, uJwt, uSeed)
|
|
defer removeFile(t, creds)
|
|
// Simulate an account server that drops the first request to /B/acc
|
|
chanImpA := make(chan struct{}, 4)
|
|
defer close(chanImpA)
|
|
chanImpB := make(chan struct{}, 4)
|
|
defer close(chanImpB)
|
|
chanExpA := make(chan struct{}, 4)
|
|
defer close(chanExpA)
|
|
chanExpB := make(chan struct{}, 4)
|
|
defer close(chanExpB)
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/A/" {
|
|
// Server A startup
|
|
w.Write(nil)
|
|
chanImpA <- struct{}{}
|
|
} else if r.URL.Path == "/B/" {
|
|
// Server B startup
|
|
w.Write(nil)
|
|
chanImpB <- struct{}{}
|
|
} else if r.URL.Path == "/A/"+imppub {
|
|
// First Client connecting to Server A
|
|
w.Write([]byte(impjwt))
|
|
chanImpA <- struct{}{}
|
|
} else if r.URL.Path == "/B/"+imppub {
|
|
// Second Client connecting to Server B
|
|
w.Write([]byte(impjwt))
|
|
chanImpB <- struct{}{}
|
|
} else if r.URL.Path == "/A/"+exppub {
|
|
// First Client connecting to Server A
|
|
w.Write([]byte(expjwt))
|
|
chanExpA <- struct{}{}
|
|
} else if r.URL.Path == "/B/"+exppub {
|
|
// Second Client connecting to Server B
|
|
w.Write([]byte(expjwt))
|
|
chanExpB <- struct{}{}
|
|
} else {
|
|
t.Fatal("not expected")
|
|
}
|
|
}))
|
|
defer ts.Close()
|
|
// Create seed server A
|
|
confA := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
operator: %s
|
|
resolver: URL("%s/A/")
|
|
cluster {
|
|
name: clust
|
|
no_advertise: true
|
|
listen: -1
|
|
}
|
|
`, ojwt, ts.URL)))
|
|
defer removeFile(t, confA)
|
|
sA := RunServer(LoadConfig(confA))
|
|
defer sA.Shutdown()
|
|
// Create Server B (using no_advertise to prevent failover)
|
|
confB := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
operator: %s
|
|
resolver: URL("%s/B/")
|
|
cluster {
|
|
name: clust
|
|
no_advertise: true
|
|
listen: -1
|
|
routes [
|
|
nats-route://localhost:%d
|
|
]
|
|
}
|
|
`, ojwt, ts.URL, sA.opts.Cluster.Port)))
|
|
defer removeFile(t, confB)
|
|
sB := RunServer(LoadConfig(confB))
|
|
defer sB.Shutdown()
|
|
// startup cluster
|
|
checkClusterFormed(t, sA, sB)
|
|
// Both server observed one fetch on startup
|
|
chanRecv(t, chanImpA, 10*time.Second)
|
|
chanRecv(t, chanImpB, 10*time.Second)
|
|
assertChanLen(0, chanImpA, chanImpB, chanExpA, chanExpB)
|
|
// Create first client, directly connects to A
|
|
urlA := fmt.Sprintf("nats://%s:%d", sA.opts.Host, sA.opts.Port)
|
|
ncA, err := nats.Connect(urlA, nats.UserCredentials(creds),
|
|
nats.DisconnectErrHandler(func(_ *nats.Conn, err error) {
|
|
if err != nil {
|
|
t.Fatal("error not expected in this test", err)
|
|
}
|
|
}),
|
|
nats.ErrorHandler(func(_ *nats.Conn, _ *nats.Subscription, err error) {
|
|
t.Fatal("error not expected in this test", err)
|
|
}),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Expected to connect, got %v %s", err, urlA)
|
|
}
|
|
defer ncA.Close()
|
|
// create a test subscription
|
|
subA, err := ncA.SubscribeSync(subj)
|
|
if err != nil {
|
|
t.Fatalf("Expected no error during subscribe: %v", err)
|
|
}
|
|
defer subA.Unsubscribe()
|
|
// Connect of client triggered a fetch by Server A
|
|
chanRecv(t, chanImpA, 10*time.Second)
|
|
chanRecv(t, chanExpA, 10*time.Second)
|
|
assertChanLen(0, chanImpA, chanImpB, chanExpA, chanExpB)
|
|
//time.Sleep(10 * time.Second)
|
|
// create second client, directly connect to B
|
|
urlB := fmt.Sprintf("nats://%s:%d", sB.opts.Host, sB.opts.Port)
|
|
ncB, err := nats.Connect(urlB, nats.UserCredentials(creds), nats.NoReconnect())
|
|
if err != nil {
|
|
t.Fatalf("Expected to connect, got %v %s", err, urlB)
|
|
}
|
|
defer ncB.Close()
|
|
// Connect of client triggered a fetch by Server B
|
|
chanRecv(t, chanImpB, 10*time.Second)
|
|
chanRecv(t, chanExpB, 10*time.Second)
|
|
assertChanLen(0, chanImpA, chanImpB, chanExpA, chanExpB)
|
|
checkClusterFormed(t, sA, sB)
|
|
// the route subscription was lost due to the failed fetch
|
|
// Now we test if some recover mechanism is in play
|
|
checkSubInterest(t, sB, imppub, subj, 10*time.Second) // Will fail as a result of this issue
|
|
checkSubInterest(t, sB, exppub, crossAccSubj, 10*time.Second) // Will fail as a result of this issue
|
|
if err := ncB.Publish(subj, []byte("msg")); err != nil {
|
|
t.Fatalf("Expected to publish %v", err)
|
|
}
|
|
// expect the message from B to flow to A
|
|
if m, err := subA.NextMsg(10 * time.Second); err != nil {
|
|
t.Fatalf("Expected to receive a message %v", err)
|
|
} else if string(m.Data) != "msg" {
|
|
t.Fatalf("Expected to receive 'msg', got: %s", string(m.Data))
|
|
}
|
|
assertChanLen(0, chanImpA, chanImpB, chanExpA, chanExpB)
|
|
}
|
|
|
|
func TestAccountURLResolverReturnDifferentOperator(t *testing.T) {
|
|
// Create a valid chain of op/acc/usr using a different operator
|
|
// This is so we can test if the server rejects this chain.
|
|
// Create Operator
|
|
op, _ := nkeys.CreateOperator()
|
|
// Create Account, this account is the one returned by the resolver
|
|
akp, _ := nkeys.CreateAccount()
|
|
apub, _ := akp.PublicKey()
|
|
nac := jwt.NewAccountClaims(apub)
|
|
ajwt, err := nac.Encode(op)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
// Create User
|
|
nkp, _ := nkeys.CreateUser()
|
|
uSeed, _ := nkp.Seed()
|
|
upub, _ := nkp.PublicKey()
|
|
nuc := newJWTTestUserClaims()
|
|
nuc.Subject = upub
|
|
uJwt, err := nuc.Encode(akp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating user JWT: %v", err)
|
|
}
|
|
creds := genCredsFile(t, uJwt, uSeed)
|
|
defer removeFile(t, creds)
|
|
// Simulate an account server that was hijacked/mis configured
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(ajwt))
|
|
}))
|
|
defer ts.Close()
|
|
// Create Server
|
|
confA := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
operator: %s
|
|
resolver: URL("%s/A/")
|
|
`, ojwt, ts.URL)))
|
|
defer removeFile(t, confA)
|
|
sA, _ := RunServerWithConfig(confA)
|
|
defer sA.Shutdown()
|
|
// Create first client, directly connects to A
|
|
urlA := fmt.Sprintf("nats://%s:%d", sA.opts.Host, sA.opts.Port)
|
|
if _, err := nats.Connect(urlA, nats.UserCredentials(creds),
|
|
nats.DisconnectErrHandler(func(_ *nats.Conn, err error) {
|
|
if err != nil {
|
|
t.Fatal("error not expected in this test", err)
|
|
}
|
|
}),
|
|
nats.ErrorHandler(func(_ *nats.Conn, _ *nats.Subscription, err error) {
|
|
t.Fatal("error not expected in this test", err)
|
|
}),
|
|
); err == nil {
|
|
t.Fatal("Expected connect to fail")
|
|
}
|
|
// Test if the server has the account in memory. (shouldn't)
|
|
if v, ok := sA.accounts.Load(apub); ok {
|
|
t.Fatalf("Expected account to NOT be in memory: %v", v.(*Account))
|
|
}
|
|
}
|
|
|
|
func TestJWTUserSigningKey(t *testing.T) {
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
|
|
// Check to make sure we would have an authTimer
|
|
if !s.info.AuthRequired {
|
|
t.Fatalf("Expect the server to require auth")
|
|
}
|
|
|
|
c, cr, _ := newClientForServer(s)
|
|
defer c.close()
|
|
// Don't send jwt field, should fail.
|
|
c.parseAsync("CONNECT {\"verbose\":true,\"pedantic\":true}\r\nPING\r\n")
|
|
l, _ := cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "-ERR ") {
|
|
t.Fatalf("Expected an error")
|
|
}
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
|
|
// Create an account
|
|
akp, _ := nkeys.CreateAccount()
|
|
apub, _ := akp.PublicKey()
|
|
|
|
// Create a signing key for the account
|
|
askp, _ := nkeys.CreateAccount()
|
|
aspub, _ := askp.PublicKey()
|
|
|
|
nac := jwt.NewAccountClaims(apub)
|
|
ajwt, err := nac.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
// Create a client with the account signing key
|
|
c, cr, cs := createClientWithIssuer(t, s, askp, apub)
|
|
defer c.close()
|
|
|
|
// PING needed to flush the +OK/-ERR to us.
|
|
// This should fail too since no account resolver is defined.
|
|
c.parseAsync(cs)
|
|
l, _ = cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "-ERR ") {
|
|
t.Fatalf("Expected an error")
|
|
}
|
|
|
|
// Ok now let's walk through and make sure all is good.
|
|
// We will set the account resolver by hand to a memory resolver.
|
|
buildMemAccResolver(s)
|
|
addAccountToMemResolver(s, apub, ajwt)
|
|
|
|
// Create a client with a signing key
|
|
c, cr, cs = createClientWithIssuer(t, s, askp, apub)
|
|
defer c.close()
|
|
// should fail because the signing key is not known
|
|
c.parseAsync(cs)
|
|
l, _ = cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "-ERR ") {
|
|
t.Fatalf("Expected an error: %v", l)
|
|
}
|
|
|
|
// add a signing key
|
|
nac.SigningKeys.Add(aspub)
|
|
// update the memory resolver
|
|
acc, _ := s.LookupAccount(apub)
|
|
s.UpdateAccountClaims(acc, nac)
|
|
|
|
// Create a client with a signing key
|
|
c, cr, cs = createClientWithIssuer(t, s, askp, apub)
|
|
defer c.close()
|
|
|
|
// expect this to work
|
|
c.parseAsync(cs)
|
|
l, _ = cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "PONG") {
|
|
t.Fatalf("Expected a PONG, got %q", l)
|
|
}
|
|
|
|
isClosed := func() bool {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.isClosed()
|
|
}
|
|
|
|
if isClosed() {
|
|
t.Fatal("expected client to be alive")
|
|
}
|
|
// remove the signing key should bounce client
|
|
nac.SigningKeys = nil
|
|
acc, _ = s.LookupAccount(apub)
|
|
s.UpdateAccountClaims(acc, nac)
|
|
|
|
if !isClosed() {
|
|
t.Fatal("expected client to be gone")
|
|
}
|
|
}
|
|
|
|
func TestJWTAccountImportSignerRemoved(t *testing.T) {
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
|
|
// Exporter keys
|
|
srvKP, _ := nkeys.CreateAccount()
|
|
srvPK, _ := srvKP.PublicKey()
|
|
srvSignerKP, _ := nkeys.CreateAccount()
|
|
srvSignerPK, _ := srvSignerKP.PublicKey()
|
|
|
|
// Importer keys
|
|
clientKP, _ := nkeys.CreateAccount()
|
|
clientPK, _ := clientKP.PublicKey()
|
|
|
|
createSrvJwt := func(signingKeys ...string) (string, *jwt.AccountClaims) {
|
|
ac := jwt.NewAccountClaims(srvPK)
|
|
ac.SigningKeys.Add(signingKeys...)
|
|
ac.Exports.Add(&jwt.Export{Subject: "foo", Type: jwt.Service, TokenReq: true})
|
|
ac.Exports.Add(&jwt.Export{Subject: "bar", Type: jwt.Stream, TokenReq: true})
|
|
token, err := ac.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating exporter JWT: %v", err)
|
|
}
|
|
return token, ac
|
|
}
|
|
|
|
createImportToken := func(sub string, kind jwt.ExportType) string {
|
|
actC := jwt.NewActivationClaims(clientPK)
|
|
actC.IssuerAccount = srvPK
|
|
actC.ImportType = kind
|
|
actC.ImportSubject = jwt.Subject(sub)
|
|
token, err := actC.Encode(srvSignerKP)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return token
|
|
}
|
|
|
|
createClientJwt := func() string {
|
|
ac := jwt.NewAccountClaims(clientPK)
|
|
ac.Imports.Add(&jwt.Import{Account: srvPK, Subject: "foo", Type: jwt.Service, Token: createImportToken("foo", jwt.Service)})
|
|
ac.Imports.Add(&jwt.Import{Account: srvPK, Subject: "bar", Type: jwt.Stream, Token: createImportToken("bar", jwt.Stream)})
|
|
token, err := ac.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating importer JWT: %v", err)
|
|
}
|
|
return token
|
|
}
|
|
|
|
srvJWT, _ := createSrvJwt(srvSignerPK)
|
|
addAccountToMemResolver(s, srvPK, srvJWT)
|
|
|
|
clientJWT := createClientJwt()
|
|
addAccountToMemResolver(s, clientPK, clientJWT)
|
|
|
|
// Create a client that will send the request
|
|
client, clientReader, clientCS := createClient(t, s, clientKP)
|
|
defer client.close()
|
|
client.parseAsync(clientCS)
|
|
expectPong(t, clientReader)
|
|
|
|
checkShadow := func(expected int) {
|
|
t.Helper()
|
|
client.mu.Lock()
|
|
defer client.mu.Unlock()
|
|
sub := client.subs["1"]
|
|
count := 0
|
|
if sub != nil {
|
|
count = len(sub.shadow)
|
|
}
|
|
if count != expected {
|
|
t.Fatalf("Expected shadows to be %d, got %d", expected, count)
|
|
}
|
|
}
|
|
|
|
checkShadow(0)
|
|
// Create the client that will respond to the requests.
|
|
srv, srvReader, srvCS := createClient(t, s, srvKP)
|
|
defer srv.close()
|
|
srv.parseAsync(srvCS)
|
|
expectPong(t, srvReader)
|
|
|
|
// Create Subscriber.
|
|
srv.parseAsync("SUB foo 1\r\nPING\r\n")
|
|
expectPong(t, srvReader)
|
|
|
|
// Send Request
|
|
client.parseAsync("PUB foo 2\r\nhi\r\nPING\r\n")
|
|
expectPong(t, clientReader)
|
|
|
|
// We should receive the request. PING needed to flush.
|
|
srv.parseAsync("PING\r\n")
|
|
expectMsg(t, srvReader, "foo", "hi")
|
|
|
|
client.parseAsync("SUB bar 1\r\nPING\r\n")
|
|
expectPong(t, clientReader)
|
|
checkShadow(1)
|
|
|
|
srv.parseAsync("PUB bar 2\r\nhi\r\nPING\r\n")
|
|
expectPong(t, srvReader)
|
|
|
|
// We should receive from stream. PING needed to flush.
|
|
client.parseAsync("PING\r\n")
|
|
expectMsg(t, clientReader, "bar", "hi")
|
|
|
|
// Now update the exported service no signer
|
|
srvJWT, srvAC := createSrvJwt()
|
|
addAccountToMemResolver(s, srvPK, srvJWT)
|
|
acc, _ := s.LookupAccount(srvPK)
|
|
s.UpdateAccountClaims(acc, srvAC)
|
|
|
|
// Send Another Request
|
|
client.parseAsync("PUB foo 2\r\nhi\r\nPING\r\n")
|
|
expectPong(t, clientReader)
|
|
|
|
// We should not receive the request this time.
|
|
srv.parseAsync("PING\r\n")
|
|
expectPong(t, srvReader)
|
|
|
|
// Publish on the stream
|
|
srv.parseAsync("PUB bar 2\r\nhi\r\nPING\r\n")
|
|
expectPong(t, srvReader)
|
|
|
|
// We should not receive from the stream this time
|
|
client.parseAsync("PING\r\n")
|
|
expectPong(t, clientReader)
|
|
checkShadow(0)
|
|
}
|
|
|
|
func TestJWTAccountImportSignerDeadlock(t *testing.T) {
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
|
|
// Exporter keys
|
|
srvKP, _ := nkeys.CreateAccount()
|
|
srvPK, _ := srvKP.PublicKey()
|
|
srvSignerKP, _ := nkeys.CreateAccount()
|
|
srvSignerPK, _ := srvSignerKP.PublicKey()
|
|
|
|
// Importer keys
|
|
clientKP, _ := nkeys.CreateAccount()
|
|
clientPK, _ := clientKP.PublicKey()
|
|
|
|
createSrvJwt := func(signingKeys ...string) (string, *jwt.AccountClaims) {
|
|
ac := jwt.NewAccountClaims(srvPK)
|
|
ac.SigningKeys.Add(signingKeys...)
|
|
ac.Exports.Add(&jwt.Export{Subject: "foo", Type: jwt.Service, TokenReq: true})
|
|
ac.Exports.Add(&jwt.Export{Subject: "bar", Type: jwt.Stream, TokenReq: true})
|
|
token, err := ac.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating exporter JWT: %v", err)
|
|
}
|
|
return token, ac
|
|
}
|
|
|
|
createImportToken := func(sub string, kind jwt.ExportType) string {
|
|
actC := jwt.NewActivationClaims(clientPK)
|
|
actC.IssuerAccount = srvPK
|
|
actC.ImportType = kind
|
|
actC.ImportSubject = jwt.Subject(sub)
|
|
token, err := actC.Encode(srvSignerKP)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return token
|
|
}
|
|
|
|
createClientJwt := func() string {
|
|
ac := jwt.NewAccountClaims(clientPK)
|
|
ac.Imports.Add(&jwt.Import{Account: srvPK, Subject: "foo", Type: jwt.Service, Token: createImportToken("foo", jwt.Service)})
|
|
ac.Imports.Add(&jwt.Import{Account: srvPK, Subject: "bar", Type: jwt.Stream, Token: createImportToken("bar", jwt.Stream)})
|
|
token, err := ac.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating importer JWT: %v", err)
|
|
}
|
|
return token
|
|
}
|
|
|
|
srvJWT, _ := createSrvJwt(srvSignerPK)
|
|
addAccountToMemResolver(s, srvPK, srvJWT)
|
|
|
|
clientJWT := createClientJwt()
|
|
addAccountToMemResolver(s, clientPK, clientJWT)
|
|
|
|
acc, _ := s.LookupAccount(srvPK)
|
|
// Have a go routine that constantly gets/releases the acc's write lock.
|
|
// There was a bug that could cause AddServiceImportWithClaim to deadlock.
|
|
ch := make(chan bool, 1)
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for {
|
|
select {
|
|
case <-ch:
|
|
return
|
|
default:
|
|
acc.mu.Lock()
|
|
acc.mu.Unlock()
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Create a client that will send the request
|
|
client, clientReader, clientCS := createClient(t, s, clientKP)
|
|
defer client.close()
|
|
client.parseAsync(clientCS)
|
|
expectPong(t, clientReader)
|
|
|
|
close(ch)
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestJWTAccountImportWrongIssuerAccount(t *testing.T) {
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
|
|
l := &captureErrorLogger{errCh: make(chan string, 2)}
|
|
s.SetLogger(l, false, false)
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
|
|
// Exporter keys
|
|
srvKP, _ := nkeys.CreateAccount()
|
|
srvPK, _ := srvKP.PublicKey()
|
|
srvSignerKP, _ := nkeys.CreateAccount()
|
|
srvSignerPK, _ := srvSignerKP.PublicKey()
|
|
|
|
// Importer keys
|
|
clientKP, _ := nkeys.CreateAccount()
|
|
clientPK, _ := clientKP.PublicKey()
|
|
|
|
createSrvJwt := func(signingKeys ...string) (string, *jwt.AccountClaims) {
|
|
ac := jwt.NewAccountClaims(srvPK)
|
|
ac.SigningKeys.Add(signingKeys...)
|
|
ac.Exports.Add(&jwt.Export{Subject: "foo", Type: jwt.Service, TokenReq: true})
|
|
ac.Exports.Add(&jwt.Export{Subject: "bar", Type: jwt.Stream, TokenReq: true})
|
|
token, err := ac.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating exporter JWT: %v", err)
|
|
}
|
|
return token, ac
|
|
}
|
|
|
|
createImportToken := func(sub string, kind jwt.ExportType) string {
|
|
actC := jwt.NewActivationClaims(clientPK)
|
|
// Reference ourselves, which is wrong.
|
|
actC.IssuerAccount = clientPK
|
|
actC.ImportType = kind
|
|
actC.ImportSubject = jwt.Subject(sub)
|
|
token, err := actC.Encode(srvSignerKP)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return token
|
|
}
|
|
|
|
createClientJwt := func() string {
|
|
ac := jwt.NewAccountClaims(clientPK)
|
|
ac.Imports.Add(&jwt.Import{Account: srvPK, Subject: "foo", Type: jwt.Service, Token: createImportToken("foo", jwt.Service)})
|
|
ac.Imports.Add(&jwt.Import{Account: srvPK, Subject: "bar", Type: jwt.Stream, Token: createImportToken("bar", jwt.Stream)})
|
|
token, err := ac.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating importer JWT: %v", err)
|
|
}
|
|
return token
|
|
}
|
|
|
|
srvJWT, _ := createSrvJwt(srvSignerPK)
|
|
addAccountToMemResolver(s, srvPK, srvJWT)
|
|
|
|
clientJWT := createClientJwt()
|
|
addAccountToMemResolver(s, clientPK, clientJWT)
|
|
|
|
// Create a client that will send the request
|
|
client, clientReader, clientCS := createClient(t, s, clientKP)
|
|
defer client.close()
|
|
client.parseAsync(clientCS)
|
|
if l, _, err := clientReader.ReadLine(); err != nil {
|
|
t.Fatalf("Expected no Error, got: %v", err)
|
|
} else if !strings.Contains(string(l), "-ERR 'Authorization Violation'") {
|
|
t.Fatalf("Expected Error, got: %v", l)
|
|
}
|
|
}
|
|
|
|
func TestJWTUserRevokedOnAccountUpdate(t *testing.T) {
|
|
nac := newJWTTestAccountClaims()
|
|
s, akp, c, cr := setupJWTTestWitAccountClaims(t, nac, "+OK")
|
|
defer s.Shutdown()
|
|
defer c.close()
|
|
|
|
expectPong(t, cr)
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
apub, _ := akp.PublicKey()
|
|
|
|
c.mu.Lock()
|
|
pub := c.user.Nkey
|
|
c.mu.Unlock()
|
|
|
|
// Now revoke the user.
|
|
nac.Revoke(pub)
|
|
|
|
ajwt, err := nac.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
// Update the account on the server.
|
|
addAccountToMemResolver(s, apub, ajwt)
|
|
acc, err := s.LookupAccount(apub)
|
|
if err != nil {
|
|
t.Fatalf("Error looking up the account: %v", err)
|
|
}
|
|
|
|
// This is simulating a system update for the account claims.
|
|
go s.updateAccountWithClaimJWT(acc, ajwt)
|
|
|
|
l, _ := cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "-ERR ") {
|
|
t.Fatalf("Expected an error")
|
|
}
|
|
if !strings.Contains(l, "Revoked") {
|
|
t.Fatalf("Expected 'Revoked' to be in the error")
|
|
}
|
|
}
|
|
|
|
func TestJWTUserRevoked(t *testing.T) {
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
|
|
// Create a new user that we will make sure has been revoked.
|
|
nkp, _ := nkeys.CreateUser()
|
|
pub, _ := nkp.PublicKey()
|
|
nuc := jwt.NewUserClaims(pub)
|
|
|
|
akp, _ := nkeys.CreateAccount()
|
|
apub, _ := akp.PublicKey()
|
|
nac := jwt.NewAccountClaims(apub)
|
|
// Revoke the user right away.
|
|
nac.Revoke(pub)
|
|
ajwt, err := nac.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
// Sign for the user.
|
|
jwt, err := nuc.Encode(akp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating user JWT: %v", err)
|
|
}
|
|
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
addAccountToMemResolver(s, apub, ajwt)
|
|
|
|
c, cr, l := newClientForServer(s)
|
|
defer c.close()
|
|
|
|
// Sign Nonce
|
|
var info nonceInfo
|
|
json.Unmarshal([]byte(l[5:]), &info)
|
|
sigraw, _ := nkp.Sign([]byte(info.Nonce))
|
|
sig := base64.RawURLEncoding.EncodeToString(sigraw)
|
|
|
|
// PING needed to flush the +OK/-ERR to us.
|
|
cs := fmt.Sprintf("CONNECT {\"jwt\":%q,\"sig\":\"%s\"}\r\nPING\r\n", jwt, sig)
|
|
|
|
c.parseAsync(cs)
|
|
|
|
l, _ = cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "-ERR ") {
|
|
t.Fatalf("Expected an error")
|
|
}
|
|
if !strings.Contains(l, "Authorization") {
|
|
t.Fatalf("Expected 'Revoked' to be in the error")
|
|
}
|
|
}
|
|
|
|
// Test that an account update that revokes an import authorization cancels the import.
|
|
func TestJWTImportTokenRevokedAfter(t *testing.T) {
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
|
|
// Create accounts and imports/exports.
|
|
fooKP, _ := nkeys.CreateAccount()
|
|
fooPub, _ := fooKP.PublicKey()
|
|
fooAC := jwt.NewAccountClaims(fooPub)
|
|
|
|
// Now create Exports.
|
|
export := &jwt.Export{Subject: "foo.private", Type: jwt.Stream, TokenReq: true}
|
|
|
|
fooAC.Exports.Add(export)
|
|
fooJWT, err := fooAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
addAccountToMemResolver(s, fooPub, fooJWT)
|
|
|
|
barKP, _ := nkeys.CreateAccount()
|
|
barPub, _ := barKP.PublicKey()
|
|
barAC := jwt.NewAccountClaims(barPub)
|
|
simport := &jwt.Import{Account: fooPub, Subject: "foo.private", Type: jwt.Stream}
|
|
|
|
activation := jwt.NewActivationClaims(barPub)
|
|
activation.ImportSubject = "foo.private"
|
|
activation.ImportType = jwt.Stream
|
|
actJWT, err := activation.Encode(fooKP)
|
|
if err != nil {
|
|
t.Fatalf("Error generating activation token: %v", err)
|
|
}
|
|
|
|
simport.Token = actJWT
|
|
barAC.Imports.Add(simport)
|
|
barJWT, err := barAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, barPub, barJWT)
|
|
|
|
// Now revoke the export.
|
|
decoded, _ := jwt.DecodeActivationClaims(actJWT)
|
|
export.Revoke(decoded.Subject)
|
|
|
|
fooJWT, err = fooAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
addAccountToMemResolver(s, fooPub, fooJWT)
|
|
|
|
fooAcc, _ := s.LookupAccount(fooPub)
|
|
if fooAcc == nil {
|
|
t.Fatalf("Expected to retrieve the account")
|
|
}
|
|
|
|
// Now lookup bar account and make sure it was revoked.
|
|
acc, _ := s.LookupAccount(barPub)
|
|
if acc == nil {
|
|
t.Fatalf("Expected to retrieve the account")
|
|
}
|
|
if les := len(acc.imports.streams); les != 0 {
|
|
t.Fatalf("Expected imports streams len of 0, got %d", les)
|
|
}
|
|
}
|
|
|
|
// Test that an account update that revokes an import authorization cancels the import.
|
|
func TestJWTImportTokenRevokedBefore(t *testing.T) {
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
|
|
// Create accounts and imports/exports.
|
|
fooKP, _ := nkeys.CreateAccount()
|
|
fooPub, _ := fooKP.PublicKey()
|
|
fooAC := jwt.NewAccountClaims(fooPub)
|
|
|
|
// Now create Exports.
|
|
export := &jwt.Export{Subject: "foo.private", Type: jwt.Stream, TokenReq: true}
|
|
|
|
fooAC.Exports.Add(export)
|
|
|
|
// Import account
|
|
barKP, _ := nkeys.CreateAccount()
|
|
barPub, _ := barKP.PublicKey()
|
|
barAC := jwt.NewAccountClaims(barPub)
|
|
simport := &jwt.Import{Account: fooPub, Subject: "foo.private", Type: jwt.Stream}
|
|
|
|
activation := jwt.NewActivationClaims(barPub)
|
|
activation.ImportSubject = "foo.private"
|
|
activation.ImportType = jwt.Stream
|
|
actJWT, err := activation.Encode(fooKP)
|
|
if err != nil {
|
|
t.Fatalf("Error generating activation token: %v", err)
|
|
}
|
|
|
|
simport.Token = actJWT
|
|
barAC.Imports.Add(simport)
|
|
|
|
// Now revoke the export.
|
|
decoded, _ := jwt.DecodeActivationClaims(actJWT)
|
|
export.Revoke(decoded.Subject)
|
|
|
|
fooJWT, err := fooAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
addAccountToMemResolver(s, fooPub, fooJWT)
|
|
|
|
barJWT, err := barAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, barPub, barJWT)
|
|
|
|
fooAcc, _ := s.LookupAccount(fooPub)
|
|
if fooAcc == nil {
|
|
t.Fatalf("Expected to retrieve the account")
|
|
}
|
|
|
|
// Now lookup bar account and make sure it was revoked.
|
|
acc, _ := s.LookupAccount(barPub)
|
|
if acc == nil {
|
|
t.Fatalf("Expected to retrieve the account")
|
|
}
|
|
if les := len(acc.imports.streams); les != 0 {
|
|
t.Fatalf("Expected imports streams len of 0, got %d", les)
|
|
}
|
|
}
|
|
|
|
func TestJWTCircularAccountServiceImport(t *testing.T) {
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
|
|
// Create accounts
|
|
fooKP, _ := nkeys.CreateAccount()
|
|
fooPub, _ := fooKP.PublicKey()
|
|
fooAC := jwt.NewAccountClaims(fooPub)
|
|
|
|
barKP, _ := nkeys.CreateAccount()
|
|
barPub, _ := barKP.PublicKey()
|
|
barAC := jwt.NewAccountClaims(barPub)
|
|
|
|
// Create service export/import for account foo
|
|
serviceExport := &jwt.Export{Subject: "foo", Type: jwt.Service, TokenReq: true}
|
|
serviceImport := &jwt.Import{Account: barPub, Subject: "bar", Type: jwt.Service}
|
|
|
|
fooAC.Exports.Add(serviceExport)
|
|
fooAC.Imports.Add(serviceImport)
|
|
fooJWT, err := fooAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
addAccountToMemResolver(s, fooPub, fooJWT)
|
|
|
|
// Create service export/import for account bar
|
|
serviceExport = &jwt.Export{Subject: "bar", Type: jwt.Service, TokenReq: true}
|
|
serviceImport = &jwt.Import{Account: fooPub, Subject: "foo", Type: jwt.Service}
|
|
|
|
barAC.Exports.Add(serviceExport)
|
|
barAC.Imports.Add(serviceImport)
|
|
barJWT, err := barAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
addAccountToMemResolver(s, barPub, barJWT)
|
|
|
|
c, cr, cs := createClient(t, s, fooKP)
|
|
defer c.close()
|
|
|
|
c.parseAsync(cs)
|
|
expectPong(t, cr)
|
|
|
|
c.parseAsync("SUB foo 1\r\nPING\r\n")
|
|
expectPong(t, cr)
|
|
}
|
|
|
|
// This test ensures that connected clients are properly evicted
|
|
// (no deadlock) if the max conns of an account has been lowered
|
|
// and the account is being updated (following expiration during
|
|
// a lookup).
|
|
func TestJWTAccountLimitsMaxConnsAfterExpired(t *testing.T) {
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
|
|
// Create accounts and imports/exports.
|
|
fooKP, _ := nkeys.CreateAccount()
|
|
fooPub, _ := fooKP.PublicKey()
|
|
fooAC := jwt.NewAccountClaims(fooPub)
|
|
fooAC.Limits.Conn = 10
|
|
fooJWT, err := fooAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, fooPub, fooJWT)
|
|
|
|
newClient := func(expPre string) *testAsyncClient {
|
|
t.Helper()
|
|
// Create a client.
|
|
c, cr, cs := createClient(t, s, fooKP)
|
|
c.parseAsync(cs)
|
|
l, _ := cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, expPre) {
|
|
t.Fatalf("Expected a response starting with %q, got %q", expPre, l)
|
|
}
|
|
go func() {
|
|
for {
|
|
if _, _, err := cr.ReadLine(); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
return c
|
|
}
|
|
|
|
for i := 0; i < 4; i++ {
|
|
c := newClient("PONG")
|
|
defer c.close()
|
|
}
|
|
|
|
// We will simulate that the account has expired. When
|
|
// a new client will connect, the server will do a lookup
|
|
// and find the account expired, which then will cause
|
|
// a fetch and a rebuild of the account. Since max conns
|
|
// is now lower, some clients should have been removed.
|
|
acc, _ := s.LookupAccount(fooPub)
|
|
acc.mu.Lock()
|
|
acc.expired = true
|
|
acc.updated = time.Now().UTC().Add(-2 * time.Second) // work around updating to quickly
|
|
acc.mu.Unlock()
|
|
|
|
// Now update with new expiration and max connections lowered to 2
|
|
fooAC.Limits.Conn = 2
|
|
fooJWT, err = fooAC.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
addAccountToMemResolver(s, fooPub, fooJWT)
|
|
|
|
// Cause the lookup that will detect that account was expired
|
|
// and rebuild it, and kick clients out.
|
|
c := newClient("-ERR ")
|
|
defer c.close()
|
|
|
|
acc, _ = s.LookupAccount(fooPub)
|
|
checkFor(t, 2*time.Second, 15*time.Millisecond, func() error {
|
|
acc.mu.RLock()
|
|
numClients := len(acc.clients)
|
|
acc.mu.RUnlock()
|
|
if numClients != 2 {
|
|
return fmt.Errorf("Should have 2 clients, got %v", numClients)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func TestBearerToken(t *testing.T) {
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
akp, _ := nkeys.CreateAccount()
|
|
apub, _ := akp.PublicKey()
|
|
nac := jwt.NewAccountClaims(apub)
|
|
ajwt, err := nac.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
nkp, _ := nkeys.CreateUser()
|
|
pub, _ := nkp.PublicKey()
|
|
nuc := newJWTTestUserClaims()
|
|
nuc.Subject = pub
|
|
// Set bearer token.
|
|
nuc.BearerToken = true
|
|
jwt, err := nuc.Encode(akp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating user JWT: %v", err)
|
|
}
|
|
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
addAccountToMemResolver(s, apub, ajwt)
|
|
|
|
c, cr, _ := newClientForServer(s)
|
|
defer c.close()
|
|
|
|
// Skip nonce signature...
|
|
|
|
// PING needed to flush the +OK/-ERR to us.
|
|
cs := fmt.Sprintf("CONNECT {\"jwt\":%q,\"verbose\":true,\"pedantic\":true}\r\nPING\r\n", jwt)
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(1)
|
|
go func() {
|
|
c.parse([]byte(cs))
|
|
wg.Done()
|
|
}()
|
|
l, _ := cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "+OK") {
|
|
t.Fatalf("Expected +OK, got %s", l)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestBearerWithIssuerSameAsAccountToken(t *testing.T) {
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
akp, _ := nkeys.CreateAccount()
|
|
apub, _ := akp.PublicKey()
|
|
nac := jwt.NewAccountClaims(apub)
|
|
ajwt, err := nac.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
nkp, _ := nkeys.CreateUser()
|
|
pub, _ := nkp.PublicKey()
|
|
nuc := newJWTTestUserClaims()
|
|
// we are setting the issuer account here to trigger verification
|
|
// of the issuer - the account has no signing keys, but the issuer
|
|
// account is set to the public key of the account which should be OK.
|
|
nuc.IssuerAccount = apub
|
|
nuc.Subject = pub
|
|
// Set bearer token.
|
|
nuc.BearerToken = true
|
|
jwt, err := nuc.Encode(akp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating user JWT: %v", err)
|
|
}
|
|
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
addAccountToMemResolver(s, apub, ajwt)
|
|
|
|
c, cr, _ := newClientForServer(s)
|
|
defer c.close()
|
|
|
|
// Skip nonce signature...
|
|
|
|
// PING needed to flush the +OK/-ERR to us.
|
|
cs := fmt.Sprintf("CONNECT {\"jwt\":%q,\"verbose\":true,\"pedantic\":true}\r\nPING\r\n", jwt)
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(1)
|
|
go func() {
|
|
c.parse([]byte(cs))
|
|
wg.Done()
|
|
}()
|
|
l, _ := cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "+OK") {
|
|
t.Fatalf("Expected +OK, got %s", l)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestBearerWithBadIssuerToken(t *testing.T) {
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
akp, _ := nkeys.CreateAccount()
|
|
apub, _ := akp.PublicKey()
|
|
nac := jwt.NewAccountClaims(apub)
|
|
ajwt, err := nac.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
nkp, _ := nkeys.CreateUser()
|
|
pub, _ := nkp.PublicKey()
|
|
nuc := newJWTTestUserClaims()
|
|
bakp, _ := nkeys.CreateAccount()
|
|
bapub, _ := bakp.PublicKey()
|
|
nuc.IssuerAccount = bapub
|
|
nuc.Subject = pub
|
|
// Set bearer token.
|
|
nuc.BearerToken = true
|
|
jwt, err := nuc.Encode(akp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating user JWT: %v", err)
|
|
}
|
|
|
|
s := opTrustBasicSetup()
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
addAccountToMemResolver(s, apub, ajwt)
|
|
|
|
c, cr, _ := newClientForServer(s)
|
|
defer c.close()
|
|
|
|
// Skip nonce signature...
|
|
|
|
// PING needed to flush the +OK/-ERR to us.
|
|
cs := fmt.Sprintf("CONNECT {\"jwt\":%q,\"verbose\":true,\"pedantic\":true}\r\nPING\r\n", jwt)
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(1)
|
|
go func() {
|
|
c.parse([]byte(cs))
|
|
wg.Done()
|
|
}()
|
|
l, _ := cr.ReadString('\n')
|
|
if !strings.HasPrefix(l, "-ERR") {
|
|
t.Fatalf("Expected -ERR, got %s", l)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestExpiredUserCredentialsRenewal(t *testing.T) {
|
|
createTmpFile := func(t *testing.T, content []byte) string {
|
|
t.Helper()
|
|
conf := createFile(t, "")
|
|
fName := conf.Name()
|
|
conf.Close()
|
|
if err := ioutil.WriteFile(fName, content, 0666); err != nil {
|
|
removeFile(t, fName)
|
|
t.Fatalf("Error writing conf file: %v", err)
|
|
}
|
|
return fName
|
|
}
|
|
waitTime := func(ch chan bool, timeout time.Duration) error {
|
|
select {
|
|
case <-ch:
|
|
return nil
|
|
case <-time.After(timeout):
|
|
}
|
|
return errors.New("timeout")
|
|
}
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
akp, err := nkeys.CreateAccount()
|
|
if err != nil {
|
|
t.Fatalf("Error generating account")
|
|
}
|
|
aPub, _ := akp.PublicKey()
|
|
nac := jwt.NewAccountClaims(aPub)
|
|
aJwt, err := nac.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
kp, _ := nkeys.FromSeed(oSeed)
|
|
oPub, _ := kp.PublicKey()
|
|
opts := defaultServerOptions
|
|
opts.TrustedKeys = []string{oPub}
|
|
s := RunServer(&opts)
|
|
if s == nil {
|
|
t.Fatal("Server did not start")
|
|
}
|
|
defer s.Shutdown()
|
|
buildMemAccResolver(s)
|
|
addAccountToMemResolver(s, aPub, aJwt)
|
|
|
|
nkp, _ := nkeys.CreateUser()
|
|
pub, _ := nkp.PublicKey()
|
|
uSeed, _ := nkp.Seed()
|
|
nuc := newJWTTestUserClaims()
|
|
nuc.Subject = pub
|
|
nuc.Expires = time.Now().Add(time.Second).Unix()
|
|
uJwt, err := nuc.Encode(akp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating user JWT: %v", err)
|
|
}
|
|
|
|
creds, err := jwt.FormatUserConfig(uJwt, uSeed)
|
|
if err != nil {
|
|
t.Fatalf("Error encoding credentials: %v", err)
|
|
}
|
|
chainedFile := createTmpFile(t, creds)
|
|
defer removeFile(t, chainedFile)
|
|
|
|
rch := make(chan bool)
|
|
|
|
url := fmt.Sprintf("nats://%s:%d", s.opts.Host, s.opts.Port)
|
|
nc, err := nats.Connect(url,
|
|
nats.UserCredentials(chainedFile),
|
|
nats.ReconnectWait(25*time.Millisecond),
|
|
nats.ReconnectJitter(0, 0),
|
|
nats.MaxReconnects(2),
|
|
nats.ErrorHandler(noOpErrHandler),
|
|
nats.ReconnectHandler(func(nc *nats.Conn) {
|
|
rch <- true
|
|
}),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Expected to connect, got %v %s", err, url)
|
|
}
|
|
defer nc.Close()
|
|
|
|
// Place new credentials underneath.
|
|
nuc.Expires = time.Now().Add(30 * time.Second).Unix()
|
|
uJwt, err = nuc.Encode(akp)
|
|
if err != nil {
|
|
t.Fatalf("Error encoding user jwt: %v", err)
|
|
}
|
|
creds, err = jwt.FormatUserConfig(uJwt, uSeed)
|
|
if err != nil {
|
|
t.Fatalf("Error encoding credentials: %v", err)
|
|
}
|
|
if err := ioutil.WriteFile(chainedFile, creds, 0666); err != nil {
|
|
t.Fatalf("Error writing conf file: %v", err)
|
|
}
|
|
|
|
// Make sure we get disconnected and reconnected first.
|
|
if err := waitTime(rch, 2*time.Second); err != nil {
|
|
t.Fatal("Should have reconnected.")
|
|
}
|
|
|
|
// We should not have been closed.
|
|
if nc.IsClosed() {
|
|
t.Fatal("Got disconnected when we should have reconnected.")
|
|
}
|
|
|
|
// Check that we clear the lastErr that can cause the disconnect.
|
|
// Our reconnect CB will happen before the clear. So check after a bit.
|
|
time.Sleep(50 * time.Millisecond)
|
|
if nc.LastError() != nil {
|
|
t.Fatalf("Expected lastErr to be cleared, got %q", nc.LastError())
|
|
}
|
|
}
|
|
|
|
func updateJwt(t *testing.T, url string, creds string, jwt string, respCnt int) int {
|
|
t.Helper()
|
|
require_NextMsg := func(sub *nats.Subscription) bool {
|
|
t.Helper()
|
|
msg := natsNexMsg(t, sub, time.Second)
|
|
content := make(map[string]interface{})
|
|
json.Unmarshal(msg.Data, &content)
|
|
if _, ok := content["data"]; ok {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
c := natsConnect(t, url, nats.UserCredentials(creds),
|
|
nats.DisconnectErrHandler(func(_ *nats.Conn, err error) {
|
|
if err != nil {
|
|
t.Fatal("error not expected in this test", err)
|
|
}
|
|
}),
|
|
nats.ErrorHandler(func(_ *nats.Conn, _ *nats.Subscription, err error) {
|
|
t.Fatal("error not expected in this test", err)
|
|
}),
|
|
)
|
|
defer c.Close()
|
|
resp := c.NewRespInbox()
|
|
sub := natsSubSync(t, c, resp)
|
|
err := sub.AutoUnsubscribe(respCnt)
|
|
require_NoError(t, err)
|
|
require_NoError(t, c.PublishRequest(accClaimsReqSubj, resp, []byte(jwt)))
|
|
passCnt := 0
|
|
for i := 0; i < respCnt; i++ {
|
|
if require_NextMsg(sub) {
|
|
passCnt++
|
|
}
|
|
}
|
|
return passCnt
|
|
}
|
|
|
|
func require_JWTAbsent(t *testing.T, dir string, pub string) {
|
|
t.Helper()
|
|
_, err := os.Stat(filepath.Join(dir, pub+".jwt"))
|
|
require_Error(t, err)
|
|
require_True(t, os.IsNotExist(err))
|
|
}
|
|
|
|
func require_JWTPresent(t *testing.T, dir string, pub string) {
|
|
t.Helper()
|
|
_, err := os.Stat(filepath.Join(dir, pub+".jwt"))
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
func require_JWTEqual(t *testing.T, dir string, pub string, jwt string) {
|
|
t.Helper()
|
|
content, err := ioutil.ReadFile(filepath.Join(dir, pub+".jwt"))
|
|
require_NoError(t, err)
|
|
require_Equal(t, string(content), jwt)
|
|
}
|
|
|
|
func createDir(t testing.TB, prefix string) string {
|
|
t.Helper()
|
|
err := os.MkdirAll(tempRoot, 0700)
|
|
require_NoError(t, err)
|
|
dir, err := ioutil.TempDir(tempRoot, prefix)
|
|
require_NoError(t, err)
|
|
return dir
|
|
}
|
|
|
|
func createFile(t *testing.T, prefix string) *os.File {
|
|
t.Helper()
|
|
err := os.MkdirAll(tempRoot, 0700)
|
|
require_NoError(t, err)
|
|
return createFileAtDir(t, tempRoot, prefix)
|
|
}
|
|
|
|
func createFileAtDir(t *testing.T, dir, prefix string) *os.File {
|
|
t.Helper()
|
|
f, err := ioutil.TempFile(dir, prefix)
|
|
require_NoError(t, err)
|
|
return f
|
|
}
|
|
|
|
func removeDir(t *testing.T, dir string) {
|
|
t.Helper()
|
|
if err := os.RemoveAll(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func removeFile(t *testing.T, p string) {
|
|
t.Helper()
|
|
if err := os.Remove(p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func writeJWT(t *testing.T, dir string, pub string, jwt string) {
|
|
t.Helper()
|
|
err := ioutil.WriteFile(filepath.Join(dir, pub+".jwt"), []byte(jwt), 0644)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
func TestAccountNATSResolverFetch(t *testing.T) {
|
|
origEventsHBInterval := eventsHBInterval
|
|
eventsHBInterval = 50 * time.Millisecond // speed up eventing
|
|
defer func() { eventsHBInterval = origEventsHBInterval }()
|
|
require_NoLocalOrRemoteConnections := func(account string, srvs ...*Server) {
|
|
t.Helper()
|
|
for _, srv := range srvs {
|
|
if acc, ok := srv.accounts.Load(account); ok {
|
|
checkAccClientsCount(t, acc.(*Account), 0)
|
|
}
|
|
}
|
|
}
|
|
// After each connection check, require_XConnection and connect assures that
|
|
// listed server have no connections for the account used
|
|
require_1Connection := func(url, creds, acc string, srvs ...*Server) {
|
|
t.Helper()
|
|
func() {
|
|
t.Helper()
|
|
c := natsConnect(t, url, nats.UserCredentials(creds))
|
|
defer c.Close()
|
|
if _, err := nats.Connect(url, nats.UserCredentials(creds)); err == nil {
|
|
t.Fatal("Second connection was supposed to fail due to limits")
|
|
} else if !strings.Contains(err.Error(), ErrTooManyAccountConnections.Error()) {
|
|
t.Fatal("Second connection was supposed to fail with too many conns")
|
|
}
|
|
}()
|
|
require_NoLocalOrRemoteConnections(acc, srvs...)
|
|
}
|
|
require_2Connection := func(url, creds, acc string, srvs ...*Server) {
|
|
t.Helper()
|
|
func() {
|
|
t.Helper()
|
|
c1 := natsConnect(t, url, nats.UserCredentials(creds))
|
|
defer c1.Close()
|
|
c2 := natsConnect(t, url, nats.UserCredentials(creds))
|
|
defer c2.Close()
|
|
if _, err := nats.Connect(url, nats.UserCredentials(creds)); err == nil {
|
|
t.Fatal("Third connection was supposed to fail due to limits")
|
|
} else if !strings.Contains(err.Error(), ErrTooManyAccountConnections.Error()) {
|
|
t.Fatal("Third connection was supposed to fail with too many conns")
|
|
}
|
|
}()
|
|
require_NoLocalOrRemoteConnections(acc, srvs...)
|
|
}
|
|
connect := func(url string, credsfile string, acc string, srvs ...*Server) {
|
|
t.Helper()
|
|
nc := natsConnect(t, url, nats.UserCredentials(credsfile), nats.Timeout(5*time.Second))
|
|
nc.Close()
|
|
require_NoLocalOrRemoteConnections(acc, srvs...)
|
|
}
|
|
createAccountAndUser := func(limit bool, done chan struct{}, pubKey, jwt1, jwt2, creds *string) {
|
|
t.Helper()
|
|
kp, _ := nkeys.CreateAccount()
|
|
*pubKey, _ = kp.PublicKey()
|
|
claim := jwt.NewAccountClaims(*pubKey)
|
|
if limit {
|
|
claim.Limits.Conn = 1
|
|
}
|
|
var err error
|
|
*jwt1, err = claim.Encode(oKp)
|
|
require_NoError(t, err)
|
|
// need to assure that create time differs (resolution is sec)
|
|
time.Sleep(time.Millisecond * 1100)
|
|
// create updated claim allowing more connections
|
|
if limit {
|
|
claim.Limits.Conn = 2
|
|
}
|
|
*jwt2, err = claim.Encode(oKp)
|
|
require_NoError(t, err)
|
|
ukp, _ := nkeys.CreateUser()
|
|
seed, _ := ukp.Seed()
|
|
upub, _ := ukp.PublicKey()
|
|
uclaim := newJWTTestUserClaims()
|
|
uclaim.Subject = upub
|
|
ujwt, err := uclaim.Encode(kp)
|
|
require_NoError(t, err)
|
|
*creds = genCredsFile(t, ujwt, seed)
|
|
done <- struct{}{}
|
|
}
|
|
// Create Accounts and corresponding user creds. Do so concurrently to speed up the test
|
|
doneChan := make(chan struct{}, 5)
|
|
defer close(doneChan)
|
|
var syspub, sysjwt, dummy1, sysCreds string
|
|
go createAccountAndUser(false, doneChan, &syspub, &sysjwt, &dummy1, &sysCreds)
|
|
var apub, ajwt1, ajwt2, aCreds string
|
|
go createAccountAndUser(true, doneChan, &apub, &ajwt1, &ajwt2, &aCreds)
|
|
var bpub, bjwt1, bjwt2, bCreds string
|
|
go createAccountAndUser(true, doneChan, &bpub, &bjwt1, &bjwt2, &bCreds)
|
|
var cpub, cjwt1, cjwt2, cCreds string
|
|
go createAccountAndUser(true, doneChan, &cpub, &cjwt1, &cjwt2, &cCreds)
|
|
var dpub, djwt1, dummy2, dCreds string // extra user used later in the test in order to test limits
|
|
go createAccountAndUser(true, doneChan, &dpub, &djwt1, &dummy2, &dCreds)
|
|
for i := 0; i < cap(doneChan); i++ {
|
|
<-doneChan
|
|
}
|
|
defer removeFile(t, sysCreds)
|
|
defer removeFile(t, aCreds)
|
|
defer removeFile(t, bCreds)
|
|
defer removeFile(t, cCreds)
|
|
defer removeFile(t, dCreds)
|
|
// Create one directory for each server
|
|
dirA := createDir(t, "srv-a")
|
|
defer removeDir(t, dirA)
|
|
dirB := createDir(t, "srv-b")
|
|
defer removeDir(t, dirB)
|
|
dirC := createDir(t, "srv-c")
|
|
defer removeDir(t, dirC)
|
|
// simulate a restart of the server by storing files in them
|
|
// Server A/B will completely sync, so after startup each server
|
|
// will contain the union off all stored/configured jwt
|
|
// Server C will send out lookup requests for jwt it does not store itself
|
|
writeJWT(t, dirA, apub, ajwt1)
|
|
writeJWT(t, dirB, bpub, bjwt1)
|
|
writeJWT(t, dirC, cpub, cjwt1)
|
|
// Create seed server A (using no_advertise to prevent fail over)
|
|
confA := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
server_name: srv-A
|
|
operator: %s
|
|
system_account: %s
|
|
resolver: {
|
|
type: full
|
|
dir: %s
|
|
interval: "200ms"
|
|
limit: 4
|
|
}
|
|
resolver_preload: {
|
|
%s: %s
|
|
}
|
|
cluster {
|
|
name: clust
|
|
listen: -1
|
|
no_advertise: true
|
|
}
|
|
`, ojwt, syspub, dirA, cpub, cjwt1)))
|
|
defer removeFile(t, confA)
|
|
sA, _ := RunServerWithConfig(confA)
|
|
defer sA.Shutdown()
|
|
// during startup resolver_preload causes the directory to contain data
|
|
require_JWTPresent(t, dirA, cpub)
|
|
// Create Server B (using no_advertise to prevent fail over)
|
|
confB := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
server_name: srv-B
|
|
operator: %s
|
|
system_account: %s
|
|
resolver: {
|
|
type: full
|
|
dir: %s
|
|
interval: "200ms"
|
|
limit: 4
|
|
}
|
|
cluster {
|
|
name: clust
|
|
listen: -1
|
|
no_advertise: true
|
|
routes [
|
|
nats-route://localhost:%d
|
|
]
|
|
}
|
|
`, ojwt, syspub, dirB, sA.opts.Cluster.Port)))
|
|
defer removeFile(t, confB)
|
|
sB, _ := RunServerWithConfig(confB)
|
|
defer sB.Shutdown()
|
|
// Create Server C (using no_advertise to prevent fail over)
|
|
fmtC := `
|
|
listen: -1
|
|
server_name: srv-C
|
|
operator: %s
|
|
system_account: %s
|
|
resolver: {
|
|
type: cache
|
|
dir: %s
|
|
ttl: "%dms"
|
|
limit: 4
|
|
}
|
|
cluster {
|
|
name: clust
|
|
listen: -1
|
|
no_advertise: true
|
|
routes [
|
|
nats-route://localhost:%d
|
|
]
|
|
}
|
|
`
|
|
confClongTTL := createConfFile(t, []byte(fmt.Sprintf(fmtC, ojwt, syspub, dirC, 10000, sA.opts.Cluster.Port)))
|
|
defer removeFile(t, confClongTTL)
|
|
confCshortTTL := createConfFile(t, []byte(fmt.Sprintf(fmtC, ojwt, syspub, dirC, 1000, sA.opts.Cluster.Port)))
|
|
defer removeFile(t, confCshortTTL)
|
|
sC, _ := RunServerWithConfig(confClongTTL) // use long ttl to assure it is not kicking
|
|
defer sC.Shutdown()
|
|
// startup cluster
|
|
checkClusterFormed(t, sA, sB, sC)
|
|
time.Sleep(500 * time.Millisecond) // wait for the protocol to converge
|
|
// Check all accounts
|
|
require_JWTPresent(t, dirA, apub) // was already present on startup
|
|
require_JWTPresent(t, dirB, apub) // was copied from server A
|
|
require_JWTAbsent(t, dirC, apub)
|
|
require_JWTPresent(t, dirA, bpub) // was copied from server B
|
|
require_JWTPresent(t, dirB, bpub) // was already present on startup
|
|
require_JWTAbsent(t, dirC, bpub)
|
|
require_JWTPresent(t, dirA, cpub) // was present in preload
|
|
require_JWTPresent(t, dirB, cpub) // was copied from server A
|
|
require_JWTPresent(t, dirC, cpub) // was already present on startup
|
|
// This is to test that connecting to it still works
|
|
require_JWTAbsent(t, dirA, syspub)
|
|
require_JWTAbsent(t, dirB, syspub)
|
|
require_JWTAbsent(t, dirC, syspub)
|
|
// system account client can connect to every server
|
|
connect(sA.ClientURL(), sysCreds, "")
|
|
connect(sB.ClientURL(), sysCreds, "")
|
|
connect(sC.ClientURL(), sysCreds, "")
|
|
checkClusterFormed(t, sA, sB, sC)
|
|
// upload system account and require a response from each server
|
|
passCnt := updateJwt(t, sA.ClientURL(), sysCreds, sysjwt, 3)
|
|
require_True(t, passCnt == 3)
|
|
require_JWTPresent(t, dirA, syspub) // was just received
|
|
require_JWTPresent(t, dirB, syspub) // was just received
|
|
require_JWTPresent(t, dirC, syspub) // was just received
|
|
// Only files missing are in C, which is only caching
|
|
connect(sC.ClientURL(), aCreds, apub, sA, sB, sC)
|
|
connect(sC.ClientURL(), bCreds, bpub, sA, sB, sC)
|
|
require_JWTPresent(t, dirC, apub) // was looked up form A or B
|
|
require_JWTPresent(t, dirC, bpub) // was looked up from A or B
|
|
// Check limits and update jwt B connecting to server A
|
|
for port, v := range map[string]struct{ pub, jwt, creds string }{
|
|
sB.ClientURL(): {bpub, bjwt2, bCreds},
|
|
sC.ClientURL(): {cpub, cjwt2, cCreds},
|
|
} {
|
|
require_1Connection(sA.ClientURL(), v.creds, v.pub, sA, sB, sC)
|
|
require_1Connection(sB.ClientURL(), v.creds, v.pub, sA, sB, sC)
|
|
require_1Connection(sC.ClientURL(), v.creds, v.pub, sA, sB, sC)
|
|
passCnt := updateJwt(t, port, sysCreds, v.jwt, 3)
|
|
require_True(t, passCnt == 3)
|
|
require_2Connection(sA.ClientURL(), v.creds, v.pub, sA, sB, sC)
|
|
require_2Connection(sB.ClientURL(), v.creds, v.pub, sA, sB, sC)
|
|
require_2Connection(sC.ClientURL(), v.creds, v.pub, sA, sB, sC)
|
|
require_JWTEqual(t, dirA, v.pub, v.jwt)
|
|
require_JWTEqual(t, dirB, v.pub, v.jwt)
|
|
require_JWTEqual(t, dirC, v.pub, v.jwt)
|
|
}
|
|
// Simulates A having missed an update
|
|
// shutting B down as it has it will directly connect to A and connect right away
|
|
sB.Shutdown()
|
|
writeJWT(t, dirB, apub, ajwt2) // this will be copied to server A
|
|
sB, _ = RunServerWithConfig(confB)
|
|
defer sB.Shutdown()
|
|
checkClusterFormed(t, sA, sB, sC)
|
|
time.Sleep(500 * time.Millisecond) // wait for the protocol to converge
|
|
// Restart server C. this is a workaround to force C to do a lookup in the absence of account cleanup
|
|
sC.Shutdown()
|
|
sC, _ = RunServerWithConfig(confClongTTL) //TODO remove this once we clean up accounts
|
|
defer sC.Shutdown()
|
|
require_JWTEqual(t, dirA, apub, ajwt2) // was copied from server B
|
|
require_JWTEqual(t, dirB, apub, ajwt2) // was restarted with this
|
|
require_JWTEqual(t, dirC, apub, ajwt1) // still contains old cached value
|
|
require_2Connection(sA.ClientURL(), aCreds, apub, sA, sB, sC)
|
|
require_2Connection(sB.ClientURL(), aCreds, apub, sA, sB, sC)
|
|
require_1Connection(sC.ClientURL(), aCreds, apub, sA, sB, sC)
|
|
// Restart server C. this is a workaround to force C to do a lookup in the absence of account cleanup
|
|
sC.Shutdown()
|
|
sC, _ = RunServerWithConfig(confCshortTTL) //TODO remove this once we clean up accounts
|
|
defer sC.Shutdown()
|
|
require_JWTEqual(t, dirC, apub, ajwt1) // still contains old cached value
|
|
checkClusterFormed(t, sA, sB, sC)
|
|
// Force next connect to do a lookup exceeds ttl
|
|
fname := filepath.Join(dirC, apub+".jwt")
|
|
checkFor(t, 2*time.Second, 100*time.Millisecond, func() error {
|
|
_, err := os.Stat(fname)
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("File not removed in time")
|
|
})
|
|
connect(sC.ClientURL(), aCreds, apub, sA, sB, sC) // When lookup happens
|
|
require_JWTEqual(t, dirC, apub, ajwt2) // was looked up form A or B
|
|
require_2Connection(sC.ClientURL(), aCreds, apub, sA, sB, sC)
|
|
// Test exceeding limit. For the exclusive directory resolver, limit is a stop gap measure.
|
|
// It is not expected to be hit. When hit the administrator is supposed to take action.
|
|
passCnt = updateJwt(t, sA.ClientURL(), sysCreds, djwt1, 3)
|
|
require_True(t, passCnt == 1) // Only Server C updated
|
|
for _, srv := range []*Server{sA, sB, sC} {
|
|
if a, ok := srv.accounts.Load(syspub); ok {
|
|
acc := a.(*Account)
|
|
checkFor(t, time.Second, 20*time.Millisecond, func() error {
|
|
acc.mu.Lock()
|
|
defer acc.mu.Unlock()
|
|
if acc.ctmr != nil {
|
|
return fmt.Errorf("Timer still exists")
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAccountNATSResolverCrossClusterFetch(t *testing.T) {
|
|
connect := func(url string, credsfile string) {
|
|
t.Helper()
|
|
nc := natsConnect(t, url, nats.UserCredentials(credsfile))
|
|
nc.Close()
|
|
}
|
|
createAccountAndUser := func(done chan struct{}, pubKey, jwt1, jwt2, creds *string) {
|
|
t.Helper()
|
|
kp, _ := nkeys.CreateAccount()
|
|
*pubKey, _ = kp.PublicKey()
|
|
claim := jwt.NewAccountClaims(*pubKey)
|
|
var err error
|
|
*jwt1, err = claim.Encode(oKp)
|
|
require_NoError(t, err)
|
|
// need to assure that create time differs (resolution is sec)
|
|
time.Sleep(time.Millisecond * 1100)
|
|
// create updated claim
|
|
claim.Tags.Add("tag")
|
|
*jwt2, err = claim.Encode(oKp)
|
|
require_NoError(t, err)
|
|
ukp, _ := nkeys.CreateUser()
|
|
seed, _ := ukp.Seed()
|
|
upub, _ := ukp.PublicKey()
|
|
uclaim := newJWTTestUserClaims()
|
|
uclaim.Subject = upub
|
|
ujwt, err := uclaim.Encode(kp)
|
|
require_NoError(t, err)
|
|
*creds = genCredsFile(t, ujwt, seed)
|
|
done <- struct{}{}
|
|
}
|
|
// Create Accounts and corresponding user creds. Do so concurrently to speed up the test
|
|
doneChan := make(chan struct{}, 3)
|
|
defer close(doneChan)
|
|
var syspub, sysjwt, dummy1, sysCreds string
|
|
go createAccountAndUser(doneChan, &syspub, &sysjwt, &dummy1, &sysCreds)
|
|
var apub, ajwt1, ajwt2, aCreds string
|
|
go createAccountAndUser(doneChan, &apub, &ajwt1, &ajwt2, &aCreds)
|
|
var bpub, bjwt1, bjwt2, bCreds string
|
|
go createAccountAndUser(doneChan, &bpub, &bjwt1, &bjwt2, &bCreds)
|
|
for i := 0; i < cap(doneChan); i++ {
|
|
<-doneChan
|
|
}
|
|
defer removeFile(t, sysCreds)
|
|
defer removeFile(t, aCreds)
|
|
defer removeFile(t, bCreds)
|
|
// Create one directory for each server
|
|
dirAA := createDir(t, "srv-a-a")
|
|
defer removeDir(t, dirAA)
|
|
dirAB := createDir(t, "srv-a-b")
|
|
defer removeDir(t, dirAB)
|
|
dirBA := createDir(t, "srv-b-a")
|
|
defer removeDir(t, dirBA)
|
|
dirBB := createDir(t, "srv-b-b")
|
|
defer removeDir(t, dirBB)
|
|
// simulate a restart of the server by storing files in them
|
|
// Server AA & AB will completely sync
|
|
// Server BA & BB will completely sync
|
|
// Be aware that no syncing will occur between cluster
|
|
writeJWT(t, dirAA, apub, ajwt1)
|
|
writeJWT(t, dirBA, bpub, bjwt1)
|
|
// Create seed server A (using no_advertise to prevent fail over)
|
|
confAA := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
server_name: srv-A-A
|
|
operator: %s
|
|
system_account: %s
|
|
resolver: {
|
|
type: full
|
|
dir: %s
|
|
interval: "200ms"
|
|
}
|
|
gateway: {
|
|
name: "clust-A"
|
|
listen: -1
|
|
}
|
|
cluster {
|
|
name: clust-A
|
|
listen: -1
|
|
no_advertise: true
|
|
}
|
|
`, ojwt, syspub, dirAA)))
|
|
defer removeFile(t, confAA)
|
|
sAA, _ := RunServerWithConfig(confAA)
|
|
defer sAA.Shutdown()
|
|
// Create Server B (using no_advertise to prevent fail over)
|
|
confAB := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
server_name: srv-A-B
|
|
operator: %s
|
|
system_account: %s
|
|
resolver: {
|
|
type: full
|
|
dir: %s
|
|
interval: "200ms"
|
|
}
|
|
gateway: {
|
|
name: "clust-A"
|
|
listen: -1
|
|
}
|
|
cluster {
|
|
name: clust-A
|
|
listen: -1
|
|
no_advertise: true
|
|
routes [
|
|
nats-route://localhost:%d
|
|
]
|
|
}
|
|
`, ojwt, syspub, dirAB, sAA.opts.Cluster.Port)))
|
|
defer removeFile(t, confAB)
|
|
sAB, _ := RunServerWithConfig(confAB)
|
|
defer sAB.Shutdown()
|
|
// Create Server C (using no_advertise to prevent fail over)
|
|
confBA := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
server_name: srv-B-A
|
|
operator: %s
|
|
system_account: %s
|
|
resolver: {
|
|
type: full
|
|
dir: %s
|
|
interval: "200ms"
|
|
}
|
|
gateway: {
|
|
name: "clust-B"
|
|
listen: -1
|
|
gateways: [
|
|
{name: "clust-A", url: "nats://localhost:%d"},
|
|
]
|
|
}
|
|
cluster {
|
|
name: clust-B
|
|
listen: -1
|
|
no_advertise: true
|
|
}
|
|
`, ojwt, syspub, dirBA, sAA.opts.Gateway.Port)))
|
|
defer removeFile(t, confBA)
|
|
sBA, _ := RunServerWithConfig(confBA)
|
|
defer sBA.Shutdown()
|
|
// Create Sever BA (using no_advertise to prevent fail over)
|
|
confBB := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
server_name: srv-B-B
|
|
operator: %s
|
|
system_account: %s
|
|
resolver: {
|
|
type: full
|
|
dir: %s
|
|
interval: "200ms"
|
|
}
|
|
cluster {
|
|
name: clust-B
|
|
listen: -1
|
|
no_advertise: true
|
|
routes [
|
|
nats-route://localhost:%d
|
|
]
|
|
}
|
|
gateway: {
|
|
name: "clust-B"
|
|
listen: -1
|
|
gateways: [
|
|
{name: "clust-A", url: "nats://localhost:%d"},
|
|
]
|
|
}
|
|
`, ojwt, syspub, dirBB, sBA.opts.Cluster.Port, sAA.opts.Cluster.Port)))
|
|
defer removeFile(t, confBB)
|
|
sBB, _ := RunServerWithConfig(confBB)
|
|
defer sBB.Shutdown()
|
|
// Assert topology
|
|
checkClusterFormed(t, sAA, sAB)
|
|
checkClusterFormed(t, sBA, sBB)
|
|
waitForOutboundGateways(t, sAA, 1, 5*time.Second)
|
|
waitForOutboundGateways(t, sAB, 1, 5*time.Second)
|
|
waitForOutboundGateways(t, sBA, 1, 5*time.Second)
|
|
waitForOutboundGateways(t, sBB, 1, 5*time.Second)
|
|
time.Sleep(500 * time.Millisecond) // wait for the protocol to converge
|
|
updateJwt(t, sAA.ClientURL(), sysCreds, sysjwt, 4) // update system account jwt on all server
|
|
require_JWTEqual(t, dirAA, syspub, sysjwt) // assure this update made it to every server
|
|
require_JWTEqual(t, dirAB, syspub, sysjwt) // assure this update made it to every server
|
|
require_JWTEqual(t, dirBA, syspub, sysjwt) // assure this update made it to every server
|
|
require_JWTEqual(t, dirBB, syspub, sysjwt) // assure this update made it to every server
|
|
require_JWTAbsent(t, dirAA, bpub) // assure that jwt are not synced across cluster
|
|
require_JWTAbsent(t, dirAB, bpub) // assure that jwt are not synced across cluster
|
|
require_JWTAbsent(t, dirBA, apub) // assure that jwt are not synced across cluster
|
|
require_JWTAbsent(t, dirBB, apub) // assure that jwt are not synced across cluster
|
|
connect(sAA.ClientURL(), aCreds) // connect to cluster where jwt was initially stored
|
|
connect(sAB.ClientURL(), aCreds) // connect to cluster where jwt was initially stored
|
|
connect(sBA.ClientURL(), bCreds) // connect to cluster where jwt was initially stored
|
|
connect(sBB.ClientURL(), bCreds) // connect to cluster where jwt was initially stored
|
|
time.Sleep(500 * time.Millisecond) // wait for the protocol to (NOT) converge
|
|
require_JWTAbsent(t, dirAA, bpub) // assure that jwt are still not synced across cluster
|
|
require_JWTAbsent(t, dirAB, bpub) // assure that jwt are still not synced across cluster
|
|
require_JWTAbsent(t, dirBA, apub) // assure that jwt are still not synced across cluster
|
|
require_JWTAbsent(t, dirBB, apub) // assure that jwt are still not synced across cluster
|
|
// We have verified that account B does not exist in cluster A, neither does account A in cluster B
|
|
// Despite that clients from account B can connect to server A, same for account A in cluster B
|
|
connect(sAA.ClientURL(), bCreds) // connect to cluster where jwt was not initially stored
|
|
connect(sAB.ClientURL(), bCreds) // connect to cluster where jwt was not initially stored
|
|
connect(sBA.ClientURL(), aCreds) // connect to cluster where jwt was not initially stored
|
|
connect(sBB.ClientURL(), aCreds) // connect to cluster where jwt was not initially stored
|
|
require_JWTEqual(t, dirAA, bpub, bjwt1) // assure that now jwt used in connect is stored
|
|
require_JWTEqual(t, dirAB, bpub, bjwt1) // assure that now jwt used in connect is stored
|
|
require_JWTEqual(t, dirBA, apub, ajwt1) // assure that now jwt used in connect is stored
|
|
require_JWTEqual(t, dirBB, apub, ajwt1) // assure that now jwt used in connect is stored
|
|
updateJwt(t, sAA.ClientURL(), sysCreds, bjwt2, 4) // update bjwt, expect updates from everywhere
|
|
updateJwt(t, sBA.ClientURL(), sysCreds, ajwt2, 4) // update ajwt, expect updates from everywhere
|
|
require_JWTEqual(t, dirAA, bpub, bjwt2) // assure that jwt got updated accordingly
|
|
require_JWTEqual(t, dirAB, bpub, bjwt2) // assure that jwt got updated accordingly
|
|
require_JWTEqual(t, dirBA, apub, ajwt2) // assure that jwt got updated accordingly
|
|
require_JWTEqual(t, dirBB, apub, ajwt2) // assure that jwt got updated accordingly
|
|
}
|
|
|
|
func newTimeRange(start time.Time, dur time.Duration) jwt.TimeRange {
|
|
return jwt.TimeRange{Start: start.Format("15:04:05"), End: start.Add(dur).Format("15:04:05")}
|
|
}
|
|
|
|
func createUserWithLimit(t *testing.T, accKp nkeys.KeyPair, expiration time.Time, limits func(*jwt.Limits)) string {
|
|
t.Helper()
|
|
ukp, _ := nkeys.CreateUser()
|
|
seed, _ := ukp.Seed()
|
|
upub, _ := ukp.PublicKey()
|
|
uclaim := newJWTTestUserClaims()
|
|
uclaim.Subject = upub
|
|
if limits != nil {
|
|
limits(&uclaim.Limits)
|
|
}
|
|
if !expiration.IsZero() {
|
|
uclaim.Expires = expiration.Unix()
|
|
}
|
|
vr := jwt.ValidationResults{}
|
|
uclaim.Validate(&vr)
|
|
require_Len(t, len(vr.Errors()), 0)
|
|
ujwt, err := uclaim.Encode(accKp)
|
|
require_NoError(t, err)
|
|
return genCredsFile(t, ujwt, seed)
|
|
}
|
|
|
|
func TestJWTUserLimits(t *testing.T) {
|
|
// helper for time
|
|
inAnHour := time.Now().Add(time.Hour)
|
|
inTwoHours := time.Now().Add(2 * time.Hour)
|
|
doNotExpire := time.Now().AddDate(1, 0, 0)
|
|
// create account
|
|
kp, _ := nkeys.CreateAccount()
|
|
aPub, _ := kp.PublicKey()
|
|
claim := jwt.NewAccountClaims(aPub)
|
|
aJwt, err := claim.Encode(oKp)
|
|
require_NoError(t, err)
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
operator: %s
|
|
resolver: MEM
|
|
resolver_preload: {
|
|
%s: %s
|
|
}
|
|
`, ojwt, aPub, aJwt)))
|
|
defer removeFile(t, conf)
|
|
sA, _ := RunServerWithConfig(conf)
|
|
defer sA.Shutdown()
|
|
for _, v := range []struct {
|
|
pass bool
|
|
f func(*jwt.Limits)
|
|
}{
|
|
{true, nil},
|
|
{false, func(j *jwt.Limits) { j.Src.Set("8.8.8.8/8") }},
|
|
{true, func(j *jwt.Limits) { j.Src.Set("8.8.8.8/0") }},
|
|
{true, func(j *jwt.Limits) { j.Src.Set("127.0.0.1/8") }},
|
|
{true, func(j *jwt.Limits) { j.Src.Set("8.8.8.8/8,127.0.0.1/8") }},
|
|
{false, func(j *jwt.Limits) { j.Src.Set("8.8.8.8/8,9.9.9.9/8") }},
|
|
{true, func(j *jwt.Limits) { j.Times = append(j.Times, newTimeRange(time.Now(), time.Hour)) }},
|
|
{false, func(j *jwt.Limits) { j.Times = append(j.Times, newTimeRange(time.Now().Add(time.Hour), time.Hour)) }},
|
|
{true, func(j *jwt.Limits) {
|
|
j.Times = append(j.Times, newTimeRange(inAnHour, time.Hour), newTimeRange(time.Now(), time.Hour))
|
|
}}, // last one is within range
|
|
{false, func(j *jwt.Limits) {
|
|
j.Times = append(j.Times, newTimeRange(inAnHour, time.Hour), newTimeRange(inTwoHours, time.Hour))
|
|
}}, // out of range
|
|
{false, func(j *jwt.Limits) {
|
|
j.Times = append(j.Times, newTimeRange(inAnHour, 3*time.Hour), newTimeRange(inTwoHours, 2*time.Hour))
|
|
}}, // overlapping [a[]b] out of range*/
|
|
{false, func(j *jwt.Limits) {
|
|
j.Times = append(j.Times, newTimeRange(inAnHour, 3*time.Hour), newTimeRange(inTwoHours, time.Hour))
|
|
}}, // overlapping [a[b]] out of range
|
|
// next day tests where end < begin
|
|
{true, func(j *jwt.Limits) { j.Times = append(j.Times, newTimeRange(time.Now(), 25*time.Hour)) }},
|
|
{true, func(j *jwt.Limits) { j.Times = append(j.Times, newTimeRange(time.Now(), -time.Hour)) }},
|
|
} {
|
|
t.Run("", func(t *testing.T) {
|
|
creds := createUserWithLimit(t, kp, doNotExpire, v.f)
|
|
defer removeFile(t, creds)
|
|
if c, err := nats.Connect(sA.ClientURL(), nats.UserCredentials(creds)); err == nil {
|
|
c.Close()
|
|
if !v.pass {
|
|
t.Fatalf("Expected failure got none")
|
|
}
|
|
} else if v.pass {
|
|
t.Fatalf("Expected success got %v", err)
|
|
} else if !strings.Contains(err.Error(), "Authorization Violation") {
|
|
t.Fatalf("Expected error other than %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJWTTimeExpiration(t *testing.T) {
|
|
validFor := 1500 * time.Millisecond
|
|
validRange := 500 * time.Millisecond
|
|
doNotExpire := time.Now().AddDate(1, 0, 0)
|
|
// create account
|
|
kp, _ := nkeys.CreateAccount()
|
|
aPub, _ := kp.PublicKey()
|
|
claim := jwt.NewAccountClaims(aPub)
|
|
aJwt, err := claim.Encode(oKp)
|
|
require_NoError(t, err)
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
operator: %s
|
|
resolver: MEM
|
|
resolver_preload: {
|
|
%s: %s
|
|
}
|
|
`, ojwt, aPub, aJwt)))
|
|
defer removeFile(t, conf)
|
|
sA, _ := RunServerWithConfig(conf)
|
|
defer sA.Shutdown()
|
|
for _, l := range []string{"", "Europe/Berlin", "America/New_York"} {
|
|
t.Run("simple expiration "+l, func(t *testing.T) {
|
|
start := time.Now()
|
|
creds := createUserWithLimit(t, kp, doNotExpire, func(j *jwt.Limits) {
|
|
if l == "" {
|
|
j.Times = []jwt.TimeRange{newTimeRange(start, validFor)}
|
|
} else {
|
|
loc, err := time.LoadLocation(l)
|
|
require_NoError(t, err)
|
|
j.Times = []jwt.TimeRange{newTimeRange(start.In(loc), validFor)}
|
|
j.Locale = l
|
|
}
|
|
})
|
|
defer removeFile(t, creds)
|
|
disconnectChan := make(chan struct{})
|
|
defer close(disconnectChan)
|
|
errChan := make(chan struct{})
|
|
defer close(errChan)
|
|
c := natsConnect(t, sA.ClientURL(),
|
|
nats.UserCredentials(creds),
|
|
nats.DisconnectErrHandler(func(conn *nats.Conn, err error) {
|
|
if err != io.EOF {
|
|
return
|
|
}
|
|
disconnectChan <- struct{}{}
|
|
}),
|
|
nats.ErrorHandler(func(conn *nats.Conn, s *nats.Subscription, err error) {
|
|
if err != nats.ErrAuthExpired {
|
|
return
|
|
}
|
|
now := time.Now()
|
|
stop := start.Add(validFor)
|
|
// assure event happens within a second of stop
|
|
if stop.Add(-validRange).Before(stop) && now.Before(stop.Add(validRange)) {
|
|
errChan <- struct{}{}
|
|
}
|
|
}))
|
|
chanRecv(t, errChan, 10*time.Second)
|
|
chanRecv(t, disconnectChan, 10*time.Second)
|
|
require_True(t, c.IsReconnecting())
|
|
require_False(t, c.IsConnected())
|
|
c.Close()
|
|
})
|
|
}
|
|
t.Run("double expiration", func(t *testing.T) {
|
|
start1 := time.Now()
|
|
start2 := start1.Add(2 * validFor)
|
|
creds := createUserWithLimit(t, kp, doNotExpire, func(j *jwt.Limits) {
|
|
j.Times = []jwt.TimeRange{newTimeRange(start1, validFor), newTimeRange(start2, validFor)}
|
|
})
|
|
defer removeFile(t, creds)
|
|
errChan := make(chan struct{})
|
|
defer close(errChan)
|
|
reConnectChan := make(chan struct{})
|
|
defer close(reConnectChan)
|
|
c := natsConnect(t, sA.ClientURL(),
|
|
nats.UserCredentials(creds),
|
|
nats.ReconnectHandler(func(conn *nats.Conn) {
|
|
reConnectChan <- struct{}{}
|
|
}),
|
|
nats.ErrorHandler(func(conn *nats.Conn, s *nats.Subscription, err error) {
|
|
if err != nats.ErrAuthExpired {
|
|
return
|
|
}
|
|
now := time.Now()
|
|
stop := start1.Add(validFor)
|
|
// assure event happens within a second of stop
|
|
if stop.Add(-validRange).Before(stop) && now.Before(stop.Add(validRange)) {
|
|
errChan <- struct{}{}
|
|
return
|
|
}
|
|
stop = start2.Add(validFor)
|
|
// assure event happens within a second of stop
|
|
if stop.Add(-validRange).Before(stop) && now.Before(stop.Add(validRange)) {
|
|
errChan <- struct{}{}
|
|
}
|
|
}))
|
|
chanRecv(t, errChan, 10*time.Second)
|
|
chanRecv(t, reConnectChan, 10*time.Second)
|
|
require_False(t, c.IsReconnecting())
|
|
require_True(t, c.IsConnected())
|
|
chanRecv(t, errChan, 10*time.Second)
|
|
c.Close()
|
|
})
|
|
t.Run("lower jwt expiration overwrites time", func(t *testing.T) {
|
|
start := time.Now()
|
|
creds := createUserWithLimit(t, kp, start.Add(validFor), func(j *jwt.Limits) { j.Times = []jwt.TimeRange{newTimeRange(start, 2*validFor)} })
|
|
defer removeFile(t, creds)
|
|
disconnectChan := make(chan struct{})
|
|
defer close(disconnectChan)
|
|
errChan := make(chan struct{})
|
|
defer close(errChan)
|
|
c := natsConnect(t, sA.ClientURL(),
|
|
nats.UserCredentials(creds),
|
|
nats.DisconnectErrHandler(func(conn *nats.Conn, err error) {
|
|
if err != io.EOF {
|
|
return
|
|
}
|
|
disconnectChan <- struct{}{}
|
|
}),
|
|
nats.ErrorHandler(func(conn *nats.Conn, s *nats.Subscription, err error) {
|
|
if err != nats.ErrAuthExpired {
|
|
return
|
|
}
|
|
now := time.Now()
|
|
stop := start.Add(validFor)
|
|
// assure event happens within a second of stop
|
|
if stop.Add(-validRange).Before(stop) && now.Before(stop.Add(validRange)) {
|
|
errChan <- struct{}{}
|
|
}
|
|
}))
|
|
chanRecv(t, errChan, 10*time.Second)
|
|
chanRecv(t, disconnectChan, 10*time.Second)
|
|
require_True(t, c.IsReconnecting())
|
|
require_False(t, c.IsConnected())
|
|
c.Close()
|
|
})
|
|
}
|
|
|
|
func TestJWTLimits(t *testing.T) {
|
|
doNotExpire := time.Now().AddDate(1, 0, 0)
|
|
// create account
|
|
kp, _ := nkeys.CreateAccount()
|
|
aPub, _ := kp.PublicKey()
|
|
claim := jwt.NewAccountClaims(aPub)
|
|
aJwt, err := claim.Encode(oKp)
|
|
require_NoError(t, err)
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
operator: %s
|
|
resolver: MEM
|
|
resolver_preload: {
|
|
%s: %s
|
|
}
|
|
`, ojwt, aPub, aJwt)))
|
|
defer removeFile(t, conf)
|
|
sA, _ := RunServerWithConfig(conf)
|
|
defer sA.Shutdown()
|
|
errChan := make(chan struct{})
|
|
defer close(errChan)
|
|
t.Run("subs", func(t *testing.T) {
|
|
creds := createUserWithLimit(t, kp, doNotExpire, func(j *jwt.Limits) { j.Subs = 1 })
|
|
defer removeFile(t, creds)
|
|
c := natsConnect(t, sA.ClientURL(), nats.UserCredentials(creds),
|
|
nats.DisconnectErrHandler(func(conn *nats.Conn, err error) {
|
|
if e := conn.LastError(); e != nil && strings.Contains(e.Error(), "maximum subscriptions exceeded") {
|
|
errChan <- struct{}{}
|
|
}
|
|
}),
|
|
)
|
|
defer c.Close()
|
|
if _, err := c.Subscribe("foo", func(msg *nats.Msg) {}); err != nil {
|
|
t.Fatalf("couldn't subscribe: %v", err)
|
|
}
|
|
if _, err = c.Subscribe("bar", func(msg *nats.Msg) {}); err != nil {
|
|
t.Fatalf("expected error got: %v", err)
|
|
}
|
|
chanRecv(t, errChan, time.Second)
|
|
})
|
|
t.Run("payload", func(t *testing.T) {
|
|
creds := createUserWithLimit(t, kp, doNotExpire, func(j *jwt.Limits) { j.Payload = 5 })
|
|
defer removeFile(t, creds)
|
|
c := natsConnect(t, sA.ClientURL(), nats.UserCredentials(creds))
|
|
defer c.Close()
|
|
if err := c.Flush(); err != nil {
|
|
t.Fatalf("flush failed %v", err)
|
|
}
|
|
if err := c.Publish("foo", []byte("world")); err != nil {
|
|
t.Fatalf("couldn't publish: %v", err)
|
|
}
|
|
if err := c.Publish("foo", []byte("worldX")); err != nats.ErrMaxPayload {
|
|
t.Fatalf("couldn't publish: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestJWTNoOperatorMode(t *testing.T) {
|
|
for _, login := range []bool{true, false} {
|
|
t.Run("", func(t *testing.T) {
|
|
opts := DefaultOptions()
|
|
if login {
|
|
opts.Users = append(opts.Users, &User{Username: "u", Password: "pwd"})
|
|
}
|
|
sA := RunServer(opts)
|
|
defer sA.Shutdown()
|
|
kp, _ := nkeys.CreateAccount()
|
|
creds := createUserWithLimit(t, kp, time.Now().Add(time.Hour), nil)
|
|
defer removeFile(t, creds)
|
|
url := sA.ClientURL()
|
|
if login {
|
|
url = fmt.Sprintf("nats://u:pwd@%s:%d", sA.opts.Host, sA.opts.Port)
|
|
}
|
|
c := natsConnect(t, url, nats.UserCredentials(creds))
|
|
defer c.Close()
|
|
sA.mu.Lock()
|
|
defer sA.mu.Unlock()
|
|
if len(sA.clients) != 1 {
|
|
t.Fatalf("Expected exactly one client")
|
|
}
|
|
for _, v := range sA.clients {
|
|
if v.opts.JWT != "" {
|
|
t.Fatalf("Expected no jwt %v", v.opts.JWT)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJWTJetStreamLimits(t *testing.T) {
|
|
updateJwt := func(url string, creds string, pubKey string, jwt string) {
|
|
t.Helper()
|
|
c := natsConnect(t, url, nats.UserCredentials(creds))
|
|
defer c.Close()
|
|
if msg, err := c.Request(fmt.Sprintf(accUpdateEventSubjNew, pubKey), []byte(jwt), time.Second); err != nil {
|
|
t.Fatal("error not expected in this test", err)
|
|
} else {
|
|
content := make(map[string]interface{})
|
|
if err := json.Unmarshal(msg.Data, &content); err != nil {
|
|
t.Fatalf("%v", err)
|
|
} else if _, ok := content["data"]; !ok {
|
|
t.Fatalf("did not get an ok response got: %v", content)
|
|
}
|
|
}
|
|
}
|
|
require_IdenticalLimits := func(infoLim JetStreamAccountLimits, lim jwt.JetStreamLimits) {
|
|
t.Helper()
|
|
if int64(infoLim.MaxConsumers) != lim.Consumer || int64(infoLim.MaxStreams) != lim.Streams ||
|
|
infoLim.MaxMemory != lim.MemoryStorage || infoLim.MaxStore != lim.DiskStorage {
|
|
t.Fatalf("limits do not match %v != %v", infoLim, lim)
|
|
}
|
|
}
|
|
expect_JSDisabledForAccount := func(c *nats.Conn) {
|
|
t.Helper()
|
|
if _, err := c.Request("$JS.API.INFO", nil, time.Second); err != nats.ErrTimeout && err != nats.ErrNoResponders {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
}
|
|
expect_InfoError := func(c *nats.Conn) {
|
|
t.Helper()
|
|
var info JSApiAccountInfoResponse
|
|
if resp, err := c.Request("$JS.API.INFO", nil, time.Second); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
} else if err = json.Unmarshal(resp.Data, &info); err != nil {
|
|
t.Fatalf("response1 %v got error %v", string(resp.Data), err)
|
|
} else if info.Error == nil {
|
|
t.Fatalf("expected error")
|
|
}
|
|
}
|
|
validate_limits := func(c *nats.Conn, expectedLimits jwt.JetStreamLimits) {
|
|
t.Helper()
|
|
var info JSApiAccountInfoResponse
|
|
if resp, err := c.Request("$JS.API.INFO", nil, time.Second); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
} else if err = json.Unmarshal(resp.Data, &info); err != nil {
|
|
t.Fatalf("response1 %v got error %v", string(resp.Data), err)
|
|
} else {
|
|
require_IdenticalLimits(info.Limits, expectedLimits)
|
|
}
|
|
}
|
|
// create system account
|
|
sysKp, _ := nkeys.CreateAccount()
|
|
sysPub, _ := sysKp.PublicKey()
|
|
sysUKp, _ := nkeys.CreateUser()
|
|
sysUSeed, _ := sysUKp.Seed()
|
|
uclaim := newJWTTestUserClaims()
|
|
uclaim.Subject, _ = sysUKp.PublicKey()
|
|
sysUserJwt, err := uclaim.Encode(sysKp)
|
|
require_NoError(t, err)
|
|
sysKp.Seed()
|
|
sysCreds := genCredsFile(t, sysUserJwt, sysUSeed)
|
|
// limits to apply and check
|
|
limits1 := jwt.JetStreamLimits{MemoryStorage: 1024 * 1024, DiskStorage: 2048 * 1024, Streams: 1, Consumer: 2}
|
|
// has valid limits that would fail when incorrectly applied twice
|
|
limits2 := jwt.JetStreamLimits{MemoryStorage: 4096 * 1024, DiskStorage: 8192 * 1024, Streams: 3, Consumer: 4}
|
|
// limits exceeding actual configured value of DiskStorage
|
|
limitsExceeded := jwt.JetStreamLimits{MemoryStorage: 8192 * 1024, DiskStorage: 16384 * 1024, Streams: 5, Consumer: 6}
|
|
// create account using jetstream with both limits
|
|
akp, _ := nkeys.CreateAccount()
|
|
aPub, _ := akp.PublicKey()
|
|
claim := jwt.NewAccountClaims(aPub)
|
|
claim.Limits.JetStreamLimits = limits1
|
|
aJwt1, err := claim.Encode(oKp)
|
|
require_NoError(t, err)
|
|
claim.Limits.JetStreamLimits = limits2
|
|
aJwt2, err := claim.Encode(oKp)
|
|
require_NoError(t, err)
|
|
claim.Limits.JetStreamLimits = limitsExceeded
|
|
aJwtLimitsExceeded, err := claim.Encode(oKp)
|
|
require_NoError(t, err)
|
|
claim.Limits.JetStreamLimits = jwt.JetStreamLimits{} // disabled
|
|
aJwt4, err := claim.Encode(oKp)
|
|
require_NoError(t, err)
|
|
// account user
|
|
uKp, _ := nkeys.CreateUser()
|
|
uSeed, _ := uKp.Seed()
|
|
uclaim = newJWTTestUserClaims()
|
|
uclaim.Subject, _ = uKp.PublicKey()
|
|
userJwt, err := uclaim.Encode(akp)
|
|
require_NoError(t, err)
|
|
userCreds := genCredsFile(t, userJwt, uSeed)
|
|
dir := createDir(t, "srv")
|
|
defer removeDir(t, dir)
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
jetstream: {max_mem_store: 10Mb, max_file_store: 10Mb}
|
|
operator: %s
|
|
resolver: {
|
|
type: full
|
|
dir: %s
|
|
}
|
|
system_account: %s
|
|
`, ojwt, dir, sysPub)))
|
|
defer removeFile(t, conf)
|
|
s, opts := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
port := opts.Port
|
|
updateJwt(s.ClientURL(), sysCreds, aPub, aJwt1)
|
|
c := natsConnect(t, s.ClientURL(), nats.UserCredentials(userCreds), nats.ReconnectWait(200*time.Millisecond))
|
|
defer c.Close()
|
|
validate_limits(c, limits1)
|
|
// keep using the same connection
|
|
updateJwt(s.ClientURL(), sysCreds, aPub, aJwt2)
|
|
validate_limits(c, limits2)
|
|
// keep using the same connection but do NOT CHANGE anything.
|
|
// This tests if the jwt is applied a second time (would fail)
|
|
updateJwt(s.ClientURL(), sysCreds, aPub, aJwt2)
|
|
validate_limits(c, limits2)
|
|
// keep using the same connection. This update EXCEEDS LIMITS
|
|
updateJwt(s.ClientURL(), sysCreds, aPub, aJwtLimitsExceeded)
|
|
validate_limits(c, limits2)
|
|
// disable test after failure
|
|
updateJwt(s.ClientURL(), sysCreds, aPub, aJwt4)
|
|
expect_InfoError(c)
|
|
// re enable, again testing with a value that can't be applied twice
|
|
updateJwt(s.ClientURL(), sysCreds, aPub, aJwt2)
|
|
validate_limits(c, limits2)
|
|
// disable test no prior failure
|
|
updateJwt(s.ClientURL(), sysCreds, aPub, aJwt4)
|
|
expect_InfoError(c)
|
|
// Wrong limits form start
|
|
updateJwt(s.ClientURL(), sysCreds, aPub, aJwtLimitsExceeded)
|
|
expect_JSDisabledForAccount(c)
|
|
// enable js but exceed limits. Followed by fix via restart
|
|
updateJwt(s.ClientURL(), sysCreds, aPub, aJwt2)
|
|
validate_limits(c, limits2)
|
|
updateJwt(s.ClientURL(), sysCreds, aPub, aJwtLimitsExceeded)
|
|
validate_limits(c, limits2)
|
|
s.Shutdown()
|
|
conf = createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: %d
|
|
jetstream: {max_mem_store: 20Mb, max_file_store: 20Mb}
|
|
operator: %s
|
|
resolver: {
|
|
type: full
|
|
dir: %s
|
|
}
|
|
system_account: %s
|
|
`, port, ojwt, dir, sysPub)))
|
|
defer removeFile(t, conf)
|
|
s, _ = RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
c.Flush() // force client to discover the disconnect
|
|
checkClientsCount(t, s, 1)
|
|
validate_limits(c, limitsExceeded)
|
|
s.Shutdown()
|
|
// disable jetstream test
|
|
conf = createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: %d
|
|
operator: %s
|
|
resolver: {
|
|
type: full
|
|
dir: %s
|
|
}
|
|
system_account: %s
|
|
`, port, ojwt, dir, sysPub)))
|
|
defer removeFile(t, conf)
|
|
s, _ = RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
c.Flush() // force client to discover the disconnect
|
|
checkClientsCount(t, s, 1)
|
|
expect_JSDisabledForAccount(c)
|
|
// test that it stays disabled
|
|
updateJwt(s.ClientURL(), sysCreds, aPub, aJwt2)
|
|
expect_JSDisabledForAccount(c)
|
|
c.Close()
|
|
}
|
|
|
|
func TestJWTUserRevocation(t *testing.T) {
|
|
createAccountAndUser := func(done chan struct{}, pubKey, jwt1, jwt2, creds1, creds2 *string) {
|
|
t.Helper()
|
|
kp, _ := nkeys.CreateAccount()
|
|
*pubKey, _ = kp.PublicKey()
|
|
claim := jwt.NewAccountClaims(*pubKey)
|
|
var err error
|
|
*jwt1, err = claim.Encode(oKp)
|
|
require_NoError(t, err)
|
|
|
|
ukp, _ := nkeys.CreateUser()
|
|
seed, _ := ukp.Seed()
|
|
upub, _ := ukp.PublicKey()
|
|
uclaim := newJWTTestUserClaims()
|
|
uclaim.Subject = upub
|
|
|
|
ujwt1, err := uclaim.Encode(kp)
|
|
require_NoError(t, err)
|
|
*creds1 = genCredsFile(t, ujwt1, seed)
|
|
|
|
// create updated claim need to assure that issue time differs
|
|
claim.Revoke(upub) // revokes all jwt from now on
|
|
time.Sleep(time.Millisecond * 1100)
|
|
*jwt2, err = claim.Encode(oKp)
|
|
require_NoError(t, err)
|
|
|
|
ujwt2, err := uclaim.Encode(kp)
|
|
require_NoError(t, err)
|
|
*creds2 = genCredsFile(t, ujwt2, seed)
|
|
|
|
done <- struct{}{}
|
|
}
|
|
// Create Accounts and corresponding revoked and non revoked user creds. Do so concurrently to speed up the test
|
|
doneChan := make(chan struct{}, 2)
|
|
defer close(doneChan)
|
|
var syspub, sysjwt, dummy1, sysCreds, dummyCreds string
|
|
go createAccountAndUser(doneChan, &syspub, &sysjwt, &dummy1, &sysCreds, &dummyCreds)
|
|
var apub, ajwt1, ajwt2, aCreds1, aCreds2 string
|
|
go createAccountAndUser(doneChan, &apub, &ajwt1, &ajwt2, &aCreds1, &aCreds2)
|
|
for i := 0; i < cap(doneChan); i++ {
|
|
<-doneChan
|
|
}
|
|
defer removeFile(t, sysCreds)
|
|
defer removeFile(t, dummyCreds)
|
|
defer removeFile(t, aCreds1)
|
|
defer removeFile(t, aCreds2)
|
|
dirSrv := createDir(t, "srv")
|
|
defer removeDir(t, dirSrv)
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
operator: %s
|
|
system_account: %s
|
|
resolver: {
|
|
type: full
|
|
dir: %s
|
|
}
|
|
`, ojwt, syspub, dirSrv)))
|
|
defer removeFile(t, conf)
|
|
srv, _ := RunServerWithConfig(conf)
|
|
defer srv.Shutdown()
|
|
updateJwt(t, srv.ClientURL(), sysCreds, sysjwt, 1) // update system account jwt
|
|
updateJwt(t, srv.ClientURL(), sysCreds, ajwt1, 1) // set account jwt without revocation
|
|
ncSys := natsConnect(t, srv.ClientURL(), nats.UserCredentials(sysCreds), nats.Name("conn name"))
|
|
defer ncSys.Close()
|
|
ncChan := make(chan *nats.Msg, 10)
|
|
defer close(ncChan)
|
|
sub, _ := ncSys.ChanSubscribe(fmt.Sprintf(disconnectEventSubj, apub), ncChan) // observe disconnect message
|
|
defer sub.Unsubscribe()
|
|
// use credentials that will be revoked ans assure that the connection will be disconnected
|
|
nc := natsConnect(t, srv.ClientURL(), nats.UserCredentials(aCreds1),
|
|
nats.ErrorHandler(func(_ *nats.Conn, _ *nats.Subscription, err error) {
|
|
if err != nil && strings.Contains(err.Error(), "authentication revoked") {
|
|
doneChan <- struct{}{}
|
|
}
|
|
}),
|
|
)
|
|
defer nc.Close()
|
|
// update account jwt to contain revocation
|
|
if updateJwt(t, srv.ClientURL(), sysCreds, ajwt2, 1) != 1 {
|
|
t.Fatalf("Expected jwt update to pass")
|
|
}
|
|
// assure that nc got disconnected due to the revocation
|
|
select {
|
|
case <-doneChan:
|
|
case <-time.After(time.Second):
|
|
t.Fatalf("Expected connection to have failed")
|
|
}
|
|
m := <-ncChan
|
|
require_Len(t, strings.Count(string(m.Data), apub), 2)
|
|
require_True(t, strings.Contains(string(m.Data), `"jwt":"eyJ0`))
|
|
// try again with old credentials. Expected to fail
|
|
if nc1, err := nats.Connect(srv.ClientURL(), nats.UserCredentials(aCreds1)); err == nil {
|
|
nc1.Close()
|
|
t.Fatalf("Expected revoked credentials to fail")
|
|
}
|
|
// Assure new creds pass
|
|
nc2 := natsConnect(t, srv.ClientURL(), nats.UserCredentials(aCreds2))
|
|
defer nc2.Close()
|
|
}
|
|
|
|
func TestJWTAccountFetchTimeout(t *testing.T) {
|
|
createAccountAndUser := func(pubKey, jwt1, creds1 *string) {
|
|
t.Helper()
|
|
kp, _ := nkeys.CreateAccount()
|
|
*pubKey, _ = kp.PublicKey()
|
|
claim := jwt.NewAccountClaims(*pubKey)
|
|
var err error
|
|
*jwt1, err = claim.Encode(oKp)
|
|
require_NoError(t, err)
|
|
ukp, _ := nkeys.CreateUser()
|
|
seed, _ := ukp.Seed()
|
|
upub, _ := ukp.PublicKey()
|
|
uclaim := newJWTTestUserClaims()
|
|
uclaim.Subject = upub
|
|
ujwt1, err := uclaim.Encode(kp)
|
|
require_NoError(t, err)
|
|
*creds1 = genCredsFile(t, ujwt1, seed)
|
|
}
|
|
for _, cfg := range []string{
|
|
`type: full`,
|
|
`type: cache`,
|
|
} {
|
|
t.Run("", func(t *testing.T) {
|
|
var syspub, sysjwt, sysCreds string
|
|
createAccountAndUser(&syspub, &sysjwt, &sysCreds)
|
|
defer removeFile(t, sysCreds)
|
|
var apub, ajwt1, aCreds1 string
|
|
createAccountAndUser(&apub, &ajwt1, &aCreds1)
|
|
defer removeFile(t, aCreds1)
|
|
dirSrv := createDir(t, "srv")
|
|
defer removeDir(t, dirSrv)
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
operator: %s
|
|
system_account: %s
|
|
resolver: {
|
|
%s
|
|
timeout: "100ms"
|
|
dir: %s
|
|
}
|
|
`, ojwt, syspub, cfg, dirSrv)))
|
|
defer removeFile(t, conf)
|
|
srv, _ := RunServerWithConfig(conf)
|
|
defer srv.Shutdown()
|
|
updateJwt(t, srv.ClientURL(), sysCreds, sysjwt, 1) // update system account jwt
|
|
start := time.Now()
|
|
nc, err := nats.Connect(srv.ClientURL(), nats.UserCredentials(aCreds1))
|
|
if err == nil {
|
|
t.Fatal("expected an error, got none")
|
|
} else if !strings.Contains(err.Error(), "Authorization Violation") {
|
|
t.Fatalf("expected an authorization violation, got: %v", err)
|
|
}
|
|
if time.Since(start) > 300*time.Millisecond {
|
|
t.Fatal("expected timeout earlier")
|
|
}
|
|
defer nc.Close()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJWTAccountOps(t *testing.T) {
|
|
op, _ := nkeys.CreateOperator()
|
|
opPk, _ := op.PublicKey()
|
|
sk, _ := nkeys.CreateOperator()
|
|
skPk, _ := sk.PublicKey()
|
|
opClaim := jwt.NewOperatorClaims(opPk)
|
|
opClaim.SigningKeys.Add(skPk)
|
|
opJwt, err := opClaim.Encode(op)
|
|
require_NoError(t, err)
|
|
createAccountAndUser := func(pubKey, jwt1, creds1 *string) {
|
|
t.Helper()
|
|
kp, _ := nkeys.CreateAccount()
|
|
*pubKey, _ = kp.PublicKey()
|
|
claim := jwt.NewAccountClaims(*pubKey)
|
|
var err error
|
|
*jwt1, err = claim.Encode(sk)
|
|
require_NoError(t, err)
|
|
|
|
ukp, _ := nkeys.CreateUser()
|
|
seed, _ := ukp.Seed()
|
|
upub, _ := ukp.PublicKey()
|
|
uclaim := newJWTTestUserClaims()
|
|
uclaim.Subject = upub
|
|
|
|
ujwt1, err := uclaim.Encode(kp)
|
|
require_NoError(t, err)
|
|
*creds1 = genCredsFile(t, ujwt1, seed)
|
|
}
|
|
generateRequest := func(accs []string, kp nkeys.KeyPair) []byte {
|
|
t.Helper()
|
|
opk, _ := kp.PublicKey()
|
|
c := jwt.NewGenericClaims(opk)
|
|
c.Data["accounts"] = accs
|
|
cJwt, err := c.Encode(kp)
|
|
if err != nil {
|
|
t.Fatalf("Expected no error %v", err)
|
|
}
|
|
return []byte(cJwt)
|
|
}
|
|
for _, cfg := range []string{
|
|
`type: full
|
|
allow_delete: true`,
|
|
`type: cache`,
|
|
} {
|
|
t.Run("", func(t *testing.T) {
|
|
var syspub, sysjwt, sysCreds string
|
|
createAccountAndUser(&syspub, &sysjwt, &sysCreds)
|
|
defer removeFile(t, sysCreds)
|
|
var apub, ajwt1, aCreds1 string
|
|
createAccountAndUser(&apub, &ajwt1, &aCreds1)
|
|
defer removeFile(t, aCreds1)
|
|
dirSrv := createDir(t, "srv")
|
|
defer removeDir(t, dirSrv)
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
operator: %s
|
|
system_account: %s
|
|
resolver: {
|
|
%s
|
|
dir: %s
|
|
}
|
|
`, opJwt, syspub, cfg, dirSrv)))
|
|
disconnectErrChan := make(chan struct{}, 1)
|
|
defer close(disconnectErrChan)
|
|
defer removeFile(t, conf)
|
|
srv, _ := RunServerWithConfig(conf)
|
|
defer srv.Shutdown()
|
|
updateJwt(t, srv.ClientURL(), sysCreds, sysjwt, 1) // update system account jwt
|
|
// push jwt (for full resolver)
|
|
updateJwt(t, srv.ClientURL(), sysCreds, ajwt1, 1) // set jwt
|
|
nc := natsConnect(t, srv.ClientURL(), nats.UserCredentials(sysCreds))
|
|
defer nc.Close()
|
|
// simulate nas resolver in case of a lookup request (cache)
|
|
nc.Subscribe(fmt.Sprintf(accLookupReqSubj, apub), func(msg *nats.Msg) {
|
|
msg.Respond([]byte(ajwt1))
|
|
})
|
|
// connect so there is a reason to cache the request and so disconnect can be observed
|
|
ncA := natsConnect(t, srv.ClientURL(), nats.UserCredentials(aCreds1), nats.NoReconnect(),
|
|
nats.ErrorHandler(func(_ *nats.Conn, _ *nats.Subscription, err error) {
|
|
if err != nil && strings.Contains(err.Error(), "account authentication expired") {
|
|
disconnectErrChan <- struct{}{}
|
|
}
|
|
}))
|
|
defer ncA.Close()
|
|
resp, err := nc.Request(accListReqSubj, nil, time.Second)
|
|
require_NoError(t, err)
|
|
require_True(t, strings.Contains(string(resp.Data), apub))
|
|
require_True(t, strings.Contains(string(resp.Data), syspub))
|
|
// delete nothing
|
|
resp, err = nc.Request(accDeleteReqSubj, generateRequest([]string{}, op), time.Second)
|
|
require_NoError(t, err)
|
|
require_True(t, strings.Contains(string(resp.Data), `"message":"deleted 0 accounts"`))
|
|
// issue delete, twice to also delete a non existing account
|
|
// also switch which key used to sign the request
|
|
for i := 0; i < 2; i++ {
|
|
resp, err = nc.Request(accDeleteReqSubj, generateRequest([]string{apub}, sk), time.Second)
|
|
require_NoError(t, err)
|
|
require_True(t, strings.Contains(string(resp.Data), `"message":"deleted 1 accounts"`))
|
|
resp, err = nc.Request(accListReqSubj, nil, time.Second)
|
|
require_False(t, strings.Contains(string(resp.Data), apub))
|
|
require_True(t, strings.Contains(string(resp.Data), syspub))
|
|
require_NoError(t, err)
|
|
if i > 0 {
|
|
continue
|
|
}
|
|
select {
|
|
case <-disconnectErrChan:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Callback not executed")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func createKey(t *testing.T) (nkeys.KeyPair, string) {
|
|
t.Helper()
|
|
kp, _ := nkeys.CreateAccount()
|
|
syspub, _ := kp.PublicKey()
|
|
return kp, syspub
|
|
}
|
|
|
|
func encodeClaim(t *testing.T, claim *jwt.AccountClaims, pub string) string {
|
|
t.Helper()
|
|
theJWT, err := claim.Encode(oKp)
|
|
require_NoError(t, err)
|
|
return theJWT
|
|
}
|
|
|
|
// returns user creds
|
|
func newUserEx(t *testing.T, accKp nkeys.KeyPair, scoped bool, issuerAccount string) string {
|
|
ukp, _ := nkeys.CreateUser()
|
|
seed, _ := ukp.Seed()
|
|
upub, _ := ukp.PublicKey()
|
|
uclaim := newJWTTestUserClaims()
|
|
uclaim.Subject = upub
|
|
uclaim.SetScoped(scoped)
|
|
if issuerAccount != _EMPTY_ {
|
|
uclaim.IssuerAccount = issuerAccount
|
|
}
|
|
ujwt, err := uclaim.Encode(accKp)
|
|
require_NoError(t, err)
|
|
return genCredsFile(t, ujwt, seed)
|
|
}
|
|
|
|
// returns user creds
|
|
func newUser(t *testing.T, accKp nkeys.KeyPair) string {
|
|
return newUserEx(t, accKp, false, "")
|
|
}
|
|
|
|
func TestJWTHeader(t *testing.T) {
|
|
sysKp, syspub := createKey(t)
|
|
sysJwt := encodeClaim(t, jwt.NewAccountClaims(syspub), syspub)
|
|
sysCreds := newUser(t, sysKp)
|
|
defer removeFile(t, sysCreds)
|
|
|
|
test := func(share bool) {
|
|
aExpKp, aExpPub := createKey(t)
|
|
aExpClaim := jwt.NewAccountClaims(aExpPub)
|
|
aExpClaim.Exports.Add(&jwt.Export{
|
|
Name: "test",
|
|
Subject: "srvc",
|
|
Type: jwt.Service,
|
|
TokenReq: false,
|
|
Latency: &jwt.ServiceLatency{
|
|
Sampling: jwt.Headers,
|
|
Results: "res",
|
|
},
|
|
})
|
|
aExpJwt := encodeClaim(t, aExpClaim, aExpPub)
|
|
aExpCreds := newUser(t, aExpKp)
|
|
defer removeFile(t, aExpCreds)
|
|
|
|
aImpKp, aImpPub := createKey(t)
|
|
aImpClaim := jwt.NewAccountClaims(aImpPub)
|
|
aImpClaim.Imports.Add(&jwt.Import{
|
|
Name: "test",
|
|
Subject: "srvc",
|
|
Account: aExpPub,
|
|
Type: jwt.Service,
|
|
Share: share,
|
|
})
|
|
aImpJwt := encodeClaim(t, aImpClaim, aImpPub)
|
|
aImpCreds := newUser(t, aImpKp)
|
|
defer removeFile(t, aImpCreds)
|
|
|
|
dirSrv := createDir(t, "srv")
|
|
defer removeDir(t, dirSrv)
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
operator: %s
|
|
system_account: %s
|
|
resolver: {
|
|
type: full
|
|
dir: %s
|
|
}
|
|
`, ojwt, syspub, dirSrv)))
|
|
defer removeFile(t, conf)
|
|
srv, _ := RunServerWithConfig(conf)
|
|
defer srv.Shutdown()
|
|
updateJwt(t, srv.ClientURL(), sysCreds, sysJwt, 1) // update system account jwt
|
|
updateJwt(t, srv.ClientURL(), sysCreds, aExpJwt, 1)
|
|
updateJwt(t, srv.ClientURL(), sysCreds, aImpJwt, 1)
|
|
|
|
expNc := natsConnect(t, srv.ClientURL(), nats.UserCredentials(aExpCreds))
|
|
defer expNc.Close()
|
|
resChan := make(chan *nats.Msg, 1)
|
|
expNc.ChanSubscribe("res", resChan)
|
|
sub, err := expNc.Subscribe("srvc", func(msg *nats.Msg) {
|
|
msg.Respond(nil)
|
|
})
|
|
require_NoError(t, err)
|
|
defer sub.Unsubscribe()
|
|
|
|
impNc := natsConnect(t, srv.ClientURL(), nats.UserCredentials(aImpCreds))
|
|
defer impNc.Close()
|
|
// send request w/o header
|
|
_, err = impNc.Request("srvc", []byte("msg1"), time.Second)
|
|
require_NoError(t, err)
|
|
require_True(t, len(resChan) == 0)
|
|
|
|
_, err = impNc.RequestMsg(&nats.Msg{
|
|
Subject: "srvc", Data: []byte("msg2"), Header: http.Header{
|
|
"X-B3-Sampled": []string{"1"},
|
|
"Share": []string{"Me"}}}, time.Second)
|
|
require_NoError(t, err)
|
|
select {
|
|
case <-time.After(time.Second):
|
|
t.Fatalf("should have received a response")
|
|
case m := <-resChan:
|
|
obj := map[string]interface{}{}
|
|
err = json.Unmarshal(m.Data, &obj)
|
|
require_NoError(t, err)
|
|
// test if shared is honored
|
|
reqInfo := obj["requestor"].(map[string]interface{})
|
|
// fields always set
|
|
require_True(t, reqInfo["acc"] != nil)
|
|
require_True(t, reqInfo["rtt"] != nil)
|
|
|
|
// fields only set when shared
|
|
_, ok1 := reqInfo["lang"]
|
|
_, ok2 := reqInfo["ver"]
|
|
_, ok3 := reqInfo["host"]
|
|
_, ok4 := reqInfo["start"]
|
|
if !share {
|
|
ok1 = !ok1
|
|
ok2 = !ok2
|
|
ok3 = !ok3
|
|
ok4 = !ok4
|
|
}
|
|
require_True(t, ok1)
|
|
require_True(t, ok2)
|
|
require_True(t, ok3)
|
|
require_True(t, ok4)
|
|
|
|
}
|
|
require_True(t, len(resChan) == 0)
|
|
}
|
|
test(true)
|
|
test(false)
|
|
}
|
|
|
|
func TestJWTAccountImportsWithWildcardSupport(t *testing.T) {
|
|
test := func(aExpPub, aExpJwt, aExpCreds, aImpPub, aImpJwt, aImpCreds string, jsEnabled bool, exSubExpect, exPub, imReq, imSubExpect string) {
|
|
t.Helper()
|
|
|
|
var jsSetting string
|
|
if jsEnabled {
|
|
jsSetting = "jetstream: {max_mem_store: 10Mb, max_file_store: 10Mb}"
|
|
}
|
|
|
|
_, aSysPub := createKey(t)
|
|
aSysClaim := jwt.NewAccountClaims(aSysPub)
|
|
aSysJwt := encodeClaim(t, aSysClaim, aSysPub)
|
|
cf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
operator = %s
|
|
resolver = MEMORY
|
|
resolver_preload = {
|
|
%s : "%s"
|
|
%s : "%s"
|
|
%s : "%s"
|
|
}
|
|
system_account: %s
|
|
%s
|
|
`, ojwt, aExpPub, aExpJwt, aImpPub, aImpJwt, aSysPub, aSysJwt, aSysPub, jsSetting)))
|
|
defer removeFile(t, cf)
|
|
|
|
s, opts := RunServerWithConfig(cf)
|
|
defer s.Shutdown()
|
|
|
|
ncExp := natsConnect(t, fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port), nats.UserCredentials(aExpCreds))
|
|
defer ncExp.Close()
|
|
|
|
ncImp := natsConnect(t, fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port), nats.UserCredentials(aImpCreds))
|
|
defer ncImp.Close()
|
|
|
|
// Create subscriber for the service endpoint in foo.
|
|
_, err := ncExp.Subscribe(exSubExpect, func(m *nats.Msg) {
|
|
m.Respond([]byte("yes!"))
|
|
})
|
|
require_NoError(t, err)
|
|
ncExp.Flush()
|
|
|
|
// Now test service import.
|
|
if resp, err := ncImp.Request(imReq, []byte("yes?"), time.Second); err != nil {
|
|
t.Fatalf("Expected a response to request %s got: %v", imReq, err)
|
|
} else if string(resp.Data) != "yes!" {
|
|
t.Fatalf("Expected a response of %q, got %q", "yes!", resp.Data)
|
|
}
|
|
subBar, err := ncImp.SubscribeSync(imSubExpect)
|
|
require_NoError(t, err)
|
|
ncImp.Flush()
|
|
|
|
ncExp.Publish(exPub, []byte("event!"))
|
|
|
|
if m, err := subBar.NextMsg(time.Second); err != nil {
|
|
t.Fatalf("Expected a stream message got %v", err)
|
|
} else if string(m.Data) != "event!" {
|
|
t.Fatalf("Expected a response of %q, got %q", "event!", m.Data)
|
|
}
|
|
}
|
|
createExporter := func() (string, string, string) {
|
|
t.Helper()
|
|
aExpKp, aExpPub := createKey(t)
|
|
aExpClaim := jwt.NewAccountClaims(aExpPub)
|
|
aExpClaim.Name = "Export"
|
|
aExpClaim.Exports.Add(&jwt.Export{
|
|
Subject: "$request.*.$in.*.>",
|
|
Type: jwt.Service,
|
|
}, &jwt.Export{
|
|
Subject: "$events.*.$in.*.>",
|
|
Type: jwt.Stream,
|
|
})
|
|
aExpJwt := encodeClaim(t, aExpClaim, aExpPub)
|
|
aExpCreds := newUser(t, aExpKp)
|
|
return aExpPub, aExpJwt, aExpCreds
|
|
}
|
|
t.Run("To", func(t *testing.T) {
|
|
aExpPub, aExpJwt, aExpCreds := createExporter()
|
|
defer removeFile(t, aExpCreds)
|
|
aImpKp, aImpPub := createKey(t)
|
|
aImpClaim := jwt.NewAccountClaims(aImpPub)
|
|
aImpClaim.Name = "Import"
|
|
aImpClaim.Imports.Add(&jwt.Import{
|
|
Subject: "my.request.*.*.>",
|
|
Type: jwt.Service,
|
|
To: "$request.*.$in.*.>", // services have local and remote switched between Subject and To
|
|
Account: aExpPub,
|
|
}, &jwt.Import{
|
|
Subject: "$events.*.$in.*.>",
|
|
Type: jwt.Stream,
|
|
To: "prefix",
|
|
Account: aExpPub,
|
|
})
|
|
aImpJwt := encodeClaim(t, aImpClaim, aImpPub)
|
|
aImpCreds := newUser(t, aImpKp)
|
|
defer removeFile(t, aImpCreds)
|
|
test(aExpPub, aExpJwt, aExpCreds, aImpPub, aImpJwt, aImpCreds, false,
|
|
"$request.1.$in.2.bar", "$events.1.$in.2.bar",
|
|
"my.request.1.2.bar", "prefix.$events.1.$in.2.bar")
|
|
})
|
|
t.Run("LocalSubject-No-Reorder", func(t *testing.T) {
|
|
aExpPub, aExpJwt, aExpCreds := createExporter()
|
|
defer removeFile(t, aExpCreds)
|
|
aImpKp, aImpPub := createKey(t)
|
|
aImpClaim := jwt.NewAccountClaims(aImpPub)
|
|
aImpClaim.Name = "Import"
|
|
aImpClaim.Imports.Add(&jwt.Import{
|
|
Subject: "$request.*.$in.*.>",
|
|
Type: jwt.Service,
|
|
LocalSubject: "my.request.*.*.>",
|
|
Account: aExpPub,
|
|
}, &jwt.Import{
|
|
Subject: "$events.*.$in.*.>",
|
|
Type: jwt.Stream,
|
|
LocalSubject: "my.events.*.*.>",
|
|
Account: aExpPub,
|
|
})
|
|
aImpJwt := encodeClaim(t, aImpClaim, aImpPub)
|
|
aImpCreds := newUser(t, aImpKp)
|
|
defer removeFile(t, aImpCreds)
|
|
test(aExpPub, aExpJwt, aExpCreds, aImpPub, aImpJwt, aImpCreds, false,
|
|
"$request.1.$in.2.bar", "$events.1.$in.2.bar",
|
|
"my.request.1.2.bar", "my.events.1.2.bar")
|
|
})
|
|
t.Run("LocalSubject-Reorder", func(t *testing.T) {
|
|
for _, jsEnabled := range []bool{false, true} {
|
|
t.Run(fmt.Sprintf("%t", jsEnabled), func(t *testing.T) {
|
|
aExpPub, aExpJwt, aExpCreds := createExporter()
|
|
defer removeFile(t, aExpCreds)
|
|
aImpKp, aImpPub := createKey(t)
|
|
aImpClaim := jwt.NewAccountClaims(aImpPub)
|
|
aImpClaim.Name = "Import"
|
|
aImpClaim.Imports.Add(&jwt.Import{
|
|
Subject: "$request.*.$in.*.>",
|
|
Type: jwt.Service,
|
|
LocalSubject: "my.request.$2.$1.>",
|
|
Account: aExpPub,
|
|
}, &jwt.Import{
|
|
Subject: "$events.*.$in.*.>",
|
|
Type: jwt.Stream,
|
|
LocalSubject: "my.events.$2.$1.>",
|
|
Account: aExpPub,
|
|
})
|
|
aImpJwt := encodeClaim(t, aImpClaim, aImpPub)
|
|
aImpCreds := newUser(t, aImpKp)
|
|
defer removeFile(t, aImpCreds)
|
|
test(aExpPub, aExpJwt, aExpCreds, aImpPub, aImpJwt, aImpCreds, jsEnabled,
|
|
"$request.2.$in.1.bar", "$events.1.$in.2.bar",
|
|
"my.request.1.2.bar", "my.events.2.1.bar")
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestJWTAccountTokenImportMisuse(t *testing.T) {
|
|
sysKp, syspub := createKey(t)
|
|
sysJwt := encodeClaim(t, jwt.NewAccountClaims(syspub), syspub)
|
|
sysCreds := newUser(t, sysKp)
|
|
defer removeFile(t, sysCreds)
|
|
|
|
aExpKp, aExpPub := createKey(t)
|
|
aExpClaim := jwt.NewAccountClaims(aExpPub)
|
|
aExpClaim.Name = "Export"
|
|
aExpClaim.Exports.Add(&jwt.Export{
|
|
Subject: "$events.*.$in.*.>",
|
|
Type: jwt.Stream,
|
|
TokenReq: true,
|
|
}, &jwt.Export{
|
|
Subject: "foo",
|
|
Type: jwt.Stream,
|
|
TokenReq: true,
|
|
})
|
|
aExpJwt := encodeClaim(t, aExpClaim, aExpPub)
|
|
aExpCreds := newUser(t, aExpKp)
|
|
defer removeFile(t, aExpCreds)
|
|
|
|
createImportingAccountClaim := func(aImpKp nkeys.KeyPair, aExpPub string, ac *jwt.ActivationClaims) (string, string) {
|
|
t.Helper()
|
|
token, err := ac.Encode(aExpKp)
|
|
require_NoError(t, err)
|
|
|
|
aImpPub, err := aImpKp.PublicKey()
|
|
require_NoError(t, err)
|
|
aImpClaim := jwt.NewAccountClaims(aImpPub)
|
|
aImpClaim.Name = "Import"
|
|
aImpClaim.Imports.Add(&jwt.Import{
|
|
Subject: "$events.*.$in.*.>",
|
|
Type: jwt.Stream,
|
|
Account: aExpPub,
|
|
Token: token,
|
|
})
|
|
aImpJwt := encodeClaim(t, aImpClaim, aImpPub)
|
|
aImpCreds := newUser(t, aImpKp)
|
|
return aImpJwt, aImpCreds
|
|
}
|
|
|
|
testConnect := func(aExpPub, aExpJwt, aExpCreds, aImpPub, aImpJwt, aImpCreds string) {
|
|
t.Helper()
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/A/" {
|
|
// Server startup
|
|
w.Write(nil)
|
|
} else if r.URL.Path == "/A/"+aExpPub {
|
|
w.Write([]byte(aExpJwt))
|
|
} else if r.URL.Path == "/A/"+aImpPub {
|
|
w.Write([]byte(aImpJwt))
|
|
} else {
|
|
t.Fatal("not expected")
|
|
}
|
|
}))
|
|
defer ts.Close()
|
|
cf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
operator: %s
|
|
resolver: URL("%s/A/")
|
|
`, ojwt, ts.URL)))
|
|
defer removeFile(t, cf)
|
|
|
|
s, opts := RunServerWithConfig(cf)
|
|
defer s.Shutdown()
|
|
|
|
ncImp, err := nats.Connect(fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port), nats.UserCredentials(aImpCreds))
|
|
require_Error(t, err) // misuse needs to result in an error
|
|
defer ncImp.Close()
|
|
}
|
|
|
|
testNatsResolver := func(aImpJwt string) {
|
|
t.Helper()
|
|
dirSrv := createDir(t, "srv")
|
|
defer removeDir(t, dirSrv)
|
|
cf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
operator: %s
|
|
system_account: %s
|
|
resolver: {
|
|
type: full
|
|
dir: %s
|
|
}
|
|
`, ojwt, syspub, dirSrv)))
|
|
|
|
s, _ := RunServerWithConfig(cf)
|
|
defer s.Shutdown()
|
|
|
|
require_True(t, updateJwt(t, s.ClientURL(), sysCreds, sysJwt, 1) == 1)
|
|
require_True(t, updateJwt(t, s.ClientURL(), sysCreds, aExpJwt, 1) == 1)
|
|
require_True(t, updateJwt(t, s.ClientURL(), sysCreds, aImpJwt, 1) == 0) // assure this did not succeed
|
|
}
|
|
|
|
t.Run("wrong-account", func(t *testing.T) {
|
|
aImpKp, aImpPub := createKey(t)
|
|
ac := &jwt.ActivationClaims{}
|
|
_, ac.Subject = createKey(t) // on purpose issue this token for another account
|
|
ac.ImportSubject = "$events.*.$in.*.>"
|
|
ac.ImportType = jwt.Stream
|
|
|
|
aImpJwt, aImpCreds := createImportingAccountClaim(aImpKp, aExpPub, ac)
|
|
defer removeFile(t, aImpCreds)
|
|
testConnect(aExpPub, aExpJwt, aExpCreds, aImpPub, aImpJwt, aImpCreds)
|
|
testNatsResolver(aImpJwt)
|
|
})
|
|
|
|
t.Run("different-subject", func(t *testing.T) {
|
|
aImpKp, aImpPub := createKey(t)
|
|
ac := &jwt.ActivationClaims{}
|
|
ac.Subject = aImpPub
|
|
ac.ImportSubject = "foo" // on purpose use a subject from another export
|
|
ac.ImportType = jwt.Stream
|
|
|
|
aImpJwt, aImpCreds := createImportingAccountClaim(aImpKp, aExpPub, ac)
|
|
defer removeFile(t, aImpCreds)
|
|
testConnect(aExpPub, aExpJwt, aExpCreds, aImpPub, aImpJwt, aImpCreds)
|
|
testNatsResolver(aImpJwt)
|
|
})
|
|
|
|
t.Run("non-existing-subject", func(t *testing.T) {
|
|
aImpKp, aImpPub := createKey(t)
|
|
ac := &jwt.ActivationClaims{}
|
|
ac.Subject = aImpPub
|
|
ac.ImportSubject = "does-not-exist-or-from-different-export" // on purpose use a non exported subject
|
|
ac.ImportType = jwt.Stream
|
|
|
|
aImpJwt, aImpCreds := createImportingAccountClaim(aImpKp, aExpPub, ac)
|
|
defer removeFile(t, aImpCreds)
|
|
testConnect(aExpPub, aExpJwt, aExpCreds, aImpPub, aImpJwt, aImpCreds)
|
|
testNatsResolver(aImpJwt)
|
|
})
|
|
}
|
|
|
|
func TestJWTResponseThreshold(t *testing.T) {
|
|
respThresh := 20 * time.Millisecond
|
|
aExpKp, aExpPub := createKey(t)
|
|
aExpClaim := jwt.NewAccountClaims(aExpPub)
|
|
aExpClaim.Name = "Export"
|
|
aExpClaim.Exports.Add(&jwt.Export{
|
|
Subject: "srvc",
|
|
Type: jwt.Service,
|
|
ResponseThreshold: respThresh,
|
|
})
|
|
aExpJwt := encodeClaim(t, aExpClaim, aExpPub)
|
|
aExpCreds := newUser(t, aExpKp)
|
|
|
|
defer removeFile(t, aExpCreds)
|
|
aImpKp, aImpPub := createKey(t)
|
|
aImpClaim := jwt.NewAccountClaims(aImpPub)
|
|
aImpClaim.Name = "Import"
|
|
aImpClaim.Imports.Add(&jwt.Import{
|
|
Subject: "srvc",
|
|
Type: jwt.Service,
|
|
Account: aExpPub,
|
|
})
|
|
aImpJwt := encodeClaim(t, aImpClaim, aImpPub)
|
|
aImpCreds := newUser(t, aImpKp)
|
|
defer removeFile(t, aImpCreds)
|
|
|
|
cf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
operator = %s
|
|
resolver = MEMORY
|
|
resolver_preload = {
|
|
%s : "%s"
|
|
%s : "%s"
|
|
}
|
|
`, ojwt, aExpPub, aExpJwt, aImpPub, aImpJwt)))
|
|
defer removeFile(t, cf)
|
|
|
|
s, opts := RunServerWithConfig(cf)
|
|
defer s.Shutdown()
|
|
|
|
ncExp := natsConnect(t, fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port), nats.UserCredentials(aExpCreds))
|
|
defer ncExp.Close()
|
|
|
|
ncImp := natsConnect(t, fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port), nats.UserCredentials(aImpCreds))
|
|
defer ncImp.Close()
|
|
|
|
delayChan := make(chan time.Duration, 1)
|
|
|
|
// Create subscriber for the service endpoint in foo.
|
|
_, err := ncExp.Subscribe("srvc", func(m *nats.Msg) {
|
|
time.Sleep(<-delayChan)
|
|
m.Respond([]byte("yes!"))
|
|
})
|
|
require_NoError(t, err)
|
|
ncExp.Flush()
|
|
|
|
t.Run("No-Timeout", func(t *testing.T) {
|
|
delayChan <- respThresh / 2
|
|
if resp, err := ncImp.Request("srvc", []byte("yes?"), 4*respThresh); err != nil {
|
|
t.Fatalf("Expected a response to request srvc got: %v", err)
|
|
} else if string(resp.Data) != "yes!" {
|
|
t.Fatalf("Expected a response of %q, got %q", "yes!", resp.Data)
|
|
}
|
|
})
|
|
t.Run("Timeout", func(t *testing.T) {
|
|
delayChan <- 2 * respThresh
|
|
if _, err := ncImp.Request("srvc", []byte("yes?"), 4*respThresh); err == nil || err != nats.ErrTimeout {
|
|
t.Fatalf("Expected a timeout")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestJWTQueuePermissions(t *testing.T) {
|
|
aExpKp, aExpPub := createKey(t)
|
|
aExpClaim := jwt.NewAccountClaims(aExpPub)
|
|
aExpJwt := encodeClaim(t, aExpClaim, aExpPub)
|
|
newUser := func(t *testing.T, permType string) string {
|
|
ukp, _ := nkeys.CreateUser()
|
|
seed, _ := ukp.Seed()
|
|
upub, _ := ukp.PublicKey()
|
|
uclaim := newJWTTestUserClaims()
|
|
uclaim.Subject = upub
|
|
switch permType {
|
|
case "allow":
|
|
uclaim.Permissions.Sub.Allow.Add("foo.> *.dev")
|
|
case "deny":
|
|
uclaim.Permissions.Sub.Deny.Add("foo.> *.dev")
|
|
}
|
|
ujwt, err := uclaim.Encode(aExpKp)
|
|
require_NoError(t, err)
|
|
return genCredsFile(t, ujwt, seed)
|
|
}
|
|
confFileName := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
operator = %s
|
|
resolver = MEMORY
|
|
resolver_preload = {
|
|
%s : %s
|
|
}`, ojwt, aExpPub, aExpJwt)))
|
|
defer removeFile(t, confFileName)
|
|
opts, err := ProcessConfigFile(confFileName)
|
|
if err != nil {
|
|
t.Fatalf("Received unexpected error %s", err)
|
|
}
|
|
opts.NoLog, opts.NoSigs = true, true
|
|
errChan := make(chan error, 1)
|
|
defer close(errChan)
|
|
s := RunServer(opts)
|
|
defer s.Shutdown()
|
|
|
|
for _, test := range []struct {
|
|
permType string
|
|
queue string
|
|
errExpected bool
|
|
}{
|
|
{"allow", "queue.dev", false},
|
|
{"allow", "", true},
|
|
{"allow", "bad", true},
|
|
{"deny", "", false},
|
|
{"deny", "queue.dev", true},
|
|
} {
|
|
t.Run(test.permType+test.queue, func(t *testing.T) {
|
|
usrCreds := newUser(t, test.permType)
|
|
defer removeFile(t, usrCreds)
|
|
nc, err := nats.Connect(fmt.Sprintf("nats://127.0.0.1:%d", opts.Port),
|
|
nats.ErrorHandler(func(conn *nats.Conn, s *nats.Subscription, err error) {
|
|
errChan <- err
|
|
}),
|
|
nats.UserCredentials(usrCreds))
|
|
if err != nil {
|
|
t.Fatalf("No error expected: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
if test.queue == "" {
|
|
if _, err := nc.Subscribe("foo.bar", func(msg *nats.Msg) {}); err != nil {
|
|
t.Fatalf("no error expected: %v", err)
|
|
}
|
|
} else {
|
|
if _, err := nc.QueueSubscribe("foo.bar", test.queue, func(msg *nats.Msg) {}); err != nil {
|
|
t.Fatalf("no error expected: %v", err)
|
|
}
|
|
}
|
|
nc.Flush()
|
|
select {
|
|
case err := <-errChan:
|
|
if !test.errExpected {
|
|
t.Fatalf("Expected no error, got %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), `Permissions Violation for Subscription to "foo.bar"`) {
|
|
t.Fatalf("error %v", err)
|
|
}
|
|
case <-time.After(150 * time.Millisecond):
|
|
if test.errExpected {
|
|
t.Fatal("Expected an error")
|
|
}
|
|
}
|
|
})
|
|
|
|
}
|
|
}
|
|
|
|
func TestJWScopedSigningKeys(t *testing.T) {
|
|
sysKp, syspub := createKey(t)
|
|
sysJwt := encodeClaim(t, jwt.NewAccountClaims(syspub), syspub)
|
|
sysCreds := newUser(t, sysKp)
|
|
defer removeFile(t, sysCreds)
|
|
|
|
_, aExpPub := createKey(t)
|
|
accClaim := jwt.NewAccountClaims(aExpPub)
|
|
accClaim.Name = "acc"
|
|
|
|
aSignNonScopedKp, aSignNonScopedPub := createKey(t)
|
|
accClaim.SigningKeys.Add(aSignNonScopedPub)
|
|
|
|
aSignScopedKp, aSignScopedPub := createKey(t)
|
|
signer := jwt.NewUserScope()
|
|
signer.Key = aSignScopedPub
|
|
signer.Template.Pub.Deny.Add("denied")
|
|
signer.Template.Payload = 5
|
|
accClaim.SigningKeys.AddScopedSigner(signer)
|
|
accJwt := encodeClaim(t, accClaim, aExpPub)
|
|
|
|
aNonScopedCreds := newUserEx(t, aSignNonScopedKp, false, aExpPub)
|
|
defer removeFile(t, aNonScopedCreds)
|
|
aBadScopedCreds := newUserEx(t, aSignScopedKp, false, aExpPub)
|
|
defer removeFile(t, aBadScopedCreds)
|
|
aScopedCreds := newUserEx(t, aSignScopedKp, true, aExpPub)
|
|
defer removeFile(t, aScopedCreds)
|
|
|
|
dirSrv := createDir(t, "srv")
|
|
defer removeDir(t, dirSrv)
|
|
cf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
operator: %s
|
|
system_account: %s
|
|
resolver: {
|
|
type: full
|
|
dir: %s
|
|
}
|
|
`, ojwt, syspub, dirSrv)))
|
|
defer removeFile(t, cf)
|
|
s, opts := RunServerWithConfig(cf)
|
|
defer s.Shutdown()
|
|
|
|
url := fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port)
|
|
errChan := make(chan error, 1)
|
|
defer close(errChan)
|
|
awaitError := func(expected bool) {
|
|
t.Helper()
|
|
select {
|
|
case err := <-errChan:
|
|
if !expected {
|
|
t.Fatalf("Expected no error, got %v", err)
|
|
}
|
|
case <-time.After(150 * time.Millisecond):
|
|
if expected {
|
|
t.Fatal("Expected an error")
|
|
}
|
|
}
|
|
}
|
|
errHdlr := nats.ErrorHandler(func(conn *nats.Conn, s *nats.Subscription, err error) {
|
|
errChan <- err
|
|
})
|
|
if updateJwt(t, url, sysCreds, sysJwt, 1) != 1 {
|
|
t.Error("Expected update to pass")
|
|
} else if updateJwt(t, url, sysCreds, accJwt, 1) != 1 {
|
|
t.Error("Expected update to pass")
|
|
}
|
|
t.Run("bad-scoped-signing-key", func(t *testing.T) {
|
|
_, err := nats.Connect(url, nats.UserCredentials(aBadScopedCreds))
|
|
require_Error(t, err)
|
|
})
|
|
t.Run("regular-signing-key", func(t *testing.T) {
|
|
nc := natsConnect(t, url, nats.UserCredentials(aNonScopedCreds), errHdlr)
|
|
defer nc.Close()
|
|
nc.Flush()
|
|
err := nc.Publish("denied", nil)
|
|
require_NoError(t, err)
|
|
nc.Flush()
|
|
awaitError(false)
|
|
})
|
|
t.Run("scoped-signing-key-client-side", func(t *testing.T) {
|
|
nc := natsConnect(t, url, nats.UserCredentials(aScopedCreds), errHdlr)
|
|
defer nc.Close()
|
|
nc.Flush()
|
|
err := nc.Publish("too-long", []byte("way.too.long.for.payload.limit"))
|
|
require_Error(t, err)
|
|
require_True(t, strings.Contains(err.Error(), ErrMaxPayload.Error()))
|
|
})
|
|
t.Run("scoped-signing-key-server-side", func(t *testing.T) {
|
|
nc := natsConnect(t, url, nats.UserCredentials(aScopedCreds), errHdlr)
|
|
defer nc.Close()
|
|
nc.Flush()
|
|
err := nc.Publish("denied", nil)
|
|
require_NoError(t, err)
|
|
nc.Flush()
|
|
awaitError(true)
|
|
})
|
|
t.Run("scoped-signing-key-reload", func(t *testing.T) {
|
|
reconChan := make(chan struct{}, 1)
|
|
defer close(reconChan)
|
|
msgChan := make(chan *nats.Msg, 2)
|
|
defer close(msgChan)
|
|
nc := natsConnect(t, url, nats.UserCredentials(aScopedCreds), errHdlr,
|
|
nats.DisconnectErrHandler(func(conn *nats.Conn, err error) {
|
|
if err != nil {
|
|
errChan <- err
|
|
}
|
|
}),
|
|
nats.ReconnectHandler(func(conn *nats.Conn) {
|
|
reconChan <- struct{}{}
|
|
}),
|
|
)
|
|
defer nc.Close()
|
|
_, err := nc.ChanSubscribe("denied", msgChan)
|
|
require_NoError(t, err)
|
|
nc.Flush()
|
|
err = nc.Publish("denied", nil)
|
|
require_NoError(t, err)
|
|
awaitError(true)
|
|
require_Len(t, len(msgChan), 0)
|
|
// Alter scoped permissions and update
|
|
signer.Template.Payload = -1
|
|
signer.Template.Pub.Deny.Remove("denied")
|
|
accClaim.SigningKeys.AddScopedSigner(signer)
|
|
accUpdatedJwt := encodeClaim(t, accClaim, aExpPub)
|
|
if updateJwt(t, url, sysCreds, accUpdatedJwt, 1) != 1 {
|
|
t.Error("Expected update to pass")
|
|
}
|
|
// disconnect triggered by update
|
|
awaitError(true)
|
|
<-reconChan
|
|
nc.Flush()
|
|
err = nc.Publish("denied", []byte("way.too.long.for.old.payload.limit"))
|
|
require_NoError(t, err)
|
|
awaitError(false)
|
|
msg := <-msgChan
|
|
require_Equal(t, string(msg.Data), "way.too.long.for.old.payload.limit")
|
|
require_Len(t, len(msgChan), 0)
|
|
})
|
|
require_Len(t, len(errChan), 0)
|
|
}
|
|
|
|
func TestJWTStrictSigningKeys(t *testing.T) {
|
|
newAccount := func(opKp nkeys.KeyPair) (nkeys.KeyPair, nkeys.KeyPair, string, *jwt.AccountClaims, string) {
|
|
accId, err := nkeys.CreateAccount()
|
|
require_NoError(t, err)
|
|
accIdPub, err := accId.PublicKey()
|
|
require_NoError(t, err)
|
|
|
|
accSig, err := nkeys.CreateAccount()
|
|
require_NoError(t, err)
|
|
accSigPub, err := accSig.PublicKey()
|
|
require_NoError(t, err)
|
|
|
|
aClaim := jwt.NewAccountClaims(accIdPub)
|
|
aClaim.SigningKeys.Add(accSigPub)
|
|
theJwt, err := aClaim.Encode(opKp)
|
|
require_NoError(t, err)
|
|
return accId, accSig, accIdPub, aClaim, theJwt
|
|
}
|
|
|
|
opId, err := nkeys.CreateOperator()
|
|
require_NoError(t, err)
|
|
opIdPub, err := opId.PublicKey()
|
|
require_NoError(t, err)
|
|
|
|
opSig, err := nkeys.CreateOperator()
|
|
require_NoError(t, err)
|
|
opSigPub, err := opSig.PublicKey()
|
|
require_NoError(t, err)
|
|
|
|
aBadBadKp, aBadGoodKp, aBadPub, _, aBadJwt := newAccount(opId)
|
|
aGoodBadKp, aGoodGoodKp, aGoodPub, _, aGoodJwt := newAccount(opSig)
|
|
_, aSysKp, aSysPub, _, aSysJwt := newAccount(opSig)
|
|
|
|
oClaim := jwt.NewOperatorClaims(opIdPub)
|
|
oClaim.StrictSigningKeyUsage = true
|
|
oClaim.SigningKeys.Add(opSigPub)
|
|
oClaim.SystemAccount = aSysPub
|
|
oJwt, err := oClaim.Encode(opId)
|
|
require_NoError(t, err)
|
|
|
|
uBadBadCreds := newUserEx(t, aBadBadKp, false, aBadPub)
|
|
defer removeFile(t, uBadBadCreds)
|
|
uBadGoodCreds := newUserEx(t, aBadGoodKp, false, aBadPub)
|
|
defer removeFile(t, uBadGoodCreds)
|
|
uGoodBadCreds := newUserEx(t, aGoodBadKp, false, aGoodPub)
|
|
defer removeFile(t, uGoodBadCreds)
|
|
uGoodGoodCreds := newUserEx(t, aGoodGoodKp, false, aGoodPub)
|
|
defer removeFile(t, uGoodGoodCreds)
|
|
uSysCreds := newUserEx(t, aSysKp, false, aSysPub)
|
|
defer removeFile(t, uSysCreds)
|
|
|
|
connectTest := func(url string) {
|
|
for _, test := range []struct {
|
|
creds string
|
|
fail bool
|
|
}{
|
|
{uBadBadCreds, true},
|
|
{uBadGoodCreds, true},
|
|
{uGoodBadCreds, true},
|
|
{uGoodGoodCreds, false},
|
|
} {
|
|
nc, err := nats.Connect(url, nats.UserCredentials(test.creds))
|
|
nc.Close()
|
|
if test.fail {
|
|
require_Error(t, err)
|
|
} else {
|
|
require_NoError(t, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
t.Run("resolver", func(t *testing.T) {
|
|
dirSrv := createDir(t, "srv")
|
|
defer removeDir(t, dirSrv)
|
|
cf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
operator = %s
|
|
resolver: {
|
|
type: full
|
|
dir: %s
|
|
}
|
|
resolver_preload = {
|
|
%s : "%s"
|
|
}
|
|
`, oJwt, dirSrv, aSysPub, aSysJwt)))
|
|
defer removeFile(t, cf)
|
|
s, _ := RunServerWithConfig(cf)
|
|
defer s.Shutdown()
|
|
url := s.ClientURL()
|
|
if updateJwt(t, url, uSysCreds, aBadJwt, 1) != 0 {
|
|
t.Fatal("Expected negative response")
|
|
}
|
|
if updateJwt(t, url, uSysCreds, aGoodJwt, 1) != 1 {
|
|
t.Fatal("Expected positive response")
|
|
}
|
|
connectTest(url)
|
|
})
|
|
|
|
t.Run("mem-resolver", func(t *testing.T) {
|
|
dirSrv := createDir(t, "srv")
|
|
defer removeDir(t, dirSrv)
|
|
cf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
operator = %s
|
|
resolver: MEMORY
|
|
resolver_preload = {
|
|
%s : "%s"
|
|
%s : "%s"
|
|
%s : "%s"
|
|
}
|
|
`, oJwt, aSysPub, aSysJwt, aBadPub, aBadJwt, aGoodPub, aGoodJwt)))
|
|
defer removeFile(t, cf)
|
|
s, _ := RunServerWithConfig(cf)
|
|
defer s.Shutdown()
|
|
connectTest(s.ClientURL())
|
|
})
|
|
}
|
|
|
|
func TestJWTAccountProtectedImport(t *testing.T) {
|
|
srvFmt := `
|
|
port: -1
|
|
operator = %s
|
|
resolver: MEMORY
|
|
resolver_preload = {
|
|
%s : "%s"
|
|
%s : "%s"
|
|
} `
|
|
setupAccounts := func(pass bool) (nkeys.KeyPair, string, string, string, nkeys.KeyPair, string, string, string, string) {
|
|
// Create accounts and imports/exports.
|
|
exportKP, _ := nkeys.CreateAccount()
|
|
exportPub, _ := exportKP.PublicKey()
|
|
exportAC := jwt.NewAccountClaims(exportPub)
|
|
exportAC.Exports.Add(&jwt.Export{Subject: "service.*", Type: jwt.Service, AccountTokenPosition: 2})
|
|
exportAC.Exports.Add(&jwt.Export{Subject: "stream.*", Type: jwt.Stream, AccountTokenPosition: 2})
|
|
exportJWT, err := exportAC.Encode(oKp)
|
|
require_NoError(t, err)
|
|
// create alternative exporter jwt without account token pos set
|
|
exportAC.Exports = jwt.Exports{}
|
|
exportAC.Exports.Add(&jwt.Export{Subject: "service.*", Type: jwt.Service})
|
|
exportAC.Exports.Add(&jwt.Export{Subject: "stream.*", Type: jwt.Stream})
|
|
exportJWTNoPos, err := exportAC.Encode(oKp)
|
|
require_NoError(t, err)
|
|
|
|
importKP, _ := nkeys.CreateAccount()
|
|
importPub, _ := importKP.PublicKey()
|
|
importAc := jwt.NewAccountClaims(importPub)
|
|
srvcSub, strmSub := "service.foo", "stream.foo"
|
|
if pass {
|
|
srvcSub = fmt.Sprintf("service.%s", importPub)
|
|
strmSub = fmt.Sprintf("stream.%s", importPub)
|
|
}
|
|
importAc.Imports.Add(&jwt.Import{Account: exportPub, Subject: jwt.Subject(srvcSub), Type: jwt.Service})
|
|
importAc.Imports.Add(&jwt.Import{Account: exportPub, Subject: jwt.Subject(strmSub), Type: jwt.Stream})
|
|
importJWT, err := importAc.Encode(oKp)
|
|
require_NoError(t, err)
|
|
|
|
return exportKP, exportPub, exportJWT, exportJWTNoPos, importKP, importPub, importJWT, srvcSub, strmSub
|
|
}
|
|
t.Run("pass", func(t *testing.T) {
|
|
exportKp, exportPub, exportJWT, _, importKp, importPub, importJWT, srvcSub, strmSub := setupAccounts(true)
|
|
cf := createConfFile(t, []byte(fmt.Sprintf(srvFmt, ojwt, exportPub, exportJWT, importPub, importJWT)))
|
|
defer removeFile(t, cf)
|
|
s, _ := RunServerWithConfig(cf)
|
|
defer s.Shutdown()
|
|
ncExp := natsConnect(t, s.ClientURL(), createUserCreds(t, s, exportKp))
|
|
defer ncExp.Close()
|
|
ncImp := natsConnect(t, s.ClientURL(), createUserCreds(t, s, importKp))
|
|
defer ncImp.Close()
|
|
t.Run("service", func(t *testing.T) {
|
|
sub, err := ncExp.Subscribe("service.*", func(msg *nats.Msg) {
|
|
msg.Respond([]byte("world"))
|
|
})
|
|
defer sub.Unsubscribe()
|
|
require_NoError(t, err)
|
|
ncExp.Flush()
|
|
msg, err := ncImp.Request(srvcSub, []byte("hello"), time.Second)
|
|
require_NoError(t, err)
|
|
require_Equal(t, string(msg.Data), "world")
|
|
})
|
|
t.Run("stream", func(t *testing.T) {
|
|
msgChan := make(chan *nats.Msg, 4)
|
|
defer close(msgChan)
|
|
sub, err := ncImp.ChanSubscribe(strmSub, msgChan)
|
|
defer sub.Unsubscribe()
|
|
require_NoError(t, err)
|
|
ncImp.Flush()
|
|
err = ncExp.Publish("stream.foo", []byte("hello"))
|
|
require_NoError(t, err)
|
|
err = ncExp.Publish(strmSub, []byte("hello"))
|
|
require_NoError(t, err)
|
|
msg := <-msgChan
|
|
require_Equal(t, string(msg.Data), "hello")
|
|
require_True(t, len(msgChan) == 0)
|
|
})
|
|
})
|
|
t.Run("fail", func(t *testing.T) {
|
|
exportKp, exportPub, exportJWT, _, importKp, importPub, importJWT, srvcSub, strmSub := setupAccounts(false)
|
|
cf := createConfFile(t, []byte(fmt.Sprintf(srvFmt, ojwt, exportPub, exportJWT, importPub, importJWT)))
|
|
defer removeFile(t, cf)
|
|
s, _ := RunServerWithConfig(cf)
|
|
defer s.Shutdown()
|
|
ncExp := natsConnect(t, s.ClientURL(), createUserCreds(t, s, exportKp))
|
|
defer ncExp.Close()
|
|
ncImp := natsConnect(t, s.ClientURL(), createUserCreds(t, s, importKp))
|
|
defer ncImp.Close()
|
|
t.Run("service", func(t *testing.T) {
|
|
sub, err := ncExp.Subscribe("service.*", func(msg *nats.Msg) {
|
|
msg.Respond([]byte("world"))
|
|
})
|
|
defer sub.Unsubscribe()
|
|
require_NoError(t, err)
|
|
ncExp.Flush()
|
|
_, err = ncImp.Request(srvcSub, []byte("hello"), time.Second)
|
|
require_Error(t, err)
|
|
require_Contains(t, err.Error(), "no responders available for request")
|
|
})
|
|
t.Run("stream", func(t *testing.T) {
|
|
msgChan := make(chan *nats.Msg, 4)
|
|
defer close(msgChan)
|
|
_, err := ncImp.ChanSubscribe(strmSub, msgChan)
|
|
require_NoError(t, err)
|
|
ncImp.Flush()
|
|
err = ncExp.Publish("stream.foo", []byte("hello"))
|
|
require_NoError(t, err)
|
|
err = ncExp.Publish(strmSub, []byte("hello"))
|
|
require_NoError(t, err)
|
|
select {
|
|
case <-msgChan:
|
|
t.Fatal("did not expect a message")
|
|
case <-time.After(250 * time.Millisecond):
|
|
}
|
|
require_True(t, len(msgChan) == 0)
|
|
})
|
|
})
|
|
t.Run("reload-off-2-on", func(t *testing.T) {
|
|
exportKp, exportPub, exportJWTOn, exportJWTOff, importKp, _, importJWT, srvcSub, strmSub := setupAccounts(false)
|
|
dirSrv := createDir(t, "srv")
|
|
defer removeDir(t, dirSrv)
|
|
// set up system account. Relying bootstrapping system account to not create JWT
|
|
sysAcc, err := nkeys.CreateAccount()
|
|
require_NoError(t, err)
|
|
sysPub, err := sysAcc.PublicKey()
|
|
require_NoError(t, err)
|
|
sysUsrCreds := newUserEx(t, sysAcc, false, sysPub)
|
|
defer removeFile(t, sysUsrCreds)
|
|
cf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
operator = %s
|
|
system_account = %s
|
|
resolver: {
|
|
type: full
|
|
dir: %s
|
|
}`, ojwt, sysPub, dirSrv)))
|
|
defer removeFile(t, cf)
|
|
s, _ := RunServerWithConfig(cf)
|
|
defer s.Shutdown()
|
|
updateJwt(t, s.ClientURL(), sysUsrCreds, importJWT, 1)
|
|
updateJwt(t, s.ClientURL(), sysUsrCreds, exportJWTOff, 1)
|
|
ncExp := natsConnect(t, s.ClientURL(), createUserCreds(t, s, exportKp))
|
|
defer ncExp.Close()
|
|
ncImp := natsConnect(t, s.ClientURL(), createUserCreds(t, s, importKp))
|
|
defer ncImp.Close()
|
|
msgChan := make(chan *nats.Msg, 4)
|
|
defer close(msgChan)
|
|
// ensure service passes
|
|
subSrvc, err := ncExp.Subscribe("service.*", func(msg *nats.Msg) {
|
|
msg.Respond([]byte("world"))
|
|
})
|
|
defer subSrvc.Unsubscribe()
|
|
require_NoError(t, err)
|
|
ncExp.Flush()
|
|
respMst, err := ncImp.Request(srvcSub, []byte("hello"), time.Second)
|
|
require_NoError(t, err)
|
|
require_Equal(t, string(respMst.Data), "world")
|
|
// ensure stream passes
|
|
subStrm, err := ncImp.ChanSubscribe(strmSub, msgChan)
|
|
defer subStrm.Unsubscribe()
|
|
require_NoError(t, err)
|
|
ncImp.Flush()
|
|
err = ncExp.Publish(strmSub, []byte("hello"))
|
|
require_NoError(t, err)
|
|
msg := <-msgChan
|
|
require_Equal(t, string(msg.Data), "hello")
|
|
require_True(t, len(msgChan) == 0)
|
|
|
|
updateJwt(t, s.ClientURL(), sysUsrCreds, exportJWTOn, 1)
|
|
|
|
// ensure service fails
|
|
_, err = ncImp.Request(srvcSub, []byte("hello"), time.Second)
|
|
require_Error(t, err)
|
|
require_Contains(t, err.Error(), "timeout")
|
|
s.AccountResolver().Store(exportPub, exportJWTOn)
|
|
// ensure stream fails
|
|
err = ncExp.Publish(strmSub, []byte("hello"))
|
|
require_NoError(t, err)
|
|
select {
|
|
case <-msgChan:
|
|
t.Fatal("did not expect a message")
|
|
case <-time.After(250 * time.Millisecond):
|
|
}
|
|
require_True(t, len(msgChan) == 0)
|
|
})
|
|
}
|
|
|
|
func TestJWTMappings(t *testing.T) {
|
|
sysKp, syspub := createKey(t)
|
|
sysJwt := encodeClaim(t, jwt.NewAccountClaims(syspub), syspub)
|
|
sysCreds := newUser(t, sysKp)
|
|
defer removeFile(t, sysCreds)
|
|
|
|
// create two jwt, one with and one without mapping
|
|
aKp, aPub := createKey(t)
|
|
aClaim := jwt.NewAccountClaims(aPub)
|
|
aJwtNoM := encodeClaim(t, aClaim, aPub)
|
|
aClaim.AddMapping("foo1", jwt.WeightedMapping{Subject: "bar1"})
|
|
aJwtMap1 := encodeClaim(t, aClaim, aPub)
|
|
|
|
aClaim.Mappings = map[jwt.Subject][]jwt.WeightedMapping{}
|
|
aClaim.AddMapping("foo2", jwt.WeightedMapping{Subject: "bar2"})
|
|
aJwtMap2 := encodeClaim(t, aClaim, aPub)
|
|
|
|
dirSrv := createDir(t, "srv")
|
|
defer removeDir(t, dirSrv)
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
operator: %s
|
|
system_account: %s
|
|
resolver: {
|
|
type: full
|
|
dir: %s
|
|
}
|
|
`, ojwt, syspub, dirSrv)))
|
|
defer removeFile(t, conf)
|
|
srv, _ := RunServerWithConfig(conf)
|
|
defer srv.Shutdown()
|
|
updateJwt(t, srv.ClientURL(), sysCreds, sysJwt, 1) // update system account jwt
|
|
|
|
test := func(pub, sub string, fail bool) {
|
|
t.Helper()
|
|
nc := natsConnect(t, srv.ClientURL(), createUserCreds(t, srv, aKp))
|
|
defer nc.Close()
|
|
s, err := nc.SubscribeSync(sub)
|
|
require_NoError(t, err)
|
|
nc.Flush()
|
|
err = nc.Publish(pub, nil)
|
|
require_NoError(t, err)
|
|
_, err = s.NextMsg(500 * time.Millisecond)
|
|
switch {
|
|
case fail && err == nil:
|
|
t.Fatal("expected error, got none")
|
|
case !fail && err != nil:
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
}
|
|
|
|
// turn mappings on
|
|
require_Len(t, 1, updateJwt(t, srv.ClientURL(), sysCreds, aJwtMap1, 1))
|
|
test("foo1", "bar1", false)
|
|
// alter mappings
|
|
require_Len(t, 1, updateJwt(t, srv.ClientURL(), sysCreds, aJwtMap2, 1))
|
|
test("foo1", "bar1", true)
|
|
test("foo2", "bar2", false)
|
|
// turn mappings off
|
|
require_Len(t, 1, updateJwt(t, srv.ClientURL(), sysCreds, aJwtNoM, 1))
|
|
test("foo2", "bar2", true)
|
|
}
|
|
|
|
func TestJWTNoSystemAccountButNatsResolver(t *testing.T) {
|
|
dirSrv := createDir(t, "srv")
|
|
defer removeDir(t, dirSrv)
|
|
for _, resType := range []string{"full", "cache"} {
|
|
t.Run(resType, func(t *testing.T) {
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
operator: %s
|
|
resolver: {
|
|
type: %s
|
|
dir: %s
|
|
}`, ojwt, resType, dirSrv)))
|
|
defer removeFile(t, conf)
|
|
opts := LoadConfig(conf)
|
|
s, err := NewServer(opts)
|
|
s.Shutdown()
|
|
require_Error(t, err)
|
|
require_Contains(t, err.Error(), "the system account needs to be specified in configuration or the operator jwt")
|
|
})
|
|
}
|
|
}
|