diff --git a/pkg/issuer/acme/dns/route53/fixtures_test.go b/pkg/issuer/acme/dns/route53/fixtures_test.go index a8d3b5ff9..236122cda 100644 --- a/pkg/issuer/acme/dns/route53/fixtures_test.go +++ b/pkg/issuer/acme/dns/route53/fixtures_test.go @@ -30,6 +30,16 @@ var ListHostedZonesByNameResponse = ` 10 + + /hostedzone/HIJKLMN + foo.example.com. + D2224C5B-684A-DB4A-BB9A-E09E3BAFEA7A + + Test comment + false + + 10 + true example2.com diff --git a/pkg/issuer/acme/dns/route53/route53.go b/pkg/issuer/acme/dns/route53/route53.go index 9c87a30ac..3d002e521 100644 --- a/pkg/issuer/acme/dns/route53/route53.go +++ b/pkg/issuer/acme/dns/route53/route53.go @@ -245,16 +245,23 @@ func (r *DNSProvider) getHostedZoneID(fqdn string) (string, error) { return "", err } - var hostedZoneID string + zoneToID := make(map[string]string) + var hostedZones []string for _, hostedZone := range resp.HostedZones { // .Name has a trailing dot - if !*hostedZone.Config.PrivateZone && *hostedZone.Name == authZone { - hostedZoneID = *hostedZone.Id - break + if !*hostedZone.Config.PrivateZone { + zoneToID[*hostedZone.Name] = *hostedZone.Id + hostedZones = append(hostedZones, *hostedZone.Name) } } + authZone, err = util.FindBestMatch(fqdn, hostedZones...) + if err != nil { + return "", fmt.Errorf("Zone %s not found in Route 53 for domain %s", authZone, fqdn) + } - if len(hostedZoneID) == 0 { + hostedZoneID, ok := zoneToID[authZone] + + if len(hostedZoneID) == 0 || !ok { return "", fmt.Errorf("Zone %s not found in Route 53 for domain %s", authZone, fqdn) } diff --git a/pkg/issuer/acme/dns/route53/route53_test.go b/pkg/issuer/acme/dns/route53/route53_test.go index 79c844207..59b2b6774 100644 --- a/pkg/issuer/acme/dns/route53/route53_test.go +++ b/pkg/issuer/acme/dns/route53/route53_test.go @@ -105,6 +105,7 @@ func TestRoute53Present(t *testing.T) { mockResponses := MockResponseMap{ "/2013-04-01/hostedzonesbyname": MockResponse{StatusCode: 200, Body: ListHostedZonesByNameResponse}, "/2013-04-01/hostedzone/ABCDEFG/rrset/": MockResponse{StatusCode: 200, Body: ChangeResourceRecordSetsResponse}, + "/2013-04-01/hostedzone/HIJKLMN/rrset/": MockResponse{StatusCode: 200, Body: ChangeResourceRecordSetsResponse}, "/2013-04-01/change/123456": MockResponse{StatusCode: 200, Body: GetChangeResponse}, } @@ -118,6 +119,18 @@ func TestRoute53Present(t *testing.T) { err := provider.Present(domain, "_acme-challenge."+domain+".", keyAuth) assert.NoError(t, err, "Expected Present to return no error") + + subDomain := "foo.example.com" + err = provider.Present(subDomain, "_acme-challenge."+subDomain+".", keyAuth) + assert.NoError(t, err, "Expected Present to return no error") + + nonExistentSubDomain := "bar.foo.example.com" + err = provider.Present(nonExistentSubDomain, nonExistentSubDomain+".", keyAuth) + assert.NoError(t, err, "Expected Present to return no error") + + nonExistentDomain := "baz.com" + err = provider.Present(nonExistentDomain, nonExistentDomain+".", keyAuth) + assert.Error(t, err, "Expected Present to return an error") } func TestAssumeRole(t *testing.T) { diff --git a/pkg/issuer/acme/dns/util/BUILD.bazel b/pkg/issuer/acme/dns/util/BUILD.bazel index d75c2040f..872c3de50 100644 --- a/pkg/issuer/acme/dns/util/BUILD.bazel +++ b/pkg/issuer/acme/dns/util/BUILD.bazel @@ -16,10 +16,16 @@ go_library( go_test( name = "go_default_test", - srcs = ["wait_test.go"], + srcs = [ + "dns_test.go", + "wait_test.go", + ], data = glob(["testdata/**"]), embed = [":go_default_library"], - deps = ["@com_github_miekg_dns//:go_default_library"], + deps = [ + "@com_github_miekg_dns//:go_default_library", + "@com_github_stretchr_testify//assert:go_default_library", + ], ) filegroup( diff --git a/pkg/issuer/acme/dns/util/dns.go b/pkg/issuer/acme/dns/util/dns.go index fc1b18cb7..621bf2ea6 100644 --- a/pkg/issuer/acme/dns/util/dns.go +++ b/pkg/issuer/acme/dns/util/dns.go @@ -33,3 +33,27 @@ func DNS01LookupFQDN(domain string, followCNAME bool, nameservers ...string) (st return fqdn, nil } + +// FindBestMatch returns the longest match for a given domain within a list of domains +func FindBestMatch(query string, domains ...string) (string, error) { + var maxSoFar int + var longest string + + for _, domain := range domains { + if query == domain { + // Found exact match + return domain, nil + } + + maxHere := dns.CompareDomainName(query, domain) + if maxHere > maxSoFar && dns.IsSubDomain(domain, query) { + maxSoFar = maxHere + longest = domain + } + } + + if len(longest) == 0 { + return "", fmt.Errorf("query: %v has no matches", query) + } + return longest, nil +} diff --git a/pkg/issuer/acme/dns/util/dns_test.go b/pkg/issuer/acme/dns/util/dns_test.go new file mode 100644 index 000000000..d167648dd --- /dev/null +++ b/pkg/issuer/acme/dns/util/dns_test.go @@ -0,0 +1,98 @@ +// +skip_license_check + +package util + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +type input struct { + query string + domains []string +} + +type test struct { + name string + input input + want, got string +} + +var domains = []string{ + "foo.example.com", + "foo.bar.example.com", + "example.com", + "baz.com", +} + +var tests = []*test{ + { + name: "TestExactMatchTLD", + input: input{ + query: "example.com", + domains: domains, + }, + want: "example.com", + }, + { + name: "TestExactMatchSubDomain", + input: input{ + query: "foo.example.com", + domains: domains, + }, + want: "foo.example.com", + }, + { + name: "TestExactMatchSubDomainTwoLevels", + input: input{ + query: "foo.bar.example.com", + domains: domains, + }, + want: "foo.bar.example.com", + }, + { + name: "TestPartialMatchTLD", + input: input{ + query: "baz.example.com", + domains: domains, + }, + want: "example.com", + }, + { + name: "TestPartialMatchSubDomain", + input: input{ + query: "baz.foo.example.com", + domains: domains, + }, + want: "foo.example.com", + }, + { + name: "TestNoMatchReversedOrder", // Negative Test Case + input: input{ + query: "com.example.foo", + domains: domains, + }, + want: "", + }, + { + name: "TestNoMatches", // Negative Test Case + input: input{ + query: "bar.com", + domains: domains, + }, + want: "", + }, +} + +func TestLongestMatches(t *testing.T) { + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tc.got, _ = FindBestMatch(tc.input.query, tc.input.domains...) + if tc.got != tc.want { + assert.Equal(t, tc.want, tc.got, fmt.Sprintf("Failed: TestCase: %s | Query: %s | Want: %v | Got: %v", tc.name, tc.input.query, tc.want, tc.got)) + } + }) + } +}