From cc8884824de22d043e9fe3d439389d121adf4cfe Mon Sep 17 00:00:00 2001 From: Thomas Limoncelli Date: Mon, 24 Nov 2025 19:30:50 -0500 Subject: [PATCH] permit works --- commands/ppreviewPush_test.go | 8 +-- models/domain.go | 4 +- pkg/domaintags/domaintags.go | 39 ++++++++++++--- pkg/domaintags/domaintags_test.go | 43 ++++++++++------ pkg/domaintags/permitlist.go | 72 ++++++++++++++++----------- pkg/domaintags/permitlist_test.go | 26 ++++++++-- pkg/normalize/importTransform_test.go | 4 ++ 7 files changed, 137 insertions(+), 59 deletions(-) diff --git a/commands/ppreviewPush_test.go b/commands/ppreviewPush_test.go index aa2fea5de..8edf7f6ff 100644 --- a/commands/ppreviewPush_test.go +++ b/commands/ppreviewPush_test.go @@ -22,9 +22,10 @@ func Test_whichZonesToProcess(t *testing.T) { dcTaggedEmpty, } - // for _, dc := range allDC { - // dc.UpdateSplitHorizonNames() - // } + for _, dc := range allDC { + //dc.UpdateSplitHorizonNames() + dc.PostProcess() + } type args struct { dc []*models.DomainConfig @@ -158,6 +159,7 @@ func Test_whichZonesToProcess(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Logf("whichZonesToProcess() %s filter=%v", tt.name, tt.args.filter) got := whichZonesToProcess(tt.args.dc, tt.args.filter) if len(got) != len(tt.want) { t.Errorf("whichZonesToProcess() %s: %s", tt.name, tt.why) diff --git a/models/domain.go b/models/domain.go index 99f699016..47ca73870 100644 --- a/models/domain.go +++ b/models/domain.go @@ -69,7 +69,8 @@ func (dc *DomainConfig) PostProcess() { } // Turn the user-supplied name into the fixed forms. - dc.Tag, dc.NameRaw, dc.Name, dc.NameUnicode, dc.UniqueName = domaintags.MakeDomainFixForms(dc.Name) + ff := domaintags.MakeDomainFixForms(dc.Name) + dc.Tag, dc.NameRaw, dc.Name, dc.NameUnicode, dc.UniqueName = ff.Tag, ff.NameRaw, ff.NameIDN, ff.NameUnicode, ff.UniqueName // Store the split horizon info in metadata for backward compatibility. dc.Metadata[DomainTag] = dc.Tag @@ -90,7 +91,6 @@ func (dc *DomainConfig) GetUniqueName() (uniquename string) { // // (uniquename and tag) based on name. // func (dc *DomainConfig) UpdateSplitHorizonNames() { -// // This should probably be done elsewhere (maybe where we first ingest a domain). // // Convert all domain names to punycode. // for _, domain := range config.Domains { diff --git a/pkg/domaintags/domaintags.go b/pkg/domaintags/domaintags.go index e88ac2af5..4f5416874 100644 --- a/pkg/domaintags/domaintags.go +++ b/pkg/domaintags/domaintags.go @@ -6,20 +6,36 @@ import ( "golang.org/x/net/idna" ) +// DomainFixedForms stores the various fixed forms of a domain name and tag. +type DomainFixedForms struct { + NameRaw string // "originalinput.com" (name as input by the user, lowercased (no tag)) + NameIDN string // "punycode.com" + NameUnicode string // "unicode.com" + UniqueName string // "punycode.com!tag" + + Tag string // The tag portion of `example.com!tag` + HasBang bool // Was there a "!" in the input when creating this struct? +} + // MakeDomainFixedForms turns the user-supplied name into the fixed forms. // * .Tag: the domain tag (of "example.com!tag") // * .NameRaw: lowercase version of how the user input the name in dnsconfig.js. // * .Name: punycode version, downcased. // * .NameUnicode: unicode version of the name, downcased. // * .UniqueName: "example.com!tag" unique across the entire config. -func MakeDomainFixForms(n string) (tag, nameRaw, nameIDN, nameUnicode, UniqueName string) { +func MakeDomainFixForms(n string) DomainFixedForms { var err error + var tag, nameRaw, nameIDN, nameUnicode, uniqueName string + var hasBang bool + // Split tag from name. p := strings.SplitN(n, "!", 2) - if len(p) != 2 { - tag = "" - } else { + if len(p) == 2 { tag = p[1] + hasBang = true + } else { + tag = "" + hasBang = false } nameRaw = strings.ToLower(p[0]) @@ -48,7 +64,18 @@ func MakeDomainFixForms(n string) (tag, nameRaw, nameIDN, nameUnicode, UniqueNam } } - UniqueName = nameIDN + "!" + tag + if hasBang { + uniqueName = nameIDN + "!" + tag + } else { + uniqueName = nameIDN + } - return + return DomainFixedForms{ + Tag: tag, + NameRaw: nameRaw, + NameIDN: nameIDN, + NameUnicode: nameUnicode, + UniqueName: uniqueName, + HasBang: hasBang, + } } diff --git a/pkg/domaintags/domaintags_test.go b/pkg/domaintags/domaintags_test.go index 13b846b3d..f6c010c33 100644 --- a/pkg/domaintags/domaintags_test.go +++ b/pkg/domaintags/domaintags_test.go @@ -13,6 +13,7 @@ func Test_MakeDomainFixForms(t *testing.T) { wantNameIDN string wantNameUnicode string wantUniqueName string + wantHasBang bool }{ { name: "simple domain", @@ -21,7 +22,8 @@ func Test_MakeDomainFixForms(t *testing.T) { wantNameRaw: "example.com", wantNameIDN: "example.com", wantNameUnicode: "example.com", - wantUniqueName: "example.com!", + wantUniqueName: "example.com", + wantHasBang: false, }, { name: "domain with tag", @@ -31,6 +33,7 @@ func Test_MakeDomainFixForms(t *testing.T) { wantNameIDN: "example.com", wantNameUnicode: "example.com", wantUniqueName: "example.com!mytag", + wantHasBang: true, }, { name: "domain with empty tag", @@ -40,6 +43,7 @@ func Test_MakeDomainFixForms(t *testing.T) { wantNameIDN: "example.com", wantNameUnicode: "example.com", wantUniqueName: "example.com!", + wantHasBang: true, }, { name: "unicode domain", @@ -48,7 +52,8 @@ func Test_MakeDomainFixForms(t *testing.T) { wantNameRaw: "उदाहरण.com", wantNameIDN: "xn--p1b6ci4b4b3a.com", wantNameUnicode: "उदाहरण.com", - wantUniqueName: "xn--p1b6ci4b4b3a.com!", + wantUniqueName: "xn--p1b6ci4b4b3a.com", + wantHasBang: false, }, { name: "unicode domain with tag", @@ -58,6 +63,7 @@ func Test_MakeDomainFixForms(t *testing.T) { wantNameIDN: "xn--p1b6ci4b4b3a.com", wantNameUnicode: "उदाहरण.com", wantUniqueName: "xn--p1b6ci4b4b3a.com!mytag", + wantHasBang: true, }, { name: "punycode domain", @@ -66,7 +72,8 @@ func Test_MakeDomainFixForms(t *testing.T) { wantNameRaw: "xn--p1b6ci4b4b3a.com", wantNameIDN: "xn--p1b6ci4b4b3a.com", wantNameUnicode: "उदाहरण.com", - wantUniqueName: "xn--p1b6ci4b4b3a.com!", + wantUniqueName: "xn--p1b6ci4b4b3a.com", + wantHasBang: false, }, { name: "punycode domain with tag", @@ -76,6 +83,7 @@ func Test_MakeDomainFixForms(t *testing.T) { wantNameIDN: "xn--p1b6ci4b4b3a.com", wantNameUnicode: "उदाहरण.com", wantUniqueName: "xn--p1b6ci4b4b3a.com!mytag", + wantHasBang: true, }, { name: "mixed case domain", @@ -84,7 +92,8 @@ func Test_MakeDomainFixForms(t *testing.T) { wantNameRaw: "example.com", wantNameIDN: "example.com", wantNameUnicode: "example.com", - wantUniqueName: "example.com!", + wantUniqueName: "example.com", + wantHasBang: false, }, { name: "mixed case domain with tag", @@ -94,26 +103,30 @@ func Test_MakeDomainFixForms(t *testing.T) { wantNameIDN: "example.com", wantNameUnicode: "example.com", wantUniqueName: "example.com!MyTag", + wantHasBang: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotTag, gotNameRaw, gotNameIDN, gotNameUnicode, gotUniqueName := MakeDomainFixForms(tt.input) - if gotTag != tt.wantTag { - t.Errorf("MakeDomainFixForms() gotTag = %v, want %v", gotTag, tt.wantTag) + got := MakeDomainFixForms(tt.input) + if got.Tag != tt.wantTag { + t.Errorf("MakeDomainFixForms() gotTag = %v, want %v", got.Tag, tt.wantTag) } - if gotNameRaw != tt.wantNameRaw { - t.Errorf("MakeDomainFixForms() gotNameRaw = %v, want %v", gotNameRaw, tt.wantNameRaw) + if got.NameRaw != tt.wantNameRaw { + t.Errorf("MakeDomainFixForms() gotNameRaw = %v, want %v", got.NameRaw, tt.wantNameRaw) } - if gotNameIDN != tt.wantNameIDN { - t.Errorf("MakeDomainFixForms() gotNameIDN = %v, want %v", gotNameIDN, tt.wantNameIDN) + if got.NameIDN != tt.wantNameIDN { + t.Errorf("MakeDomainFixForms() gotNameIDN = %v, want %v", got.NameIDN, tt.wantNameIDN) } - if gotNameUnicode != tt.wantNameUnicode { - t.Errorf("MakeDomainFixForms() gotNameUnicode = %v, want %v", gotNameUnicode, tt.wantNameUnicode) + if got.NameUnicode != tt.wantNameUnicode { + t.Errorf("MakeDomainFixForms() gotNameUnicode = %v, want %v", got.NameUnicode, tt.wantNameUnicode) } - if gotUniqueName != tt.wantUniqueName { - t.Errorf("MakeDomainFixForms() gotUniqueName = %v, want %v", gotUniqueName, tt.wantUniqueName) + if got.UniqueName != tt.wantUniqueName { + t.Errorf("MakeDomainFixForms() gotUniqueName = %v, want %v", got.UniqueName, tt.wantUniqueName) + } + if got.HasBang != tt.wantHasBang { + t.Errorf("MakeDomainFixForms() gotHasTag = %v, want %v", got.HasBang, tt.wantHasBang) } }) } diff --git a/pkg/domaintags/permitlist.go b/pkg/domaintags/permitlist.go index 566249bca..2d0e1f03a 100644 --- a/pkg/domaintags/permitlist.go +++ b/pkg/domaintags/permitlist.go @@ -1,20 +1,22 @@ package domaintags -import "strings" +import ( + "strings" +) type PermitList struct { // If the permit list is "all" or "". all bool - items []permitListItem -} -type permitListItem struct { - tag, nameRaw, nameIDN, nameUnicode, uniqueName string + items []DomainFixedForms } // CompilePermitList compiles a list of domain strings into a PermitList structure. The func CompilePermitList(s string) PermitList { + //fmt.Printf("DEBUG: CompilePermitList(%q)\n", s) + s = strings.TrimSpace(s) - if s == "" || strings.ToLower(s) == "all" { + if s == "" || s == "*" || strings.ToLower(s) == "all" { + //fmt.Printf("DEBUG: CompilePermitList: ALL\n") return PermitList{all: true} } @@ -24,58 +26,72 @@ func CompilePermitList(s string) PermitList { if l == "" { // Skip empty entries. They match nothing. continue } - tag, nameRaw, nameIDN, nameUnicode, uniqueName := MakeDomainFixForms(l) - if tag == "" { // Treat empty tag as wildcard. - tag = "*" + ff := MakeDomainFixForms(l) + if ff.HasBang && ff.NameIDN == "" { // Treat empty name as wildcard. + ff.NameIDN = "*" } - if nameIDN == "" { // Treat empty name as wildcard. - nameIDN = "*" - } - sl.items = append(sl.items, permitListItem{ - tag: tag, - nameRaw: nameRaw, - nameIDN: nameIDN, - nameUnicode: nameUnicode, - uniqueName: uniqueName, - }) + sl.items = append(sl.items, ff) } + //fmt.Printf("DEBUG: CompilePermitList: RETURN %+v\n", sl) return sl } -func (pl *PermitList) Permitted(u string) bool { +func (pl *PermitList) Permitted(domToCheck string) bool { + //fmt.Printf("DEBUG: Permitted(%q)\n", domToCheck) + // If the permit list is "all", everything is permitted. if pl.all { + //fmt.Printf("DEBUG: Permitted RETURN true\n") return true } - tag, _, nameIDN, nameUnicode, _ := MakeDomainFixForms(u) + domToCheckFF := MakeDomainFixForms(domToCheck) + // fmt.Printf("DEBUG: input: %+v\n", domToCheckFF) + + for _, filterItem := range pl.items { + // fmt.Printf("DEBUG: Checking item %+v\n", filterItem) + + // Special case: filter=example.com!* does not match example.com (no tag) + if filterItem.Tag == "*" && !domToCheckFF.HasBang { + // fmt.Printf("DEBUG: Skipping due to no tag present\n") + continue + } + // Special case: filter=example.com!* does not match example.com! (empty tag) + if filterItem.Tag == "*" && domToCheckFF.HasBang && domToCheckFF.Tag == "" { + // fmt.Printf("DEBUG: Skipping due to empty tag present\n") + continue + } + // Special case: filter=example.com! does not match example.com!tag + if filterItem.HasBang && filterItem.Tag == "" && domToCheckFF.HasBang && domToCheckFF.Tag != "" { + // fmt.Printf("DEBUG: Skipping due to non-empty tag present\n") + continue + } - for _, item := range pl.items { // Skip if the tag doesn't match - if item.tag != "*" && tag != item.tag { + if (filterItem.Tag != "*") && (domToCheckFF.Tag != filterItem.Tag) { continue } // Now that we know the tag matches, we can focus on the name. - if item.nameIDN == "*" { + if filterItem.NameIDN == "*" { // `*!tag` or `*` matches everything. return true } // If the name starts with "*." then match the suffix. - if strings.HasPrefix(item.nameIDN, "*.") { + if strings.HasPrefix(filterItem.NameIDN, "*.") { // example.com matches *.example.com - if nameIDN == item.nameIDN[2:] || nameUnicode == item.nameUnicode[2:] { + if domToCheckFF.NameIDN == filterItem.NameIDN[2:] || domToCheckFF.NameUnicode == filterItem.NameUnicode[2:] { return true } // foo.example.com matches *.example.com - if strings.HasSuffix(nameIDN, item.nameIDN[1:]) || strings.HasSuffix(nameUnicode, item.nameUnicode[1:]) { + if strings.HasSuffix(domToCheckFF.NameIDN, filterItem.NameIDN[1:]) || strings.HasSuffix(domToCheckFF.NameUnicode, filterItem.NameUnicode[1:]) { return true } } // No wildcards? Exact match. - if item.nameIDN == nameIDN || item.nameUnicode == nameUnicode { + if filterItem.NameIDN == domToCheckFF.NameIDN || filterItem.NameUnicode == domToCheckFF.NameUnicode { return true } } diff --git a/pkg/domaintags/permitlist_test.go b/pkg/domaintags/permitlist_test.go index 996c0959f..d6845a50b 100644 --- a/pkg/domaintags/permitlist_test.go +++ b/pkg/domaintags/permitlist_test.go @@ -39,7 +39,7 @@ func TestPermitList_Permitted(t *testing.T) { // Wildcard tag {"wildcard tag matches", "example.com!*", "example.com!tag1", true}, - {"wildcard tag matches no tag", "example.com!*", "example.com", true}, + {"wildcard tag matches no tag", "example.com!*", "example.com", false}, {"wildcard tag mismatch domain", "example.com!*", "google.com!tag1", false}, // Suffix matching @@ -66,12 +66,12 @@ func TestPermitList_Permitted(t *testing.T) { // IDN/Unicode cases (assuming MakeDomainFixForms works) {"IDN exact match punycode", "xn--e1a4c.com", "xn--e1a4c.com", true}, // д.com {"IDN exact match unicode", "д.com", "д.com", true}, - {"IDN mixed match", "xn--e1a4c.com", "д.com", true}, - {"IDN mixed match reversed", "д.com", "xn--e1a4c.com", true}, + {"IDN mixed match", "xn--d1a.com", "д.com", true}, + {"IDN mixed match reversed", "д.com", "xn--d1a.com", true}, {"IDN suffix match punycode", "*.xn--e1a4c.com", "sub.xn--e1a4c.com", true}, {"IDN suffix match unicode", "*.д.com", "sub.д.com", true}, - {"IDN suffix match mixed", "*.xn--e1a4c.com", "sub.д.com", true}, - {"IDN suffix match mixed reversed", "*.д.com", "sub.xn--e1a4c.com", true}, + {"IDN suffix match mixed", "*.xn--d1a.com", "sub.д.com", true}, + {"IDN suffix match mixed reversed", "*.д.com", "sub.xn--d1a.com", true}, {"IDN suffix match base", "*.д.com", "д.com", true}, // Edge cases @@ -80,6 +80,22 @@ func TestPermitList_Permitted(t *testing.T) { {"list with empty items 2", "one.com,,two.com", "two.com", true}, {"list with empty items no match", "one.com,,two.com", "three.com", false}, {"no match on empty list", "nonexistent", "example.com", false}, + + // Weird backwards compatibility with no tag being different than empty tag + {"empty tag vs no tag mismatch", "example.com", "example.com!foo", false}, + + // testMultiFilterTaggedWildcard + {"testMultiFilterTaggedWildcard_0", "example.com!*", "example.com!", false}, + {"testMultiFilterTaggedWildcard_1", "example.com!*", "example.com", false}, + {"testMultiFilterTaggedWildcard_2", "example.com!*", "example.net", false}, + {"testMultiFilterTaggedWildcard_3", "example.com!*", "example.com!george", true}, + {"testMultiFilterTaggedWildcard_4", "example.com!*", "example.com!john", true}, + + // testFilterEmptyTagAndNoTag + {"testFilterEmptyTagAndNoTag_0", "example.com!,example.com", "example.com!", true}, + {"testFilterEmptyTagAndNoTag_1", "example.com!,example.com", "example.com", true}, + {"testFilterEmptyTagAndNoTag_2", "example.com!,example.com", "example.net", false}, + {"testFilterEmptyTagAndNoTag_3", "example.com!,example.com", "example.com!tag", false}, } for _, tc := range testCases { diff --git a/pkg/normalize/importTransform_test.go b/pkg/normalize/importTransform_test.go index 4e2c3bb8b..ffd41ee2a 100644 --- a/pkg/normalize/importTransform_test.go +++ b/pkg/normalize/importTransform_test.go @@ -32,6 +32,10 @@ func TestImportTransform(t *testing.T) { cfg := &models.DNSConfig{ Domains: []*models.DomainConfig{src, dst}, } + err := cfg.PostProcess() + if err != nil { + t.Fatal(err) + } if errs := ValidateAndNormalizeConfig(cfg); len(errs) != 0 { for _, err := range errs { t.Error(err)