Reverse domain names in form "0/28.0.10.10.in-addr.arpa." are now allowed.
Ticket: https://fedorahosted.org/freeipa/ticket/4143 Patches attached. -- Martin^2 Basti
>From 052462c2aba165737d7fffe0a3dc2a846a008f5b Mon Sep 17 00:00:00 2001 From: Martin Basti <mba...@redhat.com> Date: Fri, 31 Jan 2014 15:42:31 +0100 Subject: [PATCH] DNS classless support for reverse domains Now users can adding reverse zones in classless form: 0/25.1.168.192.in-addr.arpa. 0-25.1.168.192.in-addr.arpa. 128/25 NS ns.example.com. 10 CNAME 10.128/25.1.168.192.in-addr.arpa. Ticket: https://fedorahosted.org/freeipa/ticket/4143 --- ipalib/plugins/dns.py | 57 +++++++++++++++++++++++++++++++++++++++++---------- ipalib/util.py | 32 ++++++++++++++++++++--------- 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py index 57322e9bbd1ab9a9f09effc8a54fd73b5875c781..b00a84a4dcdea7fb87e67688f931108740914bcf 100644 --- a/ipalib/plugins/dns.py +++ b/ipalib/plugins/dns.py @@ -368,25 +368,31 @@ def _normalize_bind_aci(bind_acis): acis += u';' return acis -def _bind_hostname_validator(ugettext, value): +def _bind_hostname_validator(ugettext, value, allow_slash=False): if value == _dns_zone_record: return try: # Allow domain name which is not fully qualified. These are supported # in bind and then translated as <non-fqdn-name>.<domain>. - validate_hostname(value, check_fqdn=False, allow_underscore=True) + validate_hostname(value, check_fqdn=False, allow_underscore=True, allow_slash=allow_slash) except ValueError, e: return _('invalid domain-name: %s') \ % unicode(e) return None +def _cname_hostname_validator(ugettext, value): + """ + Validator for CNAME allows classless domain names (25/0.0.10.in-addr.arpa.) + """ + return _bind_hostname_validator(ugettext, value, allow_slash=True) + def _dns_record_name_validator(ugettext, value): if value == _dns_zone_record: return try: - map(lambda label:validate_dns_label(label, allow_underscore=True), \ + map(lambda label:validate_dns_label(label, allow_underscore=True, allow_slash=True), \ value.split(u'.')) except ValueError, e: return unicode(e) @@ -411,7 +417,7 @@ def _validate_bind_forwarder(ugettext, forwarder): def _domain_name_validator(ugettext, value): try: - validate_domain_name(value) + validate_domain_name(value, allow_slash=True) except ValueError, e: return unicode(e) @@ -939,7 +945,7 @@ class CNAMERecord(DNSRecord): rfc = 1035 parts = ( Str('hostname', - _bind_hostname_validator, + _cname_hostname_validator, label=_('Hostname'), doc=_('A hostname which this alias hostname points to'), ), @@ -960,7 +966,7 @@ class DNAMERecord(DNSRecord): rfc = 2672 parts = ( Str('target', - _bind_hostname_validator, + _cname_hostname_validator, label=_('Target'), ), ) @@ -1818,6 +1824,11 @@ class dnszone_add(LDAPCreate): else: record_in_zone = nameserver + #classless reverse zones can contain slash '/' + if not zone_is_reverse(normalized_zone) and (normalized_zone.count('/') > 0): + raise errors.ValidationError(name='name', + error=_("Only reverse zones can contain '/' in labels")) + if zone_is_reverse(normalized_zone): if not nameserver.endswith('.'): raise errors.ValidationError(name='name-server', @@ -2131,6 +2142,22 @@ class dnsrecord(LDAPObject): doc=_('Parse all raw DNS records and return them in a structured way'), ) + def _reverse_zone_pre_callback(self, ldap, dn, entry_attrs, *keys, **options): + #in reverse zone can record name contain /, (and -) + + if self.is_pkey_zone_record(*keys): + addr = u'' + else: + addr = keys[-1] + + zone = keys[-2] + zone = normalize_zone(zone) + if not zone_is_reverse(zone): + if addr.count('/') > 0: + raise errors.ValidationError(name='name', + error=unicode(_('Only domain names in reverse zones can contain \'/\'')) ) + + def _nsrecord_pre_callback(self, ldap, dn, entry_attrs, *keys, **options): assert isinstance(dn, DN) nsrecords = entry_attrs.get('nsrecord') @@ -2144,6 +2171,7 @@ class dnsrecord(LDAPObject): ptrrecords = entry_attrs.get('ptrrecord') if ptrrecords is None: return + zone = keys[-2] if self.is_pkey_zone_record(*keys): addr = u'' @@ -2162,16 +2190,23 @@ class dnsrecord(LDAPObject): error=unicode(_('Reverse zone for PTR record should be a sub-zone of one the following fully qualified domains: %s') % allowed_zones)) addr_len = len(addr.split('.')) if addr else 0 - ip_addr_comp_count = addr_len + len(zone.split('.')) - if ip_addr_comp_count != zone_len: - raise errors.ValidationError(name='ptrrecord', - error=unicode(_('Reverse zone %(name)s requires exactly %(count)d IP address components, %(user_count)d given') - % dict(name=zone_name, count=zone_len, user_count=ip_addr_comp_count))) + #Classless zones (0/25.0.0.10.in-addr.arpa.) -> skip check + if (addr.count('/') > 0 or addr.count('-') > 0 or + zone.count('/') > 0 or zone.count('-') > 0): + pass + else: + ip_addr_comp_count = addr_len + len(zone.split('.')) + if ip_addr_comp_count != zone_len: + raise errors.ValidationError(name='ptrrecord', + error=unicode(_('Reverse zone %(name)s requires exactly %(count)d IP address components, %(user_count)d given') + % dict(name=zone_name, count=zone_len, user_count=ip_addr_comp_count))) def run_precallback_validators(self, dn, entry_attrs, *keys, **options): assert isinstance(dn, DN) ldap = self.api.Backend.ldap2 + self._reverse_zone_pre_callback(ldap, dn, entry_attrs, *keys, **options) + for rtype in entry_attrs.keys(): rtype_cb = getattr(self, '_%s_pre_callback' % rtype, None) if rtype_cb: diff --git a/ipalib/util.py b/ipalib/util.py index 1701fbdd18670cfb920044895956d302aa53964c..82e9af994265d1e748a8bbb461c838137f8082f3 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -215,13 +215,24 @@ def normalize_zone(zone): else: return zone -def validate_dns_label(dns_label, allow_underscore=False): +def validate_dns_label(dns_label, allow_underscore=False, allow_slash=False): label_chars = r'a-z0-9' underscore_err_msg = '' + slash = '' + slash_err_msg = '' + slash_err_msg2 = '' + if allow_underscore: label_chars += "_" underscore_err_msg = u' _,' - label_regex = r'^[%(chars)s]([%(chars)s-]?[%(chars)s])*$' % dict(chars=label_chars) + + # '/' is used in classless Reverse zones + if allow_slash: + slash = r'/' + slash_err_msg = u' /,' + slash_err_msg2 = u', or /' + label_regex = r'^[%(chars)s]([%(chars)s%(slash)s-]?[%(chars)s])*$' % dict( + chars=label_chars, slash=slash) regex = re.compile(label_regex, re.IGNORECASE) if not dns_label: @@ -231,18 +242,19 @@ def validate_dns_label(dns_label, allow_underscore=False): raise ValueError(_('DNS label cannot be longer that 63 characters')) if not regex.match(dns_label): - raise ValueError(_('only letters, numbers,%(underscore)s and - are allowed. ' \ - 'DNS label may not start or end with -') \ - % dict(underscore=underscore_err_msg)) + raise ValueError(_('only letters, numbers,%(underscore)s%(slash)s and - are allowed. ' \ + 'DNS label may not start or end with -%(slash2)s') \ + % dict(underscore=underscore_err_msg, + slash=slash_err_msg, slash2=slash_err_msg2)) -def validate_domain_name(domain_name, allow_underscore=False): +def validate_domain_name(domain_name, allow_underscore=False, allow_slash=False): if domain_name.endswith('.'): domain_name = domain_name[:-1] domain_name = domain_name.split(".") # apply DNS name validator to every name part - map(lambda label:validate_dns_label(label,allow_underscore), domain_name) + map(lambda label:validate_dns_label(label, allow_underscore, allow_slash), domain_name) def validate_zonemgr(zonemgr): @@ -287,7 +299,7 @@ def validate_zonemgr(zonemgr): local_part.split(local_part_sep)): raise ValueError(local_part_errmsg) -def validate_hostname(hostname, check_fqdn=True, allow_underscore=False): +def validate_hostname(hostname, check_fqdn=True, allow_underscore=False, allow_slash=False): """ See RFC 952, 1123 :param hostname Checked value @@ -305,9 +317,9 @@ def validate_hostname(hostname, check_fqdn=True, allow_underscore=False): if '.' not in hostname: if check_fqdn: raise ValueError(_('not fully qualified')) - validate_dns_label(hostname,allow_underscore) + validate_dns_label(hostname, allow_underscore, allow_slash) else: - validate_domain_name(hostname,allow_underscore) + validate_domain_name(hostname, allow_underscore, allow_slash) def normalize_sshpubkey(value): return SSHPublicKey(value).openssh() -- 1.8.3.1
>From 7423d58f90a4ee2cf8fc01c3d6ed2b41e53d4301 Mon Sep 17 00:00:00 2001 From: Martin Basti <mba...@redhat.com> Date: Fri, 31 Jan 2014 15:52:35 +0100 Subject: [PATCH] DNS tests for classless reverse domains Ticket: https://fedorahosted.org/freeipa/ticket/4143 --- ipatests/test_xmlrpc/test_dns_plugin.py | 245 ++++++++++++++++++++++++++++++-- 1 file changed, 234 insertions(+), 11 deletions(-) diff --git a/ipatests/test_xmlrpc/test_dns_plugin.py b/ipatests/test_xmlrpc/test_dns_plugin.py index 2cc54de504745fa9a98d132d00554e0e54d8eb57..31867799048be9929dbc775e665281566784e792 100644 --- a/ipatests/test_xmlrpc/test_dns_plugin.py +++ b/ipatests/test_xmlrpc/test_dns_plugin.py @@ -42,6 +42,17 @@ zone2_dn = DN(('idnsname', zone2), api.env.container_dns, api.env.basedn) zone2_ns = u'ns1.%s.' % zone2 zone2_rname = u'root.%s.' % zone2 +zone3 = u'zone3.test' +zone3_ip = u'192.168.1.1' +zone3_ip2 = u'192.168.1.129' +zone3_dn = DN(('idnsname', zone3), api.env.container_dns, api.env.basedn) +zone3_ns = u'ns1.%s.' % zone3 +zone3_ns2 = u'ns2.%s.' % zone3 +zone3_rname = u'root.%s.' % zone3 + +zone3_ns2_arec = u'ns2' +zone3_ns2_arec_dn = DN(('idnsname',zone3_ns2_arec), zone3_dn) + revzone1 = u'31.16.172.in-addr.arpa.' revzone1_ip = u'172.16.31.0' revzone1_ipprefix = u'172.16.31.' @@ -51,6 +62,16 @@ revzone2 = u'30.15.172.in-addr.arpa.' revzone2_ip = u'172.15.30.0/24' revzone2_dn = DN(('idnsname',revzone2), api.env.container_dns, api.env.basedn) +revzone3_classless1 = u'1.168.192.in-addr.arpa.' +revzone3_classless1_ip = u'192.168.1.0' +revzone3_classless1_ipprefix = u'192.168.1.' +revzone3_classless1_dn = DN(('idnsname', revzone3_classless1), api.env.container_dns, api.env.basedn) + +revzone3_classless2 = u'128/25.1.168.192.in-addr.arpa.' +revzone3_classless2_ip = u'192.168.1.128' +revzone3_classless2_ipprefix = u'192.168.1.' +revzone3_classless2_dn = DN(('idnsname', revzone3_classless2), api.env.container_dns, api.env.basedn) + name1 = u'testdnsres' name1_dn = DN(('idnsname',name1), zone1_dn) name1_renamed = u'testdnsres-renamed' @@ -69,6 +90,17 @@ cname_dn = DN(('idnsname',cname), zone1_dn) dname = u'testdns-dname' dname_dn = DN(('idnsname',dname), zone1_dn) +nsrev = u'128/25' +nsrev_dn = DN(('idnsname',nsrev), revzone3_classless1_dn) + +cnamerev = u'129' +cnamerev_dn = DN(('idnsname',cnamerev), revzone3_classless1_dn) +cnamerev_hostname = u'129.128/25.1.168.192.in-addr.arpa.' + +ptr_revzone3 = u'129' +ptr_revzone3_dn = DN(('idnsname',cnamerev), revzone3_classless2_dn) +ptr_revzone3_hostname = zone3_ns2; + relnxname = u'does-not-exist-test' absnxname = u'does.not.exist.test.' @@ -103,7 +135,8 @@ class test_dns(Declarative): pass cleanup_commands = [ - ('dnszone_del', [zone1, zone2, revzone1, revzone2], + ('dnszone_del', [zone1, zone2, zone3, revzone1, revzone2, + revzone3_classless1, revzone3_classless2], {'continue': True}), ('dnsconfig_mod', [], {'idnsforwarders' : None, 'idnsforwardpolicy' : None, @@ -148,8 +181,8 @@ class test_dns(Declarative): } ), expected=errors.ValidationError(name='name', - error=u'only letters, numbers, and - are allowed. ' + - u'DNS label may not start or end with -'), + error=u'only letters, numbers, /, and - are allowed.' + + u' DNS label may not start or end with -, or /'), ), @@ -525,8 +558,8 @@ class test_dns(Declarative): desc='Try to create record with invalid name in zone %r' % zone1, command=('dnsrecord_add', [zone1, u'invalid record'], {'arecord': arec2}), expected=errors.ValidationError(name='name', - error=u'only letters, numbers, _, and - are allowed. ' + - u'DNS label may not start or end with -'), + error=u'only letters, numbers, _, /, and - are allowed.' + + u' DNS label may not start or end with -, or /'), ), @@ -701,8 +734,8 @@ class test_dns(Declarative): 'srv_part_port' : 123, 'srv_part_target' : u'foo bar'}), expected=errors.ValidationError(name='srv_target', - error=u'invalid domain-name: only letters, numbers, _, and - ' + - u'are allowed. DNS label may not start or end with -'), + error=u'invalid domain-name: only letters, numbers, _, and - are allowed.' + + u' DNS label may not start or end with -'), ), dict( @@ -804,8 +837,8 @@ class test_dns(Declarative): desc='Try to add invalid CNAME record %r using dnsrecord_add' % (cname), command=('dnsrecord_add', [zone1, cname], {'cnamerecord': u'-.%s' % relnxname}), expected=errors.ValidationError(name='hostname', - error=u'invalid domain-name: only letters, numbers, _, and - ' + - u'are allowed. DNS label may not start or end with -'), + error=u'invalid domain-name: only letters, numbers, _, /, and - are allowed.' + + u' DNS label may not start or end with -, or /'), ), dict( @@ -874,8 +907,8 @@ class test_dns(Declarative): command=('dnsrecord_add', [zone1, dname], {'dnamerecord': u'-.%s' % absnxname}), expected=errors.ValidationError(name='target', - error=u'invalid domain-name: only letters, numbers, _, and - ' + - u'are allowed. DNS label may not start or end with -'), + error=u'invalid domain-name: only letters, numbers, _, /, and - are allowed.' + + u' DNS label may not start or end with -, or /'), ), dict( @@ -1551,4 +1584,194 @@ class test_dns(Declarative): }, ), + dict( + desc='Create zone %r' % zone3, + command=( + 'dnszone_add', [zone3], { + 'idnssoamname': zone3_ns, + 'idnssoarname': zone3_rname, + 'ip_address' : zone3_ip, + } + ), + expected={ + 'value': zone3, + 'summary': None, + 'result': { + 'dn': zone3_dn, + 'idnsname': [zone3], + 'idnszoneactive': [u'TRUE'], + 'idnssoamname': [zone3_ns], + 'nsrecord': [zone3_ns], + 'idnssoarname': [zone3_rname], + 'idnssoaserial': [fuzzy_digits], + 'idnssoarefresh': [fuzzy_digits], + 'idnssoaretry': [fuzzy_digits], + 'idnssoaexpire': [fuzzy_digits], + 'idnssoaminimum': [fuzzy_digits], + 'idnsallowdynupdate': [u'FALSE'], + 'idnsupdatepolicy': [u'grant %(realm)s krb5-self * A; ' + u'grant %(realm)s krb5-self * AAAA; ' + u'grant %(realm)s krb5-self * SSHFP;' + % dict(realm=api.env.realm)], + 'idnsallowtransfer': [u'none;'], + 'idnsallowquery': [u'any;'], + 'objectclass': objectclasses.dnszone, + }, + }, + ), + + dict( + desc='Add A record to %r in zone %r' % (zone3_ns2_arec, zone3), + command=('dnsrecord_add', [zone3, zone3_ns2_arec], {'arecord': zone3_ip2}), + expected={ + 'value': zone3_ns2_arec, + 'summary': None, + 'result': { + 'dn': zone3_ns2_arec_dn, + 'idnsname': [zone3_ns2_arec], + 'arecord': [zone3_ip2], + 'objectclass': objectclasses.dnsrecord, + }, + }, + ), + + dict( + desc='Create reverse zone %r' % revzone3_classless1, + command=( + 'dnszone_add', [revzone3_classless1], { + 'idnssoamname': zone3_ns, + 'idnssoarname': zone3_rname, + } + ), + expected={ + 'value': revzone3_classless1, + 'summary': None, + 'result': { + 'dn': revzone3_classless1_dn, + 'idnsname': [revzone3_classless1], + 'idnszoneactive': [u'TRUE'], + 'idnssoamname': [zone3_ns], + 'nsrecord': [zone3_ns], + 'idnssoarname': [zone3_rname], + 'idnssoaserial': [fuzzy_digits], + 'idnssoarefresh': [fuzzy_digits], + 'idnssoaretry': [fuzzy_digits], + 'idnssoaexpire': [fuzzy_digits], + 'idnssoaminimum': [fuzzy_digits], + 'idnsallowdynupdate': [u'FALSE'], + 'idnsupdatepolicy': [u'grant %(realm)s krb5-subdomain %(zone)s PTR;' + % dict(realm=api.env.realm, zone=revzone3_classless1)], + 'idnsallowtransfer': [u'none;'], + 'idnsallowquery': [u'any;'], + 'objectclass': objectclasses.dnszone, + }, + }, + ), + + dict( + desc='Create classless reverse zone %r' % revzone3_classless2, + command=( + 'dnszone_add', [revzone3_classless2], { + 'idnssoamname': zone3_ns2, + 'idnssoarname': zone3_rname, + } + ), + expected={ + 'value': revzone3_classless2, + 'summary': None, + 'result': { + 'dn': revzone3_classless2_dn, + 'idnsname': [revzone3_classless2], + 'idnszoneactive': [u'TRUE'], + 'idnssoamname': [zone3_ns2], + 'nsrecord': [zone3_ns2], + 'idnssoarname': [zone3_rname], + 'idnssoaserial': [fuzzy_digits], + 'idnssoarefresh': [fuzzy_digits], + 'idnssoaretry': [fuzzy_digits], + 'idnssoaexpire': [fuzzy_digits], + 'idnssoaminimum': [fuzzy_digits], + 'idnsallowdynupdate': [u'FALSE'], + 'idnsupdatepolicy': [u'grant %(realm)s krb5-subdomain %(zone)s PTR;' + % dict(realm=api.env.realm, zone=revzone3_classless2)], + 'idnsallowtransfer': [u'none;'], + 'idnsallowquery': [u'any;'], + 'objectclass': objectclasses.dnszone, + }, + }, + ), + + dict( + desc='Add NS record to %r in revzone %r' % (nsrev, revzone3_classless1), + command=('dnsrecord_add', [revzone3_classless1, nsrev], {'nsrecord': zone3_ns2}), + expected={ + 'value': nsrev, + 'summary': None, + 'result': { + 'dn': nsrev_dn, + 'idnsname': [nsrev], + 'nsrecord': [zone3_ns2], + 'objectclass': objectclasses.dnsrecord, + }, + }, + ), + + dict( + desc='Add CNAME record to %r in revzone %r' % (cnamerev, revzone3_classless1), + command=('dnsrecord_add', [revzone3_classless1, cnamerev], {'cnamerecord': cnamerev_hostname}), + expected={ + 'value': cnamerev, + 'summary': None, + 'result': { + 'dn': cnamerev_dn, + 'idnsname': [cnamerev], + 'cnamerecord': [cnamerev_hostname], + 'objectclass': objectclasses.dnsrecord, + }, + }, + ), + + dict( + desc='Add PTR record to %r in revzone %r' % (ptr_revzone3, revzone3_classless2), + command=('dnsrecord_add', [revzone3_classless2, cnamerev], + {'ptrrecord': ptr_revzone3_hostname}), + expected={ + 'value': ptr_revzone3, + 'summary': None, + 'result': { + 'dn': ptr_revzone3_dn, + 'idnsname': [ptr_revzone3], + 'ptrrecord': [ptr_revzone3_hostname], + 'objectclass': objectclasses.dnsrecord, + }, + }, + ), + + dict( + desc='Try to create zone with invalid name', + command=( + 'dnszone_add', [u'invalid/zone'], { + 'idnssoamname': zone1_ns, + 'idnssoarname': zone1_rname, + 'ip_address' : zone1_ip, + } + ), + expected=errors.ValidationError(name='name', + error=u'Only reverse zones can contain \'/\' in labels'), + ), + + dict( + desc='Try to add NS record %r to non-reverse zone %r using dnsrecord_add' % (nsrev, zone1), + command=('dnsrecord_add', [zone1, nsrev], {'nsrecord': zone3_ns2}), + expected=errors.ValidationError(name='name', + error=u'Only domain names in reverse zones can contain \'/\''), + ), + + dict( + desc='Try to add invalid PTR hostname %r to %r using dnsrecord_add' % (cnamerev_hostname, revzone1), + command=('dnsrecord_add', [revzone1, revname1], {'ptrrecord': cnamerev_hostname }), + expected=errors.ValidationError(name='hostname', + error=u'invalid domain-name: only letters, numbers, and - ' + + u'are allowed. DNS label may not start or end with -'), + ), ] -- 1.8.3.1
_______________________________________________ Freeipa-devel mailing list Freeipa-devel@redhat.com https://www.redhat.com/mailman/listinfo/freeipa-devel