diff --git a/server/auth.go b/server/auth.go index d2295c39..72d1fc0b 100644 --- a/server/auth.go +++ b/server/auth.go @@ -371,6 +371,161 @@ func (c *client) matchesPinnedCert(tlsPinnedCerts PinnedCertSet) bool { return true } +func processUserPermissionsTemplate(lim jwt.UserPermissionLimits, ujwt *jwt.UserClaims, acc *Account) (jwt.UserPermissionLimits, error) { + nArrayCartesianProduct := func(a ...[]string) [][]string { + c := 1 + for _, a := range a { + c *= len(a) + } + if c == 0 { + return nil + } + p := make([][]string, c) + b := make([]string, c*len(a)) + n := make([]int, len(a)) + s := 0 + for i := range p { + e := s + len(a) + pi := b[s:e] + p[i] = pi + s = e + for j, n := range n { + pi[j] = a[j][n] + } + for j := len(n) - 1; j >= 0; j-- { + n[j]++ + if n[j] < len(a[j]) { + break + } + n[j] = 0 + } + } + return p + } + applyTemplate := func(list jwt.StringList, failOnBadSubject bool) (jwt.StringList, error) { + found := false + FOR_FIND: + for i := 0; i < len(list); i++ { + // check if templates are present + for _, tk := range strings.Split(list[i], tsep) { + if strings.HasPrefix(tk, "{{") && strings.HasSuffix(tk, "}}") { + found = true + break FOR_FIND + } + } + } + if !found { + return list, nil + } + // process the templates + emittedList := make([]string, 0, len(list)) + for i := 0; i < len(list); i++ { + tokens := strings.Split(list[i], tsep) + + newTokens := make([]string, len(tokens)) + tagValues := map[int][]string{} // indexed by token + + for tokenNum, tk := range tokens { + if strings.HasPrefix(tk, "{{") && strings.HasSuffix(tk, "}}") { + op := strings.ToLower(strings.TrimSuffix(strings.TrimPrefix(tk, "{{"), "}}")) + switch { + case op == "name()": + tk = ujwt.Name + case op == "subject()": + tk = ujwt.Subject + case op == "account-name()": + acc.mu.RLock() + name := acc.nameTag + acc.mu.RUnlock() + tk = name + case op == "account-subject()": + tk = ujwt.IssuerAccount + case (strings.HasPrefix(op, "tag(") || strings.HasPrefix(op, "account-tag(")) && + strings.HasSuffix(op, ")"): + // insert dummy tav value that will throw of subject validation (in case nothing is found) + tk = _EMPTY_ + // collect list of matching tag values + + var tags jwt.TagList + var tagPrefix string + if strings.HasPrefix(op, "account-tag(") { + acc.mu.RLock() + tags = acc.tags + acc.mu.RUnlock() + tagPrefix = fmt.Sprintf("%s:", strings.ToLower( + strings.TrimSuffix(strings.TrimPrefix(op, "account-tag("), ")"))) + } else { + tags = ujwt.Tags + tagPrefix = fmt.Sprintf("%s:", strings.ToLower( + strings.TrimSuffix(strings.TrimPrefix(op, "tag("), ")"))) + } + + for _, tag := range tags { + if strings.HasPrefix(tag, tagPrefix) { + tagValue := strings.TrimPrefix(tag, tagPrefix) + tagValues[tokenNum] = append(tagValues[tokenNum], tagValue) + } + } + default: + // if macro is not recognized, throw off subject check on purpose + tk = " " + } + } + newTokens[tokenNum] = tk + } + // fill in tag value placeholders + if len(tagValues) == 0 { + emitSubj := strings.Join(newTokens, tsep) + if IsValidSubject(emitSubj) { + emittedList = append(emittedList, emitSubj) + } else if failOnBadSubject { + return nil, fmt.Errorf("generated invalid subject") + } + // else skip emitting + } else { + orderedList := make([][]string, 0, len(tagValues)) + for _, valueList := range tagValues { + orderedList = append(orderedList, valueList) + } + // compute the cartesian product and compute subject to emit for each combination + for _, valueList := range nArrayCartesianProduct(orderedList...) { + b := strings.Builder{} + for i, token := range newTokens { + if token == _EMPTY_ { + b.WriteString(valueList[0]) + valueList = valueList[1:] + } else { + b.WriteString(token) + } + if i != len(newTokens)-1 { + b.WriteString(tsep) + } + } + emitSubj := b.String() + if IsValidSubject(emitSubj) { + emittedList = append(emittedList, emitSubj) + } else if failOnBadSubject { + return nil, fmt.Errorf("generated invalid subject") + } + // else skip emitting + } + } + } + return emittedList, nil + } + var err error + if lim.Permissions.Sub.Allow, err = applyTemplate(lim.Permissions.Sub.Allow, false); err != nil { + return jwt.UserPermissionLimits{}, err + } else if lim.Permissions.Sub.Deny, err = applyTemplate(lim.Permissions.Sub.Deny, true); err != nil { + return jwt.UserPermissionLimits{}, err + } else if lim.Permissions.Pub.Allow, err = applyTemplate(lim.Permissions.Pub.Allow, false); err != nil { + return jwt.UserPermissionLimits{}, err + } else if lim.Permissions.Pub.Deny, err = applyTemplate(lim.Permissions.Pub.Deny, true); err != nil { + return jwt.UserPermissionLimits{}, err + } + return lim, nil +} + func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) bool { var ( nkey *NkeyUser @@ -621,8 +776,9 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo } else if uSc, ok := scope.(*jwt.UserScope); !ok { c.Debugf("User JWT is not valid") return false - } else { - juc.UserPermissionLimits = uSc.Template + } else if juc.UserPermissionLimits, err = processUserPermissionsTemplate(uSc.Template, juc, acc); err != nil { + c.Debugf("User JWT is not valid") + return false } } if acc.IsExpired() { diff --git a/server/jwt_test.go b/server/jwt_test.go index 561b7e95..189ab22d 100644 --- a/server/jwt_test.go +++ b/server/jwt_test.go @@ -4163,6 +4163,119 @@ func TestJWTLimits(t *testing.T) { }) } +func TestJwtTemplates(t *testing.T) { + kp, _ := nkeys.CreateAccount() + aPub, _ := kp.PublicKey() + ukp, _ := nkeys.CreateUser() + upub, _ := ukp.PublicKey() + uclaim := newJWTTestUserClaims() + uclaim.Name = "myname" + uclaim.Subject = upub + uclaim.SetScoped(true) + uclaim.IssuerAccount = aPub + uclaim.Tags.Add("foo:foo1") + uclaim.Tags.Add("foo:foo2") + uclaim.Tags.Add("bar:bar1") + uclaim.Tags.Add("bar:bar2") + uclaim.Tags.Add("bar:bar3") + + lim := jwt.UserPermissionLimits{} + lim.Pub.Allow.Add("{{tag(foo)}}.none.{{tag(bar)}}") + lim.Pub.Deny.Add("{{tag(foo)}}.{{account-tag(acc)}}") + lim.Sub.Allow.Add("{{tag(NOT_THERE)}}") // expect to not emit this + lim.Sub.Deny.Add("foo.{{name()}}.{{subject()}}.{{account-name()}}.{{account-subject()}}.bar") + acc := &Account{nameTag: "accname", tags: []string{"acc:acc1", "acc:acc2"}} + + resLim, err := processUserPermissionsTemplate(lim, uclaim, acc) + require_NoError(t, err) + + test := func(expectedSubjects []string, res jwt.StringList) { + t.Helper() + require_True(t, len(res) == len(expectedSubjects)) + for _, expetedSubj := range expectedSubjects { + require_True(t, res.Contains(expetedSubj)) + } + } + + test(resLim.Pub.Allow, []string{"foo1.none.bar1", "foo1.none.bar2", "foo1.none.bar3", + "foo2.none.bar1", "foo2.none.bar2", "foo2.none.bar3"}) + + test(resLim.Pub.Deny, []string{"foo1.acc1", "foo1.acc2", "foo2.acc1", "foo2.acc2"}) + + require_True(t, len(resLim.Sub.Allow) == 0) + require_True(t, len(resLim.Sub.Deny) == 1) + require_Contains(t, resLim.Sub.Deny[0], fmt.Sprintf("foo.myname.%s.accname.%s.bar", upub, aPub)) +} + +func TestJWTLimitsTemplate(t *testing.T) { + kp, _ := nkeys.CreateAccount() + aPub, _ := kp.PublicKey() + claim := jwt.NewAccountClaims(aPub) + aSignScopedKp, aSignScopedPub := createKey(t) + signer := jwt.NewUserScope() + signer.Key = aSignScopedPub + signer.Template.Pub.Deny.Add("denied") + signer.Template.Pub.Allow.Add("foo.{{name()}}") + signer.Template.Sub.Allow.Add("foo.{{name()}}") + claim.SigningKeys.AddScopedSigner(signer) + aJwt, err := claim.Encode(oKp) + require_NoError(t, err) + conf := createConfFile(t, []byte(fmt.Sprintf(` + listen: 127.0.0.1:-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) + + ukp, _ := nkeys.CreateUser() + seed, _ := ukp.Seed() + upub, _ := ukp.PublicKey() + uclaim := newJWTTestUserClaims() + uclaim.Name = "myname" + uclaim.Subject = upub + uclaim.SetScoped(true) + uclaim.IssuerAccount = aPub + + ujwt, err := uclaim.Encode(aSignScopedKp) + require_NoError(t, err) + creds := genCredsFile(t, ujwt, seed) + + defer removeFile(t, creds) + + t.Run("pass", func(t *testing.T) { + c := natsConnect(t, sA.ClientURL(), nats.UserCredentials(creds)) + defer c.Close() + sub, err := c.SubscribeSync("foo.myname") + require_NoError(t, err) + require_NoError(t, c.Flush()) + require_NoError(t, c.Publish("foo.myname", nil)) + _, err = sub.NextMsg(time.Second) + require_NoError(t, err) + }) + t.Run("fail", func(t *testing.T) { + c := natsConnect(t, sA.ClientURL(), nats.UserCredentials(creds), + nats.ErrorHandler(func(_ *nats.Conn, _ *nats.Subscription, err error) { + if strings.Contains(err.Error(), `nats: Permissions Violation for Publish to "foo.othername"`) { + errChan <- struct{}{} + } + })) + defer c.Close() + require_NoError(t, c.Publish("foo.othername", nil)) + select { + case <-errChan: + case <-time.After(time.Second * 2): + require_True(t, false) + } + }) +} + func TestJWTNoOperatorMode(t *testing.T) { for _, login := range []bool{true, false} { t.Run("", func(t *testing.T) {