1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

Provider support for DS records as children only (#765)

This functionality is required by the GCLOUD provider, which supports
recordsets of type DS but only for child records of the zone, to enable
further delegation. It does not support them at the apex of the zone (@)
because Google Cloud DNS is not itself a registrar which needs to model
this information.

A related change (14ff68b151b5db1f24bcdaccb30b6fa95897940a, #760) was
previously introduced to enable DS support in Google, which broke
integration tests with this provider.

To cleanly support this, we introduce a new provider capability
CanUseDSForChildren and appropriate integration tests. Further, it is no
longer possible to verify a provider has the proper capabilities for a
zone simply by existence of particular records; we adapt the capability
checks to enable inspection of the individual recordsets where this is
required.

Closes #762
This commit is contained in:
Matthew Huxtable
2020-06-18 22:24:13 +01:00
committed by GitHub
parent 505062b628
commit ff8ce26cee
7 changed files with 201 additions and 37 deletions

View File

@ -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:
//

View File

@ -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 {

View File

@ -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

View File

@ -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,
)
}
})
})
}

View File

@ -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

View File

@ -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) {

View File

@ -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(),