9991ea
From c884a56c2d9996fc54c054c78d56eae50f696997 Mon Sep 17 00:00:00 2001
9991ea
From: Martin Basti <mbasti@redhat.com>
9991ea
Date: Fri, 31 Jan 2014 15:42:31 +0100
9991ea
Subject: [PATCH 45/46] DNS classless support for reverse domains
9991ea
9991ea
Now users can add reverse zones in classless form:
9991ea
0/25.1.168.192.in-addr.arpa.
9991ea
0-25.1.168.192.in-addr.arpa.
9991ea
9991ea
128/25 NS ns.example.com.
9991ea
10 CNAME 10.128/25.1.168.192.in-addr.arpa.
9991ea
9991ea
Ticket: https://fedorahosted.org/freeipa/ticket/4143
9991ea
Reviewed-By: Jan Cholasta <jcholast@redhat.com>
9991ea
---
9991ea
 ipalib/plugins/dns.py | 45 +++++++++++++++++++++++++++----------
9991ea
 ipalib/util.py        | 61 ++++++++++++++++++++++++++++++---------------------
9991ea
 2 files changed, 70 insertions(+), 36 deletions(-)
9991ea
9991ea
diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py
9991ea
index 94ae92ba5d1ae42e31ebb6100c743a2334f29e70..a78dc9e90a04a00a731541f8a04db5c0f0dd12bb 100644
9991ea
--- a/ipalib/plugins/dns.py
9991ea
+++ b/ipalib/plugins/dns.py
9991ea
@@ -368,25 +368,31 @@ def _normalize_bind_aci(bind_acis):
9991ea
     acis += u';'
9991ea
     return acis
9991ea
 
9991ea
-def _bind_hostname_validator(ugettext, value):
9991ea
+def _bind_hostname_validator(ugettext, value, allow_slash=False):
9991ea
     if value == _dns_zone_record:
9991ea
         return
9991ea
     try:
9991ea
         # Allow domain name which is not fully qualified. These are supported
9991ea
         # in bind and then translated as <non-fqdn-name>.<domain>.
9991ea
-        validate_hostname(value, check_fqdn=False, allow_underscore=True)
9991ea
+        validate_hostname(value, check_fqdn=False, allow_underscore=True, allow_slash=allow_slash)
9991ea
     except ValueError, e:
9991ea
         return _('invalid domain-name: %s') \
9991ea
             % unicode(e)
9991ea
 
9991ea
     return None
9991ea
 
9991ea
+def _bind_cname_hostname_validator(ugettext, value):
9991ea
+    """
9991ea
+    Validator for CNAME allows classless domain names (25/0.0.10.in-addr.arpa.)
9991ea
+    """
9991ea
+    return _bind_hostname_validator(ugettext, value, allow_slash=True)
9991ea
+
9991ea
 def _dns_record_name_validator(ugettext, value):
9991ea
     if value == _dns_zone_record:
9991ea
         return
9991ea
 
9991ea
     try:
9991ea
-        map(lambda label:validate_dns_label(label, allow_underscore=True), \
9991ea
+        map(lambda label:validate_dns_label(label, allow_underscore=True, allow_slash=True), \
9991ea
             value.split(u'.'))
9991ea
     except ValueError, e:
9991ea
         return unicode(e)
9991ea
@@ -411,7 +417,10 @@ def _validate_bind_forwarder(ugettext, forwarder):
9991ea
 
9991ea
 def _domain_name_validator(ugettext, value):
9991ea
     try:
9991ea
-        validate_domain_name(value)
9991ea
+        #classless reverse zones can contain slash '/'
9991ea
+        normalized_zone = normalize_zone(value)
9991ea
+        validate_domain_name(value, allow_slash=zone_is_reverse(normalized_zone))
9991ea
+
9991ea
     except ValueError, e:
9991ea
         return unicode(e)
9991ea
 
9991ea
@@ -939,7 +948,7 @@ class CNAMERecord(DNSRecord):
9991ea
     rfc = 1035
9991ea
     parts = (
9991ea
         Str('hostname',
9991ea
-            _bind_hostname_validator,
9991ea
+            _bind_cname_hostname_validator,
9991ea
             label=_('Hostname'),
9991ea
             doc=_('A hostname which this alias hostname points to'),
9991ea
         ),
9991ea
@@ -960,7 +969,7 @@ class DNAMERecord(DNSRecord):
9991ea
     rfc = 2672
9991ea
     parts = (
9991ea
         Str('target',
9991ea
-            _bind_hostname_validator,
9991ea
+            _bind_cname_hostname_validator,
9991ea
             label=_('Target'),
9991ea
         ),
9991ea
     )
9991ea
@@ -2119,6 +2128,14 @@ class dnsrecord(LDAPObject):
9991ea
                            doc=_('Parse all raw DNS records and return them in a structured way'),
9991ea
                            )
9991ea
 
9991ea
+    def _idnsname_pre_callback(self, ldap, dn, entry_attrs, *keys, **options):
9991ea
+        if not self.is_pkey_zone_record(*keys):
9991ea
+            zone, addr = normalize_zone(keys[-2]), keys[-1]
9991ea
+            try:
9991ea
+                validate_domain_name(addr, allow_underscore=True, allow_slash=zone_is_reverse(zone))
9991ea
+            except ValueError, e:
9991ea
+                raise errors.ValidationError(name='idnsname', error=unicode(e))
9991ea
+
9991ea
     def _nsrecord_pre_callback(self, ldap, dn, entry_attrs, *keys, **options):
9991ea
         assert isinstance(dn, DN)
9991ea
         nsrecords = entry_attrs.get('nsrecord')
9991ea
@@ -2132,6 +2149,7 @@ def _ptrrecord_pre_callback(self, ldap, dn, entry_attrs, *keys, **options):
9991ea
         ptrrecords = entry_attrs.get('ptrrecord')
9991ea
         if ptrrecords is None:
9991ea
             return
9991ea
+
9991ea
         zone = keys[-2]
9991ea
         if self.is_pkey_zone_record(*keys):
9991ea
             addr = u''
9991ea
@@ -2150,11 +2168,16 @@ def _ptrrecord_pre_callback(self, ldap, dn, entry_attrs, *keys, **options):
9991ea
                     error=unicode(_('Reverse zone for PTR record should be a sub-zone of one the following fully qualified domains: %s') % allowed_zones))
9991ea
 
9991ea
         addr_len = len(addr.split('.')) if addr else 0
9991ea
-        ip_addr_comp_count = addr_len + len(zone.split('.'))
9991ea
-        if ip_addr_comp_count != zone_len:
9991ea
-            raise errors.ValidationError(name='ptrrecord',
9991ea
-                error=unicode(_('Reverse zone %(name)s requires exactly %(count)d IP address components, %(user_count)d given')
9991ea
-                % dict(name=zone_name, count=zone_len, user_count=ip_addr_comp_count)))
9991ea
+
9991ea
+        #Classless zones (0/25.0.0.10.in-addr.arpa.) -> skip check
9991ea
+        #zone has to be checked without reverse domain suffix (in-addr.arpa.)
9991ea
+        if ('/' not in addr and '/' not in zone and
9991ea
+            '-' not in addr and '-' not in zone):
9991ea
+            ip_addr_comp_count = addr_len + len(zone.split('.'))
9991ea
+            if ip_addr_comp_count != zone_len:
9991ea
+                raise errors.ValidationError(name='ptrrecord',
9991ea
+                      error=unicode(_('Reverse zone %(name)s requires exactly %(count)d IP address components, %(user_count)d given')
9991ea
+                      % dict(name=zone_name, count=zone_len, user_count=ip_addr_comp_count)))
9991ea
 
9991ea
     def run_precallback_validators(self, dn, entry_attrs, *keys, **options):
9991ea
         assert isinstance(dn, DN)
9991ea
diff --git a/ipalib/util.py b/ipalib/util.py
9991ea
index 3c52e4fd9a3e08d160dd4ae7076590be8b869d2c..17851294a78507aba7035390c3695184b7d641b1 100644
9991ea
--- a/ipalib/util.py
9991ea
+++ b/ipalib/util.py
9991ea
@@ -215,34 +215,45 @@ def normalize_zone(zone):
9991ea
     else:
9991ea
         return zone
9991ea
 
9991ea
-def validate_dns_label(dns_label, allow_underscore=False):
9991ea
-    label_chars = r'a-z0-9'
9991ea
-    underscore_err_msg = ''
9991ea
-    if allow_underscore:
9991ea
-        label_chars += "_"
9991ea
-        underscore_err_msg = u' _,'
9991ea
-    label_regex = r'^[%(chars)s]([%(chars)s-]?[%(chars)s])*$' % dict(chars=label_chars)
9991ea
-    regex = re.compile(label_regex, re.IGNORECASE)
9991ea
-
9991ea
-    if not dns_label:
9991ea
-        raise ValueError(_('empty DNS label'))
9991ea
-
9991ea
-    if len(dns_label) > 63:
9991ea
-        raise ValueError(_('DNS label cannot be longer that 63 characters'))
9991ea
-
9991ea
-    if not regex.match(dns_label):
9991ea
-        raise ValueError(_('only letters, numbers,%(underscore)s and - are allowed. ' \
9991ea
-                           'DNS label may not start or end with -') \
9991ea
-                           % dict(underscore=underscore_err_msg))
9991ea
-
9991ea
-def validate_domain_name(domain_name, allow_underscore=False):
9991ea
+
9991ea
+def validate_dns_label(dns_label, allow_underscore=False, allow_slash=False):
9991ea
+     base_chars = 'a-z0-9'
9991ea
+     extra_chars = ''
9991ea
+     middle_chars = ''
9991ea
+
9991ea
+     if allow_underscore:
9991ea
+         extra_chars += '_'
9991ea
+     if allow_slash:
9991ea
+         middle_chars += '/'
9991ea
+
9991ea
+     middle_chars = middle_chars + '-' #has to be always the last in the regex [....-]
9991ea
+
9991ea
+     label_regex = r'^[%(base)s%(extra)s]([%(base)s%(extra)s%(middle)s]?[%(base)s%(extra)s])*$' \
9991ea
+         % dict(base=base_chars, extra=extra_chars, middle=middle_chars)
9991ea
+     regex = re.compile(label_regex, re.IGNORECASE)
9991ea
+
9991ea
+     if not dns_label:
9991ea
+         raise ValueError(_('empty DNS label'))
9991ea
+
9991ea
+     if len(dns_label) > 63:
9991ea
+         raise ValueError(_('DNS label cannot be longer that 63 characters'))
9991ea
+
9991ea
+     if not regex.match(dns_label):
9991ea
+         chars = ', '.join("'%s'" % c for c in extra_chars + middle_chars)
9991ea
+         chars2 = ', '.join("'%s'" % c for c in middle_chars)
9991ea
+         raise ValueError(_("only letters, numbers, %(chars)s are allowed. " \
9991ea
+                            "DNS label may not start or end with %(chars2)s") \
9991ea
+                            % dict(chars=chars, chars2=chars2))
9991ea
+
9991ea
+
9991ea
+def validate_domain_name(domain_name, allow_underscore=False, allow_slash=False):
9991ea
     if domain_name.endswith('.'):
9991ea
         domain_name = domain_name[:-1]
9991ea
 
9991ea
     domain_name = domain_name.split(".")
9991ea
 
9991ea
     # apply DNS name validator to every name part
9991ea
-    map(lambda label:validate_dns_label(label,allow_underscore), domain_name)
9991ea
+    map(lambda label:validate_dns_label(label, allow_underscore, allow_slash), domain_name)
9991ea
 
9991ea
 
9991ea
 def validate_zonemgr(zonemgr):
9991ea
@@ -287,7 +298,7 @@ def validate_zonemgr(zonemgr):
9991ea
                local_part.split(local_part_sep)):
9991ea
         raise ValueError(local_part_errmsg)
9991ea
 
9991ea
-def validate_hostname(hostname, check_fqdn=True, allow_underscore=False):
9991ea
+def validate_hostname(hostname, check_fqdn=True, allow_underscore=False, allow_slash=False):
9991ea
     """ See RFC 952, 1123
9991ea
 
9991ea
     :param hostname Checked value
9991ea
@@ -305,9 +316,9 @@ def validate_hostname(hostname, check_fqdn=True, allow_underscore=False):
9991ea
     if '.' not in hostname:
9991ea
         if check_fqdn:
9991ea
             raise ValueError(_('not fully qualified'))
9991ea
-        validate_dns_label(hostname,allow_underscore)
9991ea
+        validate_dns_label(hostname, allow_underscore, allow_slash)
9991ea
     else:
9991ea
-        validate_domain_name(hostname,allow_underscore)
9991ea
+        validate_domain_name(hostname, allow_underscore, allow_slash)
9991ea
 
9991ea
 def normalize_sshpubkey(value):
9991ea
     return SSHPublicKey(value).openssh()
9991ea
-- 
9991ea
1.8.5.3
9991ea