Skip to content

Commit

Permalink
Cherry-pick PRs into v2.10.10 release branch (#5020)
Browse files Browse the repository at this point in the history
Includes:

- #5013 
- #5017 
- #5019 
- #5022
  • Loading branch information
wallyqs committed Feb 1, 2024
2 parents f29b132 + 5527a11 commit 2f8be20
Show file tree
Hide file tree
Showing 10 changed files with 395 additions and 55 deletions.
4 changes: 4 additions & 0 deletions server/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -1877,6 +1877,10 @@ func (a *Account) addServiceImport(dest *Account, from, to string, claim *jwt.Im
rt := Singleton
var lat *serviceLatency

if dest == nil {
return nil, ErrMissingAccount
}

dest.mu.RLock()
se := dest.getServiceExport(to)
if se != nil {
Expand Down
9 changes: 8 additions & 1 deletion server/auth_callout.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,6 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
}
}
}

return targetAcc, nil
}

Expand Down Expand Up @@ -270,6 +269,14 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
return
}

// the JWT is cleared, because if in operator mode it may hold the JWT
// for the bearer token that connected to the callout if in operator mode
// the permissions are already set on the client, this prevents a decode
// on c.RegisterNKeyUser which would have wrong values
c.mu.Lock()
c.opts.JWT = _EMPTY_
c.mu.Unlock()

// Build internal user and bind to the targeted account.
nkuser := buildInternalNkeyUser(arc, allowedConnTypes, targetAcc)
if err := c.RegisterNkeyUser(nkuser); err != nil {
Expand Down
193 changes: 190 additions & 3 deletions server/auth_callout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const (
authCalloutSeed = "SUAP277QP7U4JMFFPVZHLJYEQJ2UHOTYVEIZJYAWRJXQLP4FRSEHYZJJOU"
authCalloutIssuer = "ABJHLOVMPA4CI6R5KLNGOB4GSLNIY7IOUPAJC4YFNDLQVIOBYQGUWVLA"
authCalloutIssuerSeed = "SAANDLKMXL6CUS3CP52WIXBEDN6YJ545GDKC65U5JZPPV6WH6ESWUA6YAI"
authCalloutIssuerSK = "SAAE46BB675HKZKSVJEUZAKKWIV6BJJO6XYE46Z3ZHO7TCI647M3V42IJE"
)

func serviceResponse(t *testing.T, userID string, serverID string, uJwt string, errMsg string, expires time.Duration) []byte {
Expand All @@ -62,12 +63,18 @@ func serviceResponse(t *testing.T, userID string, serverID string, uJwt string,
return []byte(token)
}

func makeScopedRole(t *testing.T, role string, pub []string, sub []string) (jwt.Scope, nkeys.KeyPair) {
func newScopedRole(t *testing.T, role string, pub []string, sub []string, allowResponses bool) (*jwt.UserScope, nkeys.KeyPair) {
akp, pk := createKey(t)
r := jwt.NewUserScope()
r.Key = pk
r.Template.Sub.Allow.Add(sub...)
r.Template.Pub.Allow.Add(pub...)
if allowResponses {
r.Template.Resp = &jwt.ResponsePermission{
MaxMsgs: 1,
Expires: time.Second * 3,
}
}
r.Role = role
return r, akp
}
Expand Down Expand Up @@ -131,7 +138,7 @@ func NewAuthTest(t *testing.T, config string, authHandler nats.MsgHandler, clien
a.srv, _ = RunServerWithConfig(a.conf)

var err error
a.authClient = a.Connect(clientOptions...)
a.authClient = a.ConnectCallout(clientOptions...)
_, err = a.authClient.Subscribe(AuthCalloutSubject, authHandler)
require_NoError(t, err)
return a
Expand All @@ -146,6 +153,15 @@ func (at *authTest) NewClient(clientOptions ...nats.Option) (*nats.Conn, error)
return conn, nil
}

func (at *authTest) ConnectCallout(clientOptions ...nats.Option) *nats.Conn {
conn, err := at.NewClient(clientOptions...)
if err != nil {
err = fmt.Errorf("callout client failed: %w", err)
}
require_NoError(at.t, err)
return conn
}

func (at *authTest) Connect(clientOptions ...nats.Option) *nats.Conn {
conn, err := at.NewClient(clientOptions...)
require_NoError(at.t, err)
Expand Down Expand Up @@ -457,6 +473,7 @@ func createAuthServiceUser(t *testing.T, accKp nkeys.KeyPair) (pub, creds string
seed, _ := ukp.Seed()
upub, _ := ukp.PublicKey()
uclaim := newJWTTestUserClaims()
uclaim.Name = "auth-service"
uclaim.Subject = upub
vr := jwt.ValidationResults{}
uclaim.Validate(&vr)
Expand All @@ -473,6 +490,7 @@ func createBasicAccountUser(t *testing.T, accKp nkeys.KeyPair) (creds string) {
upub, _ := ukp.PublicKey()
uclaim := newJWTTestUserClaims()
uclaim.Subject = upub
uclaim.Name = "auth-client"
// For these deny all permission
uclaim.Permissions.Pub.Deny.Add(">")
uclaim.Permissions.Sub.Deny.Add(">")
Expand All @@ -484,6 +502,28 @@ func createBasicAccountUser(t *testing.T, accKp nkeys.KeyPair) (creds string) {
return genCredsFile(t, ujwt, seed)
}

func createScopedUser(t *testing.T, accKp nkeys.KeyPair, sk nkeys.KeyPair) (creds string) {
t.Helper()
ukp, _ := nkeys.CreateUser()
seed, _ := ukp.Seed()
upub, _ := ukp.PublicKey()
uclaim := newJWTTestUserClaims()
apk, _ := accKp.PublicKey()
uclaim.IssuerAccount = apk
uclaim.Subject = upub
uclaim.Name = "scoped-user"
uclaim.SetScoped(true)

// Uncomment this to set the sub limits
// uclaim.Limits.Subs = 0
vr := jwt.ValidationResults{}
uclaim.Validate(&vr)
require_Len(t, len(vr.Errors()), 0)
ujwt, err := uclaim.Encode(sk)
require_NoError(t, err)
return genCredsFile(t, ujwt, seed)
}

func TestAuthCalloutOperatorNoServerConfigCalloutAllowed(t *testing.T) {
conf := createConfFile(t, []byte(fmt.Sprintf(`
listen: 127.0.0.1:-1
Expand Down Expand Up @@ -516,7 +556,7 @@ func TestAuthCalloutOperatorModeBasics(t *testing.T) {
accClaim := jwt.NewAccountClaims(tpub)
accClaim.Name = "TEST"
accClaim.SigningKeys.Add(tSigningPub)
scope, scopedKp := makeScopedRole(t, "foo", []string{"foo.>", "$SYS.REQ.USER.INFO"}, []string{"foo.>", "_INBOX.>"})
scope, scopedKp := newScopedRole(t, "foo", []string{"foo.>", "$SYS.REQ.USER.INFO"}, []string{"foo.>", "_INBOX.>"}, false)
accClaim.SigningKeys.AddScopedSigner(scope)
accJwt, err := accClaim.Encode(oKp)
require_NoError(t, err)
Expand Down Expand Up @@ -674,6 +714,153 @@ func TestAuthCalloutOperatorModeBasics(t *testing.T) {
require_Equal(t, "foo.>", userInfo.Permissions.Subscribe.Allow[1])
}

func testAuthCalloutScopedUser(t *testing.T, allowAnyAccount bool) {
_, spub := createKey(t)
sysClaim := jwt.NewAccountClaims(spub)
sysClaim.Name = "$SYS"
sysJwt, err := sysClaim.Encode(oKp)
require_NoError(t, err)

// TEST account.
_, tpub := createKey(t)
_, tSigningPub := createKey(t)
accClaim := jwt.NewAccountClaims(tpub)
accClaim.Name = "TEST"
accClaim.SigningKeys.Add(tSigningPub)
scope, scopedKp := newScopedRole(t, "foo", []string{"foo.>", "$SYS.REQ.USER.INFO"}, []string{"foo.>", "_INBOX.>"}, true)
scope.Template.Limits.Subs = 10
scope.Template.Limits.Payload = 512
accClaim.SigningKeys.AddScopedSigner(scope)
accJwt, err := accClaim.Encode(oKp)
require_NoError(t, err)

// AUTH service account.
akp, err := nkeys.FromSeed([]byte(authCalloutIssuerSeed))
require_NoError(t, err)

apub, err := akp.PublicKey()
require_NoError(t, err)

// The authorized user for the service.
upub, creds := createAuthServiceUser(t, akp)
defer removeFile(t, creds)

authClaim := jwt.NewAccountClaims(apub)
authClaim.Name = "AUTH"
authClaim.EnableExternalAuthorization(upub)
if allowAnyAccount {
authClaim.Authorization.AllowedAccounts.Add("*")
} else {
authClaim.Authorization.AllowedAccounts.Add(tpub)
}
// the scope for the bearer token which has no permissions
sentinelScope, authKP := newScopedRole(t, "sentinel", nil, nil, false)
sentinelScope.Template.Sub.Deny.Add(">")
sentinelScope.Template.Pub.Deny.Add(">")
sentinelScope.Template.Limits.Subs = 0
sentinelScope.Template.Payload = 0
authClaim.SigningKeys.AddScopedSigner(sentinelScope)

authJwt, err := authClaim.Encode(oKp)
require_NoError(t, err)

conf := fmt.Sprintf(`
listen: 127.0.0.1:-1
operator: %s
system_account: %s
resolver: MEM
resolver_preload: {
%s: %s
%s: %s
%s: %s
}
`, ojwt, spub, apub, authJwt, tpub, accJwt, spub, sysJwt)

const scopedToken = "--Scoped--"
handler := func(m *nats.Msg) {
user, si, _, opts, _ := decodeAuthRequest(t, m.Data)
if opts.Token == scopedToken {
// must have no limits set
ujwt := createAuthUser(t, user, "scoped", tpub, tpub, scopedKp, 0, &jwt.UserPermissionLimits{})
m.Respond(serviceResponse(t, user, si.ID, ujwt, "", 0))
} else {
m.Respond(nil)
}
}

ac := NewAuthTest(t, conf, handler, nats.UserCredentials(creds))
defer ac.Cleanup()
resp, err := ac.authClient.Request(userDirectInfoSubj, nil, time.Second)
require_NoError(t, err)
response := ServerAPIResponse{Data: &UserInfo{}}
err = json.Unmarshal(resp.Data, &response)
require_NoError(t, err)

userInfo := response.Data.(*UserInfo)
expected := &UserInfo{
UserID: upub,
Account: apub,
Permissions: &Permissions{
Publish: &SubjectPermission{
Deny: []string{AuthCalloutSubject}, // Will be auto-added since in auth account.
},
Subscribe: &SubjectPermission{},
},
}
if !reflect.DeepEqual(expected, userInfo) {
t.Fatalf("User info did not match expected, expected auto-deny permissions on callout subject")
}

// Bearer token - this has no permissions see sentinelScope
// This is used by all users, and the customization will be in other connect args.
// This needs to also be bound to the authorization account.
creds = createScopedUser(t, akp, authKP)
defer removeFile(t, creds)

// Send the signing key token. This should switch us to the test account, but the user
// is signed with the account signing key
nc := ac.Connect(nats.UserCredentials(creds), nats.Token(scopedToken))
require_NoError(t, err)

resp, err = nc.Request(userDirectInfoSubj, nil, time.Second)
require_NoError(t, err)
response = ServerAPIResponse{Data: &UserInfo{}}
err = json.Unmarshal(resp.Data, &response)
require_NoError(t, err)

userInfo = response.Data.(*UserInfo)
if userInfo.Account != tpub {
t.Fatalf("Expected to be switched to %q, but got %q", tpub, userInfo.Account)
}
require_True(t, len(userInfo.Permissions.Publish.Allow) == 2)
sort.Strings(userInfo.Permissions.Publish.Allow)
require_Equal(t, "foo.>", userInfo.Permissions.Publish.Allow[1])
sort.Strings(userInfo.Permissions.Subscribe.Allow)
require_True(t, len(userInfo.Permissions.Subscribe.Allow) == 2)
require_Equal(t, "foo.>", userInfo.Permissions.Subscribe.Allow[1])

_, err = nc.Subscribe("foo.>", func(msg *nats.Msg) {
t.Log("got request on foo.>")
require_NoError(t, msg.Respond(nil))
})
require_NoError(t, err)

m, err := nc.Request("foo.bar", nil, time.Second)
require_NoError(t, err)
require_NotNil(t, m)
t.Log("go response from foo.bar")

nc.Close()
}

func TestAuthCalloutScopedUserAssignedAccount(t *testing.T) {
testAuthCalloutScopedUser(t, false)
}

func TestAuthCalloutScopedUserAllAccount(t *testing.T) {
testAuthCalloutScopedUser(t, true)
}

const (
curveSeed = "SXAAXMRAEP6JWWHNB6IKFL554IE6LZVT6EY5MBRICPILTLOPHAG73I3YX4"
curvePublic = "XAB3NANV3M6N7AHSQP2U5FRWKKUT7EG2ZXXABV4XVXYQRJGM4S2CZGHT"
Expand Down

0 comments on commit 2f8be20

Please sign in to comment.