mirror of
https://github.com/gogrlx/nats-server.git
synced 2026-04-02 03:38:42 -07:00
Fix jwt based user/activation token revocation and revocation granularity
user and activation token did not honor the jwt value for all * on connect. activation token where not re evaluated when the export revoked a key. In part this is a consistency measure so servers that already have an account and servers that don't behave the same way. in jwt activation token revocations are stored per export. The server stored them per account, thus effectively merging revocations. Now they are stored per export inside the server too. fixes nats-io/nsc/issues/442 Signed-off-by: Matthias Hanel <mh@synadia.com>
This commit is contained in:
2
go.sum
2
go.sum
@@ -16,8 +16,6 @@ github.com/minio/highwayhash v1.0.1 h1:dZ6IIu8Z14VlC0VpfKofAhCy74wu/Qb5gcn52yWoz
|
||||
github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
|
||||
github.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296 h1:vU9tpM3apjYlLLeY23zRWJ9Zktr5jp+mloR942LEOpY=
|
||||
github.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k=
|
||||
github.com/nats-io/nats.go v1.13.1-0.20211122170419-d7c1d78a50fc h1:SHr4MUUZJ/fAC0uSm2OzWOJYsHpapmR86mpw7q1qPXU=
|
||||
github.com/nats-io/nats.go v1.13.1-0.20211122170419-d7c1d78a50fc/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=
|
||||
github.com/nats-io/nats.go v1.13.1-0.20220121202836-972a071d373d h1:GRSmEJutHkdoxKsRypP575IIdoXe7Bm6yHQF6GcDBnA=
|
||||
github.com/nats-io/nats.go v1.13.1-0.20220121202836-972a071d373d/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=
|
||||
github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8=
|
||||
|
||||
@@ -69,7 +69,6 @@ type Account struct {
|
||||
rm map[string]int32
|
||||
lqws map[string]int32
|
||||
usersRevoked map[string]int64
|
||||
actsRevoked map[string]int64
|
||||
mappings []*mapping
|
||||
lleafs []*client
|
||||
imports importMap
|
||||
@@ -178,9 +177,10 @@ func (rt ServiceRespType) String() string {
|
||||
// exportAuth holds configured approvals or boolean indicating an
|
||||
// auth token is required for import.
|
||||
type exportAuth struct {
|
||||
tokenReq bool
|
||||
accountPos uint
|
||||
approved map[string]*Account
|
||||
tokenReq bool
|
||||
accountPos uint
|
||||
approved map[string]*Account
|
||||
actsRevoked map[string]int64
|
||||
}
|
||||
|
||||
// streamExport
|
||||
@@ -2455,7 +2455,7 @@ func (a *Account) checkAuth(ea *exportAuth, account *Account, imClaim *jwt.Impor
|
||||
}
|
||||
// Check if token required
|
||||
if ea.tokenReq {
|
||||
return a.checkActivation(account, imClaim, true)
|
||||
return a.checkActivation(account, imClaim, ea, true)
|
||||
}
|
||||
if ea.approved == nil {
|
||||
return false
|
||||
@@ -2562,7 +2562,7 @@ func (a *Account) streamActivationExpired(exportAcc *Account, subject string) {
|
||||
}
|
||||
a.mu.RUnlock()
|
||||
|
||||
if si.acc.checkActivation(a, si.claim, false) {
|
||||
if si.acc.checkActivation(a, si.claim, nil, false) {
|
||||
// The token has been updated most likely and we are good to go.
|
||||
return
|
||||
}
|
||||
@@ -2594,7 +2594,7 @@ func (a *Account) serviceActivationExpired(subject string) {
|
||||
}
|
||||
a.mu.RUnlock()
|
||||
|
||||
if si.acc.checkActivation(a, si.claim, false) {
|
||||
if si.acc.checkActivation(a, si.claim, nil, false) {
|
||||
// The token has been updated most likely and we are good to go.
|
||||
return
|
||||
}
|
||||
@@ -2616,17 +2616,20 @@ func (a *Account) activationExpired(exportAcc *Account, subject string, kind jwt
|
||||
}
|
||||
|
||||
func isRevoked(revocations map[string]int64, subject string, issuedAt int64) bool {
|
||||
if revocations == nil {
|
||||
if len(revocations) == 0 {
|
||||
return false
|
||||
}
|
||||
if t, ok := revocations[subject]; !ok || t < issuedAt {
|
||||
return false
|
||||
if t, ok := revocations[jwt.All]; !ok || t < issuedAt {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// checkActivation will check the activation token for validity.
|
||||
func (a *Account) checkActivation(importAcc *Account, claim *jwt.Import, expTimer bool) bool {
|
||||
// ea may only be nil in cases where revocation may not be checked, say triggered by expiration timer.
|
||||
func (a *Account) checkActivation(importAcc *Account, claim *jwt.Import, ea *exportAuth, expTimer bool) bool {
|
||||
if claim == nil || claim.Token == _EMPTY_ {
|
||||
return false
|
||||
}
|
||||
@@ -2662,8 +2665,11 @@ func (a *Account) checkActivation(importAcc *Account, claim *jwt.Import, expTime
|
||||
})
|
||||
}
|
||||
}
|
||||
if ea == nil {
|
||||
return true
|
||||
}
|
||||
// Check for token revocation..
|
||||
return !isRevoked(a.actsRevoked, act.Subject, act.IssuedAt)
|
||||
return !isRevoked(ea.actsRevoked, act.Subject, act.IssuedAt)
|
||||
}
|
||||
|
||||
// Returns true if the activation claim is trusted. That is the issuer matches
|
||||
@@ -2936,9 +2942,6 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim
|
||||
delete(a.imports.services, k)
|
||||
}
|
||||
|
||||
// Reset any notion of export revocations.
|
||||
a.actsRevoked = nil
|
||||
|
||||
alteredScope := map[string]struct{}{}
|
||||
|
||||
// update account signing keys
|
||||
@@ -3013,6 +3016,9 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim
|
||||
s.checkJetStreamExports()
|
||||
}
|
||||
|
||||
streamTokenExpirationChanged := false
|
||||
serviceTokenExpirationChanged := false
|
||||
|
||||
for _, e := range ac.Exports {
|
||||
switch e.Type {
|
||||
case jwt.Stream:
|
||||
@@ -3052,17 +3058,44 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim
|
||||
}
|
||||
}
|
||||
}
|
||||
// We will track these at the account level. Should not have any collisions.
|
||||
if e.Revocations != nil {
|
||||
a.mu.Lock()
|
||||
if a.actsRevoked == nil {
|
||||
a.actsRevoked = make(map[string]int64)
|
||||
|
||||
var revocationChanged *bool
|
||||
var ea *exportAuth
|
||||
|
||||
a.mu.Lock()
|
||||
switch e.Type {
|
||||
case jwt.Stream:
|
||||
revocationChanged = &streamTokenExpirationChanged
|
||||
if se, ok := a.exports.streams[string(e.Subject)]; ok && se != nil {
|
||||
ea = &se.exportAuth
|
||||
}
|
||||
for k, t := range e.Revocations {
|
||||
a.actsRevoked[k] = t
|
||||
case jwt.Service:
|
||||
revocationChanged = &serviceTokenExpirationChanged
|
||||
if se, ok := a.exports.services[string(e.Subject)]; ok && se != nil {
|
||||
ea = &se.exportAuth
|
||||
}
|
||||
a.mu.Unlock()
|
||||
}
|
||||
if ea != nil {
|
||||
oldRevocations := ea.actsRevoked
|
||||
if len(e.Revocations) == 0 {
|
||||
// remove all, no need to evaluate existing imports
|
||||
ea.actsRevoked = nil
|
||||
} else if len(oldRevocations) == 0 {
|
||||
// add all, existing imports need to be re evaluated
|
||||
ea.actsRevoked = e.Revocations
|
||||
*revocationChanged = true
|
||||
} else {
|
||||
ea.actsRevoked = e.Revocations
|
||||
// diff, existing imports need to be conditionally re evaluated, depending on:
|
||||
// if a key was added, or it's timestamp increased
|
||||
for k, t := range e.Revocations {
|
||||
if tOld, ok := oldRevocations[k]; !ok || tOld < t {
|
||||
*revocationChanged = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
a.mu.Unlock()
|
||||
}
|
||||
var incompleteImports []*jwt.Import
|
||||
for _, i := range ac.Imports {
|
||||
@@ -3116,7 +3149,7 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim
|
||||
}
|
||||
}
|
||||
// Now check if stream exports have changed.
|
||||
if !a.checkStreamExportsEqual(old) || signersChanged {
|
||||
if !a.checkStreamExportsEqual(old) || signersChanged || streamTokenExpirationChanged {
|
||||
clients := map[*client]struct{}{}
|
||||
// We need to check all accounts that have an import claim from this account.
|
||||
awcsti := map[string]struct{}{}
|
||||
@@ -3149,7 +3182,7 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim
|
||||
}
|
||||
}
|
||||
// Now check if service exports have changed.
|
||||
if !a.checkServiceExportsEqual(old) || signersChanged {
|
||||
if !a.checkServiceExportsEqual(old) || signersChanged || serviceTokenExpirationChanged {
|
||||
s.accounts.Range(func(k, v interface{}) bool {
|
||||
acc := v.(*Account)
|
||||
// Move to the next if this account is actually account "a".
|
||||
|
||||
@@ -4376,54 +4376,59 @@ func TestJWTJetStreamLimits(t *testing.T) {
|
||||
}
|
||||
|
||||
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)
|
||||
test := func(all bool) {
|
||||
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
|
||||
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)
|
||||
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)
|
||||
// create updated claim need to assure that issue time differs
|
||||
if all {
|
||||
claim.Revoke(jwt.All) // revokes all jwt from now on
|
||||
} else {
|
||||
claim.Revoke(upub) // revokes this 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)
|
||||
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(`
|
||||
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: 127.0.0.1:-1
|
||||
operator: %s
|
||||
system_account: %s
|
||||
@@ -4432,47 +4437,189 @@ func TestJWTUserRevocation(t *testing.T) {
|
||||
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")
|
||||
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()
|
||||
}
|
||||
// assure that nc got disconnected due to the revocation
|
||||
select {
|
||||
case <-doneChan:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("Expected connection to have failed")
|
||||
t.Run("specific-key", func(t *testing.T) {
|
||||
test(false)
|
||||
})
|
||||
t.Run("all-key", func(t *testing.T) {
|
||||
test(true)
|
||||
})
|
||||
}
|
||||
|
||||
func TestJWTActivationRevocation(t *testing.T) {
|
||||
test := func(all bool) {
|
||||
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: "foo",
|
||||
Type: jwt.Stream,
|
||||
TokenReq: true,
|
||||
})
|
||||
aExp1Jwt := encodeClaim(t, aExpClaim, aExpPub)
|
||||
aExpCreds := newUser(t, aExpKp)
|
||||
|
||||
time.Sleep(1100 * time.Millisecond)
|
||||
aImpKp, aImpPub := createKey(t)
|
||||
|
||||
revPubKey := aImpPub
|
||||
if all {
|
||||
revPubKey = jwt.All
|
||||
}
|
||||
|
||||
aExpClaim.Exports[0].RevokeAt(revPubKey, time.Now())
|
||||
aExp2Jwt := encodeClaim(t, aExpClaim, aExpPub)
|
||||
|
||||
aExpClaim.Exports[0].ClearRevocation(revPubKey)
|
||||
aExp3Jwt := encodeClaim(t, aExpClaim, aExpPub)
|
||||
|
||||
ac := &jwt.ActivationClaims{}
|
||||
ac.Subject = aImpPub
|
||||
ac.ImportSubject = "foo"
|
||||
ac.ImportType = jwt.Stream
|
||||
token, err := ac.Encode(aExpKp)
|
||||
require_NoError(t, err)
|
||||
|
||||
aImpClaim := jwt.NewAccountClaims(aImpPub)
|
||||
aImpClaim.Name = "Import"
|
||||
aImpClaim.Imports.Add(&jwt.Import{
|
||||
Subject: "foo",
|
||||
Type: jwt.Stream,
|
||||
Account: aExpPub,
|
||||
Token: token,
|
||||
})
|
||||
aImpJwt := encodeClaim(t, aImpClaim, aImpPub)
|
||||
aImpCreds := newUser(t, aImpKp)
|
||||
defer removeFile(t, aExpCreds)
|
||||
defer removeFile(t, aImpCreds)
|
||||
|
||||
dirSrv := createDir(t, "srv")
|
||||
defer removeDir(t, dirSrv)
|
||||
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
||||
listen: 127.0.0.1:-1
|
||||
operator: %s
|
||||
system_account: %s
|
||||
resolver: {
|
||||
type: full
|
||||
dir: %s
|
||||
}
|
||||
`, ojwt, syspub, dirSrv)))
|
||||
defer removeFile(t, conf)
|
||||
|
||||
t.Run("token-expired-on-connect", func(t *testing.T) {
|
||||
srv, _ := RunServerWithConfig(conf)
|
||||
defer srv.Shutdown()
|
||||
defer removeDir(t, dirSrv) // clean jwt directory
|
||||
|
||||
updateJwt(t, srv.ClientURL(), sysCreds, sysJwt, 1) // update system account jwt
|
||||
updateJwt(t, srv.ClientURL(), sysCreds, aExp2Jwt, 1) // set account jwt without revocation
|
||||
updateJwt(t, srv.ClientURL(), sysCreds, aImpJwt, 1)
|
||||
|
||||
ncExp1 := natsConnect(t, srv.ClientURL(), nats.UserCredentials(aExpCreds))
|
||||
defer ncExp1.Close()
|
||||
|
||||
ncImp := natsConnect(t, srv.ClientURL(), nats.UserCredentials(aImpCreds))
|
||||
defer ncImp.Close()
|
||||
|
||||
sub, err := ncImp.SubscribeSync("foo")
|
||||
require_NoError(t, err)
|
||||
require_NoError(t, ncImp.Flush())
|
||||
require_NoError(t, ncExp1.Publish("foo", []byte("1")))
|
||||
_, err = sub.NextMsg(time.Second)
|
||||
require_Error(t, err)
|
||||
require_Equal(t, err.Error(), "nats: timeout")
|
||||
})
|
||||
|
||||
t.Run("token-expired-on-update", func(t *testing.T) {
|
||||
srv, _ := RunServerWithConfig(conf)
|
||||
defer srv.Shutdown()
|
||||
defer removeDir(t, dirSrv) // clean jwt directory
|
||||
|
||||
updateJwt(t, srv.ClientURL(), sysCreds, sysJwt, 1) // update system account jwt
|
||||
updateJwt(t, srv.ClientURL(), sysCreds, aExp1Jwt, 1) // set account jwt without revocation
|
||||
updateJwt(t, srv.ClientURL(), sysCreds, aImpJwt, 1)
|
||||
|
||||
ncExp1 := natsConnect(t, srv.ClientURL(), nats.UserCredentials(aExpCreds))
|
||||
defer ncExp1.Close()
|
||||
|
||||
ncImp := natsConnect(t, srv.ClientURL(), nats.UserCredentials(aImpCreds))
|
||||
defer ncImp.Close()
|
||||
|
||||
sub, err := ncImp.SubscribeSync("foo")
|
||||
require_NoError(t, err)
|
||||
require_NoError(t, ncImp.Flush())
|
||||
require_NoError(t, ncExp1.Publish("foo", []byte("1")))
|
||||
m1, err := sub.NextMsg(time.Second)
|
||||
require_NoError(t, err)
|
||||
require_Equal(t, string(m1.Data), "1")
|
||||
|
||||
updateJwt(t, srv.ClientURL(), sysCreds, aExp2Jwt, 1) // set account jwt with revocation
|
||||
|
||||
require_NoError(t, ncExp1.Publish("foo", []byte("2")))
|
||||
_, err = sub.NextMsg(time.Second)
|
||||
require_Error(t, err)
|
||||
require_Equal(t, err.Error(), "nats: timeout")
|
||||
|
||||
updateJwt(t, srv.ClientURL(), sysCreds, aExp3Jwt, 1) // set account with revocation cleared
|
||||
|
||||
require_NoError(t, ncExp1.Publish("foo", []byte("3")))
|
||||
m2, err := sub.NextMsg(time.Second)
|
||||
require_NoError(t, err)
|
||||
require_Equal(t, string(m2.Data), "3")
|
||||
})
|
||||
}
|
||||
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()
|
||||
t.Run("specific-key", func(t *testing.T) {
|
||||
test(false)
|
||||
})
|
||||
t.Run("all-key", func(t *testing.T) {
|
||||
test(true)
|
||||
})
|
||||
}
|
||||
|
||||
func TestJWTAccountFetchTimeout(t *testing.T) {
|
||||
|
||||
@@ -2131,7 +2131,8 @@ type ExtImport struct {
|
||||
|
||||
type ExtExport struct {
|
||||
jwt.Export
|
||||
ApprovedAccounts []string `json:"approved_accounts,omitempty"`
|
||||
ApprovedAccounts []string `json:"approved_accounts,omitempty"`
|
||||
RevokedAct map[string]time.Time `json:"revoked_activations,omitempty"`
|
||||
}
|
||||
|
||||
type ExtVrIssues struct {
|
||||
@@ -2162,7 +2163,6 @@ type AccountInfo struct {
|
||||
Claim *jwt.AccountClaims `json:"decoded_jwt,omitempty"`
|
||||
Vr []ExtVrIssues `json:"validation_result_jwt,omitempty"`
|
||||
RevokedUser map[string]time.Time `json:"revoked_user,omitempty"`
|
||||
RevokedAct map[string]time.Time `json:"revoked_activations,omitempty"`
|
||||
Sublist *SublistStats `json:"sublist_stats,omitempty"`
|
||||
Responses map[string]ExtImport `json:"responses,omitempty"`
|
||||
}
|
||||
@@ -2260,6 +2260,13 @@ func (s *Server) accountInfo(accName string) (*AccountInfo, error) {
|
||||
vrIssues[i] = ExtVrIssues{v.Description, v.Blocking, v.TimeCheck}
|
||||
}
|
||||
}
|
||||
collectRevocations := func(revocations map[string]int64) map[string]time.Time {
|
||||
rev := map[string]time.Time{}
|
||||
for k, v := range a.usersRevoked {
|
||||
rev[k] = time.Unix(v, 0)
|
||||
}
|
||||
return rev
|
||||
}
|
||||
exports := []ExtExport{}
|
||||
for k, v := range a.exports.services {
|
||||
e := ExtExport{
|
||||
@@ -2276,6 +2283,7 @@ func (s *Server) accountInfo(accName string) (*AccountInfo, error) {
|
||||
for name := range v.approved {
|
||||
e.ApprovedAccounts = append(e.ApprovedAccounts, name)
|
||||
}
|
||||
e.RevokedAct = collectRevocations(v.actsRevoked)
|
||||
}
|
||||
exports = append(exports, e)
|
||||
}
|
||||
@@ -2292,6 +2300,7 @@ func (s *Server) accountInfo(accName string) (*AccountInfo, error) {
|
||||
for name := range v.approved {
|
||||
e.ApprovedAccounts = append(e.ApprovedAccounts, name)
|
||||
}
|
||||
e.RevokedAct = collectRevocations(v.actsRevoked)
|
||||
}
|
||||
exports = append(exports, e)
|
||||
}
|
||||
@@ -2342,13 +2351,6 @@ func (s *Server) accountInfo(accName string) (*AccountInfo, error) {
|
||||
}
|
||||
mappings[src] = dests
|
||||
}
|
||||
collectRevocations := func(revocations map[string]int64) map[string]time.Time {
|
||||
rev := map[string]time.Time{}
|
||||
for k, v := range a.usersRevoked {
|
||||
rev[k] = time.Unix(v, 0)
|
||||
}
|
||||
return rev
|
||||
}
|
||||
return &AccountInfo{
|
||||
accName,
|
||||
a.updated,
|
||||
@@ -2369,7 +2371,6 @@ func (s *Server) accountInfo(accName string) (*AccountInfo, error) {
|
||||
claim,
|
||||
vrIssues,
|
||||
collectRevocations(a.usersRevoked),
|
||||
collectRevocations(a.actsRevoked),
|
||||
a.sl.Stats(),
|
||||
responses,
|
||||
}, nil
|
||||
|
||||
Reference in New Issue
Block a user