diff --git a/account_claims.go b/account_claims.go index 2f8cee8..d79c139 100644 --- a/account_claims.go +++ b/account_claims.go @@ -17,6 +17,7 @@ package jwt import ( "errors" + "fmt" "github.com/nats-io/nkeys" ) @@ -129,6 +130,20 @@ func DecodeAccountClaims(token string) (*AccountClaims, error) { if err := Decode(token, &v); err != nil { return nil, err } + // After decoding validation is complete, migrate imports + for _, i := range v.Imports { + if i.Subject != "" || i.To != "" { + if i.RemoteSubject != "" || i.LocalSubject != "" { + return nil, fmt.Errorf("import [%s] uses subject/to and remote/local - only remote/local or subject/to allowed for reading", i.Name) + } + i.RemoteSubject = i.Subject + i.Subject = "" + i.LocalSubject = i.To + i.To = "" + i.migrated = true + } + } + return &v, nil } @@ -165,3 +180,13 @@ func (a *AccountClaims) ExpectedPrefixes() []nkeys.PrefixByte { func (a *AccountClaims) Claims() *ClaimsData { return &a.ClaimsData } + +// Migrated returns true if the account claim was migrated during a read +func (a *AccountClaims) Migrated() bool { + for _, i := range a.Imports { + if i.migrated { + return true + } + } + return false +} diff --git a/account_claims_test.go b/account_claims_test.go index 4221ce9..86ee01d 100644 --- a/account_claims_test.go +++ b/account_claims_test.go @@ -44,7 +44,7 @@ func TestNewAccountClaims(t *testing.T) { account.Expires = time.Now().Add(time.Duration(time.Hour * 24 * 365)).UTC().Unix() account.Imports = Imports{} - account.Imports.Add(&Import{Subject: "test", Name: "test import", Account: apk2, Token: actJWT, To: "my", Type: Stream}) + account.Imports.Add(&Import{RemoteSubject: "test", Name: "test import", Account: apk2, Token: actJWT, LocalSubject: "my", Type: Stream}) vr := CreateValidationResults() account.Validate(vr) @@ -194,9 +194,6 @@ func TestInvalidAccountSubjects(t *testing.T) { var err error c := NewAccountClaims(pk) - if i.ok && err != nil { - t.Fatalf("error encoding activation: %v", err) - } _, err = c.Encode(i.kp) if i.ok && err != nil { t.Fatal(fmt.Sprintf("unexpected error for %q: %v", i.name, err)) diff --git a/activation_claims.go b/activation_claims.go index a9f2a39..3620808 100644 --- a/activation_claims.go +++ b/activation_claims.go @@ -117,6 +117,11 @@ func (a *ActivationClaims) String() string { return a.ClaimsData.String(a) } +// Migrated returns true if the activation claim was migrated during a read +func (a *ActivationClaims) Migrated() bool { + return false +} + // HashID returns a hash of the claims that can be used to identify it. // The hash is calculated by creating a string with // issuerPubKey.subjectPubKey. and constructing the sha-256 hash and base32 encoding that. diff --git a/claims.go b/claims.go index 45f00da..8f9a145 100644 --- a/claims.go +++ b/claims.go @@ -53,6 +53,7 @@ type Claims interface { Claims() *ClaimsData Encode(kp nkeys.KeyPair) (string, error) ExpectedPrefixes() []nkeys.PrefixByte + Migrated() bool Payload() interface{} String() string Validate(vr *ValidationResults) diff --git a/cluster_claims.go b/cluster_claims.go index d526ba3..dbc631d 100644 --- a/cluster_claims.go +++ b/cluster_claims.go @@ -92,3 +92,8 @@ func (c *ClusterClaims) ExpectedPrefixes() []nkeys.PrefixByte { func (c *ClusterClaims) Claims() *ClaimsData { return &c.ClaimsData } + +// Migrated returns true if the cluster claim was migrated during a read +func (c *ClusterClaims) Migrated() bool { + return false +} diff --git a/exports.go b/exports.go index 344b41a..54b5697 100644 --- a/exports.go +++ b/exports.go @@ -81,7 +81,7 @@ func isContainedIn(kind ExportType, subjects []Subject, vr *ValidationResults) { } // Validate calls validate on all of the exports -func (e *Exports) Validate(vr *ValidationResults) error { +func (e *Exports) Validate(vr *ValidationResults) { var serviceSubjects []Subject var streamSubjects []Subject @@ -96,8 +96,6 @@ func (e *Exports) Validate(vr *ValidationResults) error { isContainedIn(Service, serviceSubjects, vr) isContainedIn(Stream, streamSubjects, vr) - - return nil } // HasExportContainingSubject checks if the export list has an export with the provided subject diff --git a/genericlaims.go b/genericlaims.go index 7098955..bf3eb7a 100644 --- a/genericlaims.go +++ b/genericlaims.go @@ -63,6 +63,12 @@ func (gc *GenericClaims) Validate(vr *ValidationResults) { gc.ClaimsData.Validate(vr) } +// Migrated returns true if the cluster claim was migrated during a read +// GenericClaims will always return false +func (gc *GenericClaims) Migrated() bool { + return false +} + func (gc *GenericClaims) String() string { return gc.ClaimsData.String(gc) } diff --git a/imports.go b/imports.go index c93048b..3737f44 100644 --- a/imports.go +++ b/imports.go @@ -24,12 +24,17 @@ import ( // Import describes a mapping from another account into this one type Import struct { - Name string `json:"name,omitempty"` - Subject Subject `json:"subject,omitempty"` - Account string `json:"account,omitempty"` - Token string `json:"token,omitempty"` - To Subject `json:"to,omitempty"` - Type ExportType `json:"type,omitempty"` + Account string `json:"account,omitempty"` + LocalSubject Subject `json:"local_subject,omitempty"` + Name string `json:"name,omitempty"` + RemoteSubject Subject `json:"remote_subject,omitempty"` + Token string `json:"token,omitempty"` + Type ExportType `json:"type,omitempty"` + // Deprecated: use Local/Remote + Subject Subject `json:"subject,omitempty"` + // Deprecated: use Local/Remote + To Subject `json:"to,omitempty"` + migrated bool } // IsService returns true if the import is of type service @@ -52,34 +57,28 @@ func (i *Import) Validate(actPubKey string, vr *ValidationResults) { vr.AddWarning("account to import from is not specified") } - i.Subject.Validate(vr) - - if i.IsService() { - if i.Subject.HasWildCards() { - vr.AddWarning("services cannot have wildcard subject: %q", i.Subject) - } - } + i.RemoteSubject.Validate(vr) var act *ActivationClaims if i.Token != "" { // Check to see if its an embedded JWT or a URL. - if url, err := url.Parse(i.Token); err == nil && url.Scheme != "" { + if u, err := url.Parse(i.Token); err == nil && u.Scheme != "" { c := &http.Client{Timeout: 5 * time.Second} - resp, err := c.Get(url.String()) + resp, err := c.Get(u.String()) if err != nil { - vr.AddWarning("import %s contains an unreachable token URL %q", i.Subject, i.Token) + vr.AddWarning("import %s contains an unreachable token URL %q", i.RemoteSubject, i.Token) } if resp != nil { defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { - vr.AddWarning("import %s contains an unreadable token URL %q", i.Subject, i.Token) + vr.AddWarning("import %s contains an unreadable token URL %q", i.RemoteSubject, i.Token) } else { act, err = DecodeActivationClaims(string(body)) if err != nil { - vr.AddWarning("import %s contains a url %q with an invalid activation token", i.Subject, i.Token) + vr.AddWarning("import %s contains a url %q with an invalid activation token", i.RemoteSubject, i.Token) } } } @@ -87,21 +86,21 @@ func (i *Import) Validate(actPubKey string, vr *ValidationResults) { var err error act, err = DecodeActivationClaims(i.Token) if err != nil { - vr.AddWarning("import %q contains an invalid activation token", i.Subject) + vr.AddWarning("import %q contains an invalid activation token", i.RemoteSubject) } } } if act != nil { if act.Issuer != i.Account { - vr.AddWarning("activation token doesn't match account for import %q", i.Subject) + vr.AddWarning("activation token doesn't match account for import %q", i.RemoteSubject) } if act.ClaimsData.Subject != actPubKey { - vr.AddWarning("activation token doesn't match account it is being included in, %q", i.Subject) + vr.AddWarning("activation token doesn't match account it is being included in, %q", i.RemoteSubject) } } else { - vr.AddWarning("no activation provided for import %s", i.Subject) + vr.AddWarning("no activation provided for import %s", i.RemoteSubject) } } diff --git a/imports_test.go b/imports_test.go index 8a9bd3a..5fbe487 100644 --- a/imports_test.go +++ b/imports_test.go @@ -28,7 +28,7 @@ func TestImportValidation(t *testing.T) { ak2 := createAccountNKey(t) akp := publicKey(ak, t) akp2 := publicKey(ak2, t) - i := &Import{Subject: "test", Account: akp2, To: "bar", Type: Stream} + i := &Import{RemoteSubject: "test", Account: akp2, LocalSubject: "bar", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) @@ -73,7 +73,7 @@ func TestImportValidation(t *testing.T) { func TestInvalidImportType(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) - i := &Import{Subject: "foo", Account: akp, To: "bar", Type: Unknown} + i := &Import{RemoteSubject: "foo", Account: akp, LocalSubject: "bar", Type: Unknown} vr := CreateValidationResults() i.Validate("", vr) @@ -90,7 +90,7 @@ func TestInvalidImportType(t *testing.T) { func TestInvalidImportToken(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) - i := &Import{Subject: "foo", Account: akp, Token: "bad token", To: "bar", Type: Stream} + i := &Import{RemoteSubject: "foo", Account: akp, Token: "bad token", LocalSubject: "bar", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) @@ -107,7 +107,7 @@ func TestInvalidImportToken(t *testing.T) { func TestInvalidImportURL(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) - i := &Import{Subject: "foo", Account: akp, Token: "foo://bad token url", To: "bar", Type: Stream} + i := &Import{RemoteSubject: "foo", Account: akp, Token: "foo://bad token url", LocalSubject: "bar", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) @@ -126,7 +126,7 @@ func TestInvalidImportTokenValuesValidation(t *testing.T) { ak2 := createAccountNKey(t) akp := publicKey(ak, t) akp2 := publicKey(ak2, t) - i := &Import{Subject: "test", Account: akp2, To: "bar", Type: Stream} + i := &Import{RemoteSubject: "test", Account: akp2, LocalSubject: "bar", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) @@ -187,7 +187,7 @@ func TestInvalidImportTokenValuesValidation(t *testing.T) { } } func TestMissingAccountInImport(t *testing.T) { - i := &Import{Subject: "foo", To: "bar", Type: Stream} + i := &Import{RemoteSubject: "foo", LocalSubject: "bar", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) @@ -204,13 +204,13 @@ func TestMissingAccountInImport(t *testing.T) { func TestServiceImportWithWildcard(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) - i := &Import{Subject: "foo.*", Account: akp, To: "bar", Type: Service} + i := &Import{RemoteSubject: "foo.*", Account: akp, LocalSubject: "bar", Type: Service} vr := CreateValidationResults() i.Validate("", vr) - if len(vr.Issues) != 2 { - t.Errorf("imports without token or url should warn the caller, as should wildcard service") + if len(vr.Issues) != 1 { + t.Errorf("imports without token should warn the caller") } if vr.IsBlocking(true) { @@ -221,8 +221,8 @@ func TestServiceImportWithWildcard(t *testing.T) { func TestImportsValidation(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) - i := &Import{Subject: "foo", Account: akp, To: "bar", Type: Stream} - i2 := &Import{Subject: "foo.*", Account: akp, To: "bar", Type: Service} + i := &Import{RemoteSubject: "foo", Account: akp, LocalSubject: "bar", Type: Stream} + i2 := &Import{RemoteSubject: "foo.*", Account: akp, LocalSubject: "bar", Type: Service} imports := &Imports{} imports.Add(i, i2) @@ -230,8 +230,8 @@ func TestImportsValidation(t *testing.T) { vr := CreateValidationResults() imports.Validate("", vr) - if len(vr.Issues) != 3 { - t.Errorf("imports without token or url should warn the caller x2, wildcard service as well") + if len(vr.Issues) != 2 { + t.Errorf("imports without token or url should warn the caller x2") } if vr.IsBlocking(true) { @@ -244,7 +244,7 @@ func TestTokenURLImportValidation(t *testing.T) { ak2 := createAccountNKey(t) akp := publicKey(ak, t) akp2 := publicKey(ak2, t) - i := &Import{Subject: "test", Account: akp2, To: "bar", Type: Stream} + i := &Import{RemoteSubject: "test", Account: akp2, LocalSubject: "bar", Type: Stream} activation := NewActivationClaims(akp) activation.Max = 1024 * 1024 @@ -255,7 +255,10 @@ func TestTokenURLImportValidation(t *testing.T) { actJWT := encode(activation, ak2, t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(actJWT)) + _, err := w.Write([]byte(actJWT)) + if err != nil { + t.Fatal(err) + } })) defer ts.Close() @@ -277,7 +280,10 @@ func TestTokenURLImportValidation(t *testing.T) { } ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("bad jwt")) + _, err := w.Write([]byte("bad jwt")) + if err != nil { + t.Fatal(err) + } })) defer ts.Close() @@ -314,7 +320,7 @@ func TestImportSubjectValidation(t *testing.T) { ak2 := createAccountNKey(t) akp2 := publicKey(ak2, t) - i := &Import{Subject: "one.two", Account: akp2, To: "bar", Type: Stream} + i := &Import{RemoteSubject: "one.two", Account: akp2, LocalSubject: "bar", Type: Stream} actJWT := encode(activation, ak2, t) i.Token = actJWT @@ -348,3 +354,76 @@ func TestImportSubjectValidation(t *testing.T) { t.Errorf("imports with valid contains subject should be valid") } } + +func TestImport_Migration(t *testing.T) { + ak := createAccountNKey(t) + apk := publicKey(ak, t) + ac := NewAccountClaims(apk) + + a2k := createAccountNKey(t) + a2pk := publicKey(a2k, t) + // Do not update Subject or To + i := &Import{Subject: "foo", Account: a2pk, To: "bar", Type: Service} + ac.Imports.Add(i) + // Do not update Subject or To + i2 := &Import{Subject: "x", Account: a2pk, To: "y", Type: Stream} + ac.Imports.Add(i2) + + token, err := ac.Encode(ak) + if err != nil { + t.Fatal(err) + } + + ac2, err := DecodeAccountClaims(token) + if err != nil { + t.Fatal(err) + } + + if ac2.Migrated() == false { + t.Fatal("account claim should have migrated") + } + + for _, i := range ac2.Imports { + if i.RemoteSubject == "" { + t.Fatal("import 'remote_subject' should be set when old token was loaded") + } + if i.Subject != "" { + t.Fatalf("import 'subject' shouldn't be set when old token was loaded: %s", i.Subject) + } + if i.LocalSubject == "" { + t.Fatal("import 'local_subject' should be set when old token was loaded") + } + if i.To != "" { + t.Fatalf("import 'to' shouldn't be set when old token was loaded: %s", i.To) + } + } + + if ac2.Imports[0].RemoteSubject != "foo" && ac2.Imports[0].LocalSubject != "bar" { + t.Fatal("import remapped subjects don't match expected") + } + if ac2.Imports[1].RemoteSubject != "x" && ac2.Imports[1].LocalSubject != "y" { + t.Fatal("import remapped subjects don't match expected") + } +} + +func TestImport_Mix(t *testing.T) { + ak := createAccountNKey(t) + apk := publicKey(ak, t) + ac := NewAccountClaims(apk) + + a2k := createAccountNKey(t) + a2pk := publicKey(a2k, t) + // Do not update Subject or To + i := &Import{Subject: "foo", Account: a2pk, To: "bar", RemoteSubject: "foo", LocalSubject: "bar", Type: Service} + ac.Imports.Add(i) + + token, err := ac.Encode(ak) + if err != nil { + t.Fatal(err) + } + + _, err = DecodeAccountClaims(token) + if err == nil { + t.Fatal("should have failed decoding") + } +} diff --git a/operator_claims.go b/operator_claims.go index 8eef305..998593f 100644 --- a/operator_claims.go +++ b/operator_claims.go @@ -135,3 +135,8 @@ func (s *OperatorClaims) ExpectedPrefixes() []nkeys.PrefixByte { func (s *OperatorClaims) Claims() *ClaimsData { return &s.ClaimsData } + +// Migrated returns true if the cluster claim was migrated during a read +func (s *OperatorClaims) Migrated() bool { + return false +} diff --git a/revocation_claims.go b/revocation_claims.go index 20f4ad8..98253ce 100644 --- a/revocation_claims.go +++ b/revocation_claims.go @@ -103,3 +103,8 @@ func (rc *RevocationClaims) ExpectedPrefixes() []nkeys.PrefixByte { func (rc *RevocationClaims) Claims() *ClaimsData { return &rc.ClaimsData } + +// Migrated returns true if the revocation claim was migrated during a read +func (rc *RevocationClaims) Migrated() bool { + return false +} diff --git a/server_claims.go b/server_claims.go index 12f6dd7..53a6a90 100644 --- a/server_claims.go +++ b/server_claims.go @@ -92,3 +92,8 @@ func (s *ServerClaims) ExpectedPrefixes() []nkeys.PrefixByte { func (s *ServerClaims) Claims() *ClaimsData { return &s.ClaimsData } + +// Migrated returns true if the server claim was migrated during a read +func (s *ServerClaims) Migrated() bool { + return false +} diff --git a/user_claims.go b/user_claims.go index 7a8e8da..e2be1ab 100644 --- a/user_claims.go +++ b/user_claims.go @@ -88,6 +88,11 @@ func (u *UserClaims) Payload() interface{} { return &u.User } +// Migrated returns true if the user claim was migrated during a read +func (u *UserClaims) Migrated() bool { + return false +} + func (u *UserClaims) String() string { return u.ClaimsData.String(u) }