diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 7cb8d19bf..300138766 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -850,6 +850,25 @@ func makeTests(t *testing.T) []*TestGroup { tc("add 2 more DS", ds("foo", 2, 13, 4, "ADIGEST"), ds("@", 65535, 5, 4, "ADIGEST"), ds("@", 65535, 253, 4, "ADIGEST")), ), + testgroup("DS (children only)", + requires(providers.CanUseDSForChildren), + // Use a valid digest value here, because GCLOUD (which implements this capability) verifies + // the value passed in is a valid digest. RFC 4034, s5.1.4 specifies SHA1 as the only digest + // algo at present, i.e. only hexadecimal values currently usable. + tc("create DS", ds("child", 1, 13, 1, "0123456789ABCDEF")), + tc("modify field 1", ds("child", 65535, 13, 1, "0123456789ABCDEF")), + tc("modify field 3", ds("child", 65535, 13, 2, "0123456789ABCDEF")), + tc("modify field 2+3", ds("child", 65535, 1, 4, "0123456789ABCDEF")), + tc("modify field 2", ds("child", 65535, 3, 4, "0123456789ABCDEF")), + tc("modify field 2", ds("child", 65535, 254, 4, "0123456789ABCDEF")), + tc("delete 1, create 1", ds("another-child", 2, 13, 4, "0123456789ABCDEF")), + tc("add 2 more DS", + ds("another-child", 2, 13, 4, "0123456789ABCDEF"), + ds("another-child", 65535, 5, 4, "0123456789ABCDEF"), + ds("another-child", 65535, 253, 4, "0123456789ABCDEF"), + ), + ), + // // Pseudo rtypes: // diff --git a/pkg/normalize/capabilities_test.go b/pkg/normalize/capabilities_test.go index 53279472a..b59b83883 100644 --- a/pkg/normalize/capabilities_test.go +++ b/pkg/normalize/capabilities_test.go @@ -55,7 +55,9 @@ func TestCapabilitiesAreFiltered(t *testing.T) { capIntsToNames := make(map[int]string, len(providerCapabilityChecks)) for _, pair := range providerCapabilityChecks { - capIntsToNames[int(pair.cap)] = pair.rType + for _, cap := range pair.caps { + capIntsToNames[int(cap)] = pair.rType + } } for _, capName := range constantNames { diff --git a/pkg/normalize/validate.go b/pkg/normalize/validate.go index 56a78605e..50b648d4d 100644 --- a/pkg/normalize/validate.go +++ b/pkg/normalize/validate.go @@ -447,31 +447,79 @@ func checkDuplicates(records []*models.RecordConfig) (errs []error) { // We pull this out of checkProviderCapabilities() so that it's visible within // the package elsewhere, so that our test suite can look at the list of // capabilities we're checking and make sure that it's up-to-date. -var providerCapabilityChecks []pairTypeCapability +var providerCapabilityChecks = []pairTypeCapability{ + // If a zone uses rType X, the provider must support capability Y. + //{"X", providers.Y}, + capabilityCheck("ALIAS", providers.CanUseAlias), + capabilityCheck("AUTODNSSEC", providers.CanAutoDNSSEC), + capabilityCheck("CAA", providers.CanUseCAA), + capabilityCheck("NAPTR", providers.CanUseNAPTR), + capabilityCheck("PTR", providers.CanUsePTR), + capabilityCheck("R53_ALIAS", providers.CanUseRoute53Alias), + capabilityCheck("SSHFP", providers.CanUseSSHFP), + capabilityCheck("SRV", providers.CanUseSRV), + capabilityCheck("TLSA", providers.CanUseTLSA), + capabilityCheck("AZURE_ALIAS", providers.CanUseAzureAlias), + + // DS needs special record-level checks + { + rType: "DS", + caps: []providers.Capability{providers.CanUseDS, providers.CanUseDSForChildren}, + checkFunc: checkProviderDS, + }, +} type pairTypeCapability struct { rType string - cap providers.Capability + // Capabilities the provider must implement if any records of type rType are found + // in the zonefile. This is a disjunction - implementing at least one of the listed + // capabilities is sufficient. + caps []providers.Capability + // checkFunc provides additional checks of each provider. This function should be + // called if records of type rType are found in the zonefile. + checkFunc func(pType string, _ models.Records) error } -func init() { - providerCapabilityChecks = []pairTypeCapability{ - // If a zone uses rType X, the provider must support capability Y. - //{"X", providers.Y}, - {"ALIAS", providers.CanUseAlias}, - {"AUTODNSSEC", providers.CanAutoDNSSEC}, - {"CAA", providers.CanUseCAA}, - {"DS", providers.CanUseDS}, - {"NAPTR", providers.CanUseNAPTR}, - {"PTR", providers.CanUsePTR}, - {"R53_ALIAS", providers.CanUseRoute53Alias}, - {"SSHFP", providers.CanUseSSHFP}, - {"SRV", providers.CanUseSRV}, - {"TLSA", providers.CanUseTLSA}, - {"AZURE_ALIAS", providers.CanUseAzureAlias}, +func capabilityCheck(rType string, caps ...providers.Capability) pairTypeCapability { + return pairTypeCapability{ + rType: rType, + caps: caps, } } +func providerHasAtLeastOneCapability(pType string, caps ...providers.Capability) bool { + for _, cap := range caps { + if providers.ProviderHasCapability(pType, cap) { + return true + } + } + + return false +} + +func checkProviderDS(pType string, records models.Records) error { + switch { + case providers.ProviderHasCapability(pType, providers.CanUseDS): + // The provider can use DS records anywhere, including at the root + return nil + case !providers.ProviderHasCapability(pType, providers.CanUseDSForChildren): + // Provider has no support for DS records + return fmt.Errorf("provider %s uses DS records but does not support them", pType) + default: + // Provider supports DS records but not at the root + for _, record := range records { + if record.Type == "DS" && record.Name == "@" { + return fmt.Errorf( + "provider %s only supports child DS records, but zone had a record at the root (@)", + pType, + ) + } + } + } + + return nil +} + func checkProviderCapabilities(dc *models.DomainConfig) error { // Check if the zone uses a capability that the provider doesn't // support. @@ -496,9 +544,16 @@ func checkProviderCapabilities(dc *models.DomainConfig) error { } for _, provider := range dc.DNSProviderInstances { // fmt.Printf(" (checking if %q can %q for domain %q)\n", provider.ProviderType, ty.rType, dc.Name) - if !providers.ProviderHasCapability(provider.ProviderType, ty.cap) { + if !providerHasAtLeastOneCapability(provider.ProviderType, ty.caps...) { return fmt.Errorf("Domain %s uses %s records, but DNS provider type %s does not support them", dc.Name, ty.rType, provider.ProviderType) } + + if ty.checkFunc != nil { + checkErr := ty.checkFunc(provider.ProviderType, dc.Records) + if checkErr != nil { + return fmt.Errorf("while checking %s records in domain %s: %w", ty.rType, dc.Name, checkErr) + } + } } } return nil diff --git a/pkg/normalize/validate_test.go b/pkg/normalize/validate_test.go index 548f75ff7..db6b59e5b 100644 --- a/pkg/normalize/validate_test.go +++ b/pkg/normalize/validate_test.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/StackExchange/dnscontrol/v3/models" + "github.com/StackExchange/dnscontrol/v3/providers" ) func TestCheckLabel(t *testing.T) { @@ -282,3 +283,84 @@ func TestTLSAValidation(t *testing.T) { t.Error("Expect error on invalid TLSA but got none") } } + +const ( + ProviderNoDS = "NO_DS_SUPPORT" + ProviderFullDS = "FULL_DS_SUPPORT" + ProviderChildDSOnly = "CHILD_DS_SUPPORT" + ProviderBothDSCaps = "BOTH_DS_CAPABILITIES" +) + +func init() { + providers.RegisterDomainServiceProviderType(ProviderNoDS, nil, providers.DocumentationNotes{}) + providers.RegisterDomainServiceProviderType(ProviderFullDS, nil, providers.DocumentationNotes{ + providers.CanUseDS: providers.Can(), + }) + providers.RegisterDomainServiceProviderType(ProviderChildDSOnly, nil, providers.DocumentationNotes{ + providers.CanUseDSForChildren: providers.Can(), + }) + providers.RegisterDomainServiceProviderType(ProviderBothDSCaps, nil, providers.DocumentationNotes{ + providers.CanUseDS: providers.Can(), + providers.CanUseDSForChildren: providers.Can(), + }) +} + +func Test_DSChecks(t *testing.T) { + t.Run("no DS support", func(t *testing.T) { + err := checkProviderDS(ProviderNoDS, nil) + if err == nil { + t.Errorf("Provider %s implements no DS capabilities, so should have failed the check", ProviderNoDS) + } + }) + + t.Run("full DS support", func(t *testing.T) { + apexDS := models.RecordConfig{Type: "DS"} + apexDS.SetLabel("@", "example.com") + + childDS := models.RecordConfig{Type: "DS"} + childDS.SetLabel("child", "example.com") + + records := models.Records{&apexDS, &childDS} + + // check permutations of ProviderCanDS and having both DS caps + for _, pType := range []string{ProviderFullDS, ProviderBothDSCaps} { + err := checkProviderDS(pType, records) + if err != nil { + t.Errorf("Provider %s implements full DS capabilities and should process the provided records", ProviderFullDS) + } + } + }) + + t.Run("child DS support only", func(t *testing.T) { + apexDS := models.RecordConfig{Type: "DS"} + apexDS.SetLabel("@", "example.com") + + childDS := models.RecordConfig{Type: "DS"} + childDS.SetLabel("child", "example.com") + + // this record is included at the apex to check the Type of the + // recordset is verified to only inspect records with type == DS + apexA := models.RecordConfig{Type: "A"} + apexA.SetLabel("@", "example.com") + + t.Run("accepts when child DS records only", func(t *testing.T) { + records := models.Records{&childDS, &apexA} + err := checkProviderDS(ProviderChildDSOnly, records) + if err != nil { + t.Errorf("Provider %s implements child DS support so the provided records should be accepted", + ProviderChildDSOnly, + ) + } + }) + + t.Run("fails with apex and child DS records", func(t *testing.T) { + records := models.Records{&apexDS, &childDS, &apexA} + err := checkProviderDS(ProviderChildDSOnly, records) + if err == nil { + t.Errorf("Provider %s does not implement DS support at the zone apex, so should reject provided records", + ProviderChildDSOnly, + ) + } + }) + }) +} diff --git a/providers/capabilities.go b/providers/capabilities.go index f831a72f8..a8847ab3e 100644 --- a/providers/capabilities.go +++ b/providers/capabilities.go @@ -18,9 +18,14 @@ const ( // CanUseCAA indicates the provider can handle CAA records CanUseCAA - // CanUseDS indicates that the provider can handle DS record types + // CanUseDS indicates that the provider can handle DS record types. This + // implies CanUseDSForChildren without specifying the latter explicitly. CanUseDS + // CanUseDSForChildren indicates the provider can handle DS record types, but + // only for children records, not at the root of the zone. + CanUseDSForChildren + // CanUsePTR indicates the provider can handle PTR records CanUsePTR diff --git a/providers/capability_string.go b/providers/capability_string.go index bca3c663e..29f3360b7 100644 --- a/providers/capability_string.go +++ b/providers/capability_string.go @@ -11,25 +11,26 @@ func _() { _ = x[CanUseAlias-0] _ = x[CanUseCAA-1] _ = x[CanUseDS-2] - _ = x[CanUsePTR-3] - _ = x[CanUseNAPTR-4] - _ = x[CanUseSRV-5] - _ = x[CanUseSSHFP-6] - _ = x[CanUseTLSA-7] - _ = x[CanUseTXTMulti-8] - _ = x[CanAutoDNSSEC-9] - _ = x[CantUseNOPURGE-10] - _ = x[DocOfficiallySupported-11] - _ = x[DocDualHost-12] - _ = x[DocCreateDomains-13] - _ = x[CanUseRoute53Alias-14] - _ = x[CanGetZones-15] - _ = x[CanUseAzureAlias-16] + _ = x[CanUseDSForChildren-3] + _ = x[CanUsePTR-4] + _ = x[CanUseNAPTR-5] + _ = x[CanUseSRV-6] + _ = x[CanUseSSHFP-7] + _ = x[CanUseTLSA-8] + _ = x[CanUseTXTMulti-9] + _ = x[CanAutoDNSSEC-10] + _ = x[CantUseNOPURGE-11] + _ = x[DocOfficiallySupported-12] + _ = x[DocDualHost-13] + _ = x[DocCreateDomains-14] + _ = x[CanUseRoute53Alias-15] + _ = x[CanGetZones-16] + _ = x[CanUseAzureAlias-17] } -const _Capability_name = "CanUseAliasCanUseCAACanUseDSCanUsePTRCanUseNAPTRCanUseSRVCanUseSSHFPCanUseTLSACanUseTXTMultiCanAutoDNSSECCantUseNOPURGEDocOfficiallySupportedDocDualHostDocCreateDomainsCanUseRoute53AliasCanGetZonesCanUseAzureAlias" +const _Capability_name = "CanUseAliasCanUseCAACanUseDSCanUseDSForChildrenCanUsePTRCanUseNAPTRCanUseSRVCanUseSSHFPCanUseTLSACanUseTXTMultiCanAutoDNSSECCantUseNOPURGEDocOfficiallySupportedDocDualHostDocCreateDomainsCanUseRoute53AliasCanGetZonesCanUseAzureAlias" -var _Capability_index = [...]uint8{0, 11, 20, 28, 37, 48, 57, 68, 78, 92, 105, 119, 141, 152, 168, 186, 197, 213} +var _Capability_index = [...]uint8{0, 11, 20, 28, 47, 56, 67, 76, 87, 97, 111, 124, 138, 160, 171, 187, 205, 216, 232} func (i Capability) String() string { if i >= Capability(len(_Capability_index)-1) { diff --git a/providers/gcloud/gcloudProvider.go b/providers/gcloud/gcloudProvider.go index 525b273a0..85996e44a 100644 --- a/providers/gcloud/gcloudProvider.go +++ b/providers/gcloud/gcloudProvider.go @@ -16,7 +16,7 @@ import ( var features = providers.DocumentationNotes{ providers.CanGetZones: providers.Can(), - providers.CanUseDS: providers.Can(), + providers.CanUseDSForChildren: providers.Can(), providers.CanUseCAA: providers.Can(), providers.CanUsePTR: providers.Can(), providers.CanUseSRV: providers.Can(),