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:
@ -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:
|
||||
//
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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(),
|
||||
|
Reference in New Issue
Block a user