diff --git a/exports.go b/exports.go index 5578f98..9670442 100644 --- a/exports.go +++ b/exports.go @@ -17,6 +17,7 @@ package jwt import ( "fmt" + "strings" "time" ) @@ -71,13 +72,14 @@ func (sl *ServiceLatency) Validate(vr *ValidationResults) { // Export represents a single export type Export struct { - Name string `json:"name,omitempty"` - Subject Subject `json:"subject,omitempty"` - Type ExportType `json:"type,omitempty"` - TokenReq bool `json:"token_req,omitempty"` - Revocations RevocationList `json:"revocations,omitempty"` - ResponseType ResponseType `json:"response_type,omitempty"` - Latency *ServiceLatency `json:"service_latency,omitempty"` + Name string `json:"name,omitempty"` + Subject Subject `json:"subject,omitempty"` + Type ExportType `json:"type,omitempty"` + TokenReq bool `json:"token_req,omitempty"` + Revocations RevocationList `json:"revocations,omitempty"` + ResponseType ResponseType `json:"response_type,omitempty"` + Latency *ServiceLatency `json:"service_latency,omitempty"` + AccountTokenPosition uint `json:"account_token_position,omitempty"` } // IsService returns true if an export is for a service @@ -124,6 +126,23 @@ func (e *Export) Validate(vr *ValidationResults) { e.Latency.Validate(vr) } e.Subject.Validate(vr) + + if e.AccountTokenPosition > 0 { + if !e.Subject.HasWildCards() { + vr.AddError("Account Token Position can only be used with wildcard subjects: %s", e.Subject) + } else { + subj := string(e.Subject) + token := strings.Split(subj, ".") + tkCnt := uint(len(token)) + if e.AccountTokenPosition > tkCnt { + vr.AddError("Account Token Position %d exceeds length of subject '%s'", + e.AccountTokenPosition, e.Subject) + } else if tk := token[e.AccountTokenPosition-1]; tk != "*" { + vr.AddError("Account Token Position %d matches '%s' but must match a * in: %s", + e.AccountTokenPosition, tk, e.Subject) + } + } + } } // Revoke enters a revocation by publickey using time.Now(). diff --git a/exports_test.go b/exports_test.go index b674c90..5673802 100644 --- a/exports_test.go +++ b/exports_test.go @@ -288,3 +288,73 @@ func TestExport_Sorting(t *testing.T) { t.Fatal("exports not sorted") } } + +func TestExportAccountTokenPos(t *testing.T) { + okp := createOperatorNKey(t) + akp := createAccountNKey(t) + apk := publicKey(akp, t) + tbl := map[Subject]uint{ + "*": 1, + "foo.*": 2, + "foo.*.bar.*": 2, + "foo.*.bar.>": 2, + "*.*.*.>": 2, + "*.*.>": 1, + } + for k, v := range tbl { + t.Run(string(k), func(t *testing.T) { + account := NewAccountClaims(apk) + //account.Limits = OperatorLimits{} + account.Exports = append(account.Exports, + &Export{Type: Stream, Subject: k, AccountTokenPosition: v}) + actJwt := encode(account, okp, t) + account2, err := DecodeAccountClaims(actJwt) + if err != nil { + t.Fatal("error decoding account jwt", err) + } + AssertEquals(account.String(), account2.String(), t) + vr := &ValidationResults{} + account2.Validate(vr) + if len(vr.Issues) != 0 { + t.Fatal("validation issues", *vr) + } + }) + } +} + +func TestExportAccountTokenPosFail(t *testing.T) { + okp := createOperatorNKey(t) + akp := createAccountNKey(t) + apk := publicKey(akp, t) + tbl := map[Subject]uint{ + ">": 5, + "foo.>": 2, + "bar.>": 1, + "*": 5, + "*.*": 5, + "bar": 1, + "foo.bar": 2, + "foo.*.bar": 3, + "*.>": 3, + "*.*.>": 3, + } + for k, v := range tbl { + t.Run(string(k), func(t *testing.T) { + account := NewAccountClaims(apk) + //account.Limits = OperatorLimits{} + account.Exports = append(account.Exports, + &Export{Type: Stream, Subject: k, AccountTokenPosition: v}) + actJwt := encode(account, okp, t) + account2, err := DecodeAccountClaims(actJwt) + if err != nil { + t.Fatal("error decoding account jwt", err) + } + AssertEquals(account.String(), account2.String(), t) + vr := &ValidationResults{} + account2.Validate(vr) + if len(vr.Issues) != 1 { + t.Fatal("validation issue expected", *vr) + } + }) + } +}