Skip to content

Commit 0098b9f

Browse files
fix: allow import of same service subject from different accounts
nats-server already allows importing the same (service) subject from different account, while the jwt does not. This becomes an issue when you, for instance, source JetStreams from different accounts and need to import service `$JS.FC.>` from both. Before this could be solved by importing e.g. `$JS.FC.STREAM_FROM_ACC_X.>` and `$JS.FC.STREAM_FROM_ACC_Y.>`, but this requires globally unique stream names. If those streams are both called `ORDERS` then you wouldn't be able to source both. Signed-off-by: Thobias Karlsson <thobias.karlsson@gmail.com>
1 parent a2c583b commit 0098b9f

File tree

2 files changed

+124
-7
lines changed

2 files changed

+124
-7
lines changed

v2/imports.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ type Imports []*Import
126126

127127
// Validate checks if an import is valid for the wrapping account
128128
func (i *Imports) Validate(acctPubKey string, vr *ValidationResults) {
129-
toSet := make(map[Subject]struct{}, len(*i))
129+
// Group subjects by account to check for overlaps only within the same account
130+
subjectsByAccount := make(map[string]map[Subject]struct{}, len(*i))
130131
for _, v := range *i {
131132
if v == nil {
132133
vr.AddError("null import is not allowed")
@@ -140,15 +141,19 @@ func (i *Imports) Validate(acctPubKey string, vr *ValidationResults) {
140141
if sub == "" {
141142
sub = v.Subject
142143
}
143-
for k := range toSet {
144-
if sub.IsContainedIn(k) || k.IsContainedIn(sub) {
145-
vr.AddError("overlapping subject namespace for %q and %q", sub, k)
144+
// Check for overlapping subjects only within the same account
145+
for subOther := range subjectsByAccount[v.Account] {
146+
if sub.IsContainedIn(subOther) || subOther.IsContainedIn(sub) {
147+
vr.AddError("overlapping subject namespace for %q and %q in same account %q", sub, subOther, v.Account)
146148
}
147149
}
148-
if _, ok := toSet[sub]; ok {
149-
vr.AddError("overlapping subject namespace for %q", v.To)
150+
if subjectsByAccount[v.Account] == nil {
151+
subjectsByAccount[v.Account] = make(map[Subject]struct{}, len(*i))
150152
}
151-
toSet[sub] = struct{}{}
153+
if _, ok := subjectsByAccount[v.Account][sub]; ok {
154+
vr.AddError("overlapping subject namespace for %q in account %q", sub, v.Account)
155+
}
156+
subjectsByAccount[v.Account][sub] = struct{}{}
152157
}
153158
v.Validate(acctPubKey, vr)
154159
}

v2/imports_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,118 @@ func TestImports_Validate(t *testing.T) {
499499
}
500500
}
501501

502+
func TestImports_Validate_ServiceConflicts(t *testing.T) {
503+
accountA := publicKey(createAccountNKey(t), t)
504+
accountB := publicKey(createAccountNKey(t), t)
505+
506+
testCases := []struct {
507+
test string
508+
imports []*Import
509+
valid bool
510+
}{
511+
{
512+
test: "Same subject from same account",
513+
imports: []*Import{
514+
{Account: accountA, Subject: "order.create", Type: Service},
515+
{Account: accountA, Subject: "order.create", Type: Service},
516+
},
517+
valid: false,
518+
},
519+
{
520+
test: "Same subject from same account, to same local subject",
521+
imports: []*Import{
522+
{Account: accountA, Subject: "order.create", LocalSubject: "external.order.create", Type: Service},
523+
{Account: accountA, Subject: "order.create", LocalSubject: "external.order.create", Type: Service},
524+
},
525+
valid: false,
526+
},
527+
{
528+
test: "Same subject from same account, to same local subject (using To)",
529+
imports: []*Import{
530+
{Account: accountA, Subject: "order.create", To: "external.order.create", Type: Service},
531+
{Account: accountA, Subject: "order.create", To: "external.order.create", Type: Service},
532+
},
533+
valid: false,
534+
},
535+
{
536+
test: "Same subject from same account, to same local subject (using To and LocalSubject)",
537+
imports: []*Import{
538+
{Account: accountA, Subject: "order.create", To: "external.order.create", Type: Service},
539+
{Account: accountA, Subject: "order.create", LocalSubject: "external.order.create", Type: Service},
540+
},
541+
valid: false,
542+
},
543+
{
544+
test: "Same subject from same account, but different local subject",
545+
imports: []*Import{
546+
{Account: accountA, Subject: "order.create", LocalSubject: "service-a.order.create", Type: Service},
547+
{Account: accountA, Subject: "order.create", LocalSubject: "service-b.order.create", Type: Service},
548+
},
549+
valid: true,
550+
},
551+
{
552+
test: "Different subjects from same account, to same local subject",
553+
imports: []*Import{
554+
{Account: accountA, Subject: "service-a.ping", LocalSubject: "deps.ping", Type: Service},
555+
{Account: accountA, Subject: "service-b.ping", LocalSubject: "deps.ping", Type: Service},
556+
},
557+
valid: false, // nats-server does not allow this today, hence it should be rejected here as well.
558+
},
559+
{
560+
test: "Same subject from different accounts",
561+
imports: []*Import{
562+
{Account: accountA, Subject: "$JS.FC.>", Type: Service},
563+
{Account: accountB, Subject: "$JS.FC.>", Type: Service},
564+
},
565+
valid: true,
566+
},
567+
{
568+
test: "Same subject from different accounts, to same local subject",
569+
imports: []*Import{
570+
{Account: accountA, Subject: "order.create", LocalSubject: "external.order.create", Type: Service},
571+
{Account: accountB, Subject: "order.create", LocalSubject: "external.order.create", Type: Service},
572+
},
573+
valid: true,
574+
},
575+
{
576+
test: "Same subject from different accounts, but different local subject",
577+
imports: []*Import{
578+
{Account: accountA, Subject: "order.create", LocalSubject: "external.a.order.create", Type: Service},
579+
{Account: accountB, Subject: "order.create", LocalSubject: "external.b.order.create", Type: Service},
580+
},
581+
valid: true,
582+
},
583+
{
584+
test: "Different subjects from different accounts, to same local subject",
585+
imports: []*Import{
586+
{Account: accountA, Subject: "service-a.ping", LocalSubject: "deps.ping", Type: Service},
587+
{Account: accountB, Subject: "service-a.ping", LocalSubject: "deps.ping", Type: Service},
588+
},
589+
valid: true,
590+
},
591+
}
592+
593+
for _, testCase := range testCases {
594+
t.Run(testCase.test, func(t *testing.T) {
595+
// Given
596+
var imports Imports
597+
imports.Add(testCase.imports...)
598+
vr := ValidationResults{}
599+
600+
// When
601+
imports.Validate("", &vr)
602+
603+
// Then
604+
valid := !vr.IsBlocking(true)
605+
if testCase.valid && !valid {
606+
t.Fatalf("expected valid, got: %+v", vr.Issues)
607+
} else if !testCase.valid && valid {
608+
t.Fatalf("expected error, got no blocking validation issues")
609+
}
610+
})
611+
}
612+
}
613+
502614
func TestImportAllowTrace(t *testing.T) {
503615
ak2 := createAccountNKey(t)
504616
akp2 := publicKey(ak2, t)

0 commit comments

Comments
 (0)