[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]

[Freeipa-devel] [PATCHES] 0022-0023 [RFE] DNS - IDN support



Hello,

patches here contain a *draft* of IDN support for IPA DNS.

Overview:
1) IND domains stored in LDAP are punycoded(A-labels)
2) now domain can contains almost everything 
3) domains have to be normalized (AD requires normalized domains too).
Example:  groƟ => gross
4) --raw option shows domains punycoded
5) without --raw option domains are showed in Unicode(U-labels, human
readable form)
6) It works only in DNS module, rest of IPA is still without IDN
7) IDN domains are not added into realmdomains

TODO:
1) bug in dnspython can cause improper conversion with escaped
characters:  https://github.com/rthalley/dnspython/issues/46
2) discuss if validators should be more strict (only letters
allowed, ...)
3) fix parts of code where domains are showed in punycode - error
messages, exceptions
4) cleanup unused code

TESTS:
1) 3 failures: caused by TODO 3)
2) expected value: 'value' should be in Unicode(U-labels), instead of
punycode (part of TODO 3) )


-- 
Martin^2 Basti
>From 9e3159fac2b4c95a869368ce96d085ce55e646f3 Mon Sep 17 00:00:00 2001
From: Martin Basti <mbasti redhat com>
Date: Wed, 27 Nov 2013 14:40:54 +0100
Subject: [PATCH] Added support for IDN domains (only for DNS)

* this patch allows create IDN zones and records
* Domains are stored in punycode in LDAP
* allows only normalized DNS name

Ticket:
https://fedorahosted.org/freeipa/ticket/3169
---
 ipalib/plugins/dns.py | 420 +++++++++++++++++++++++++++++++++++++++++++++-----
 ipalib/util.py        | 153 ++++++++++++++++++
 2 files changed, 534 insertions(+), 39 deletions(-)

diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py
index 07523dc72466892f0e7d5fdd9261024d0e898548..263f9e4b6f796e1baa43d6a000a096d05953d21b 100644
--- a/ipalib/plugins/dns.py
+++ b/ipalib/plugins/dns.py
@@ -32,10 +32,13 @@ from ipalib.parameters import (Flag, Bool, Int, Decimal, Str, StrEnum, Any,
                                DeprecatedParam)
 from ipalib.plugins.baseldap import *
 from ipalib import _, ngettext
-from ipalib.util import (validate_zonemgr, normalize_zonemgr, normalize_zone,
-        validate_hostname, validate_dns_label, validate_domain_name,
+from ipalib.util import (validate_zonemgr, normalize_zone,
+        normalize_domain_name_idn, validate_hostname, validate_dns_label,
+        validate_domain_name, validate_domain_name_idn, normalize_zonemgr_idn,
+        validate_zonemgr_idn, convert_domain_name_to_idn, validate_hostname_idn,
         get_dns_forward_zone_update_policy, get_dns_reverse_zone_update_policy,
-        get_reverse_zone_default, zone_is_reverse, REVERSE_DNS_ZONES)
+        get_reverse_zone_default, zone_is_reverse, REVERSE_DNS_ZONES,
+        is_domain_punycoded)
 from ipapython.ipautil import valid_ip, CheckedIPAddress, is_host_resolvable
 
 __doc__ = _("""
@@ -248,6 +251,13 @@ _record_attributes = [str('%srecord' % t.lower()) for t in _record_types]
 # supported DNS classes, IN = internet, rest is almost never used
 _record_classes = (u'IN', u'CS', u'CH', u'HS')
 
+
+def _normalize_domain_name_idn(domain_name):
+    """
+    Normalize domain name, convert IDN U-label to A-label (punycode).
+    """
+    return unicode(normalize_domain_name_idn(domain_name, compare_idn_normalized=True))
+
 def _rname_validator(ugettext, zonemgr):
     try:
         validate_zonemgr(zonemgr)
@@ -255,6 +265,16 @@ def _rname_validator(ugettext, zonemgr):
         return unicode(e)
     return None
 
+
+
+def _rname_validator_idn(ugettext, zonemgr):
+    try:
+        validate_zonemgr_idn(zonemgr)
+    except ValueError, e:
+        return unicode(e)
+    return None
+
+
 def _create_zone_serial():
     """
     Generate serial number for zones. bind-dyndb-ldap expects unix time in
@@ -266,6 +286,7 @@ def _create_zone_serial():
     """
     return int(time.time())
 
+
 def _reverse_zone_name(netstr):
     try:
         netaddr.IPAddress(str(netstr))
@@ -381,6 +402,21 @@ def _bind_hostname_validator(ugettext, value):
 
     return None
 
+
+def _bind_hostname_validator_idn(ugettext, value):
+    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_idn(value, check_fqdn=False)
+    except ValueError, e:
+        return _('invalid domain-name: %s') \
+            % unicode(e)
+
+    return None
+
+
 def _dns_record_name_validator(ugettext, value):
     if value == _dns_zone_record:
         return
@@ -391,6 +427,43 @@ def _dns_record_name_validator(ugettext, value):
     except ValueError, e:
         return unicode(e)
 
+
+def _dns_record_name_validator_idn(ugettext, value):
+    """
+    Validate IDN record names
+    """
+    if value == _dns_zone_record:
+        return
+
+    if value.endswith(u'.'):
+        return unicode(_('record name must be relative'))
+
+    try:
+        validate_domain_name_idn(value, compare_idn_normalized=True)
+    except ValueError, e:
+        return unicode(e)
+
+
+def _dns_record_name_normalizer_idn(value):
+    """
+    Normalize record name, convert IDN U-label to A-label (punycode).
+    """
+    if value == _dns_zone_record:
+        return value
+
+    return unicode(normalize_domain_name_idn(value, compare_idn_normalized=True))
+
+
+def _convert_domain_name_to_idn(value):
+    """
+    Convert A-label (punycode) to U-label (human readable UTF-8)
+    """
+    if value == _dns_zone_record:
+        return value
+
+    return unicode(convert_domain_name_to_idn(value))
+
+
 def _validate_bind_forwarder(ugettext, forwarder):
     ip_address, sep, port = forwarder.partition(u' port ')
 
@@ -415,6 +488,15 @@ def _domain_name_validator(ugettext, value):
     except ValueError, e:
         return unicode(e)
 
+
+def _domain_name_validator_idn(ugettext, domain_name):
+    try:
+        validate_domain_name_idn(domain_name, compare_idn_normalized=True)
+        return None
+    except ValueError, e:
+        return unicode(e)
+
+
 def _hostname_validator(ugettext, value):
     try:
         validate_hostname(value)
@@ -424,6 +506,17 @@ def _hostname_validator(ugettext, value):
 
     return None
 
+
+def _hostname_validator_idn(ugettext, value):
+    try:
+        validate_hostname_idn(value)
+    except ValueError, e:
+        return _('invalid domain-name: %s') \
+            % unicode(e)
+
+    return None
+
+
 def _normalize_hostname(domain_name):
     """Make it fully-qualified"""
     if domain_name[-1] != '.':
@@ -431,6 +524,14 @@ def _normalize_hostname(domain_name):
     else:
         return domain_name
 
+
+def _normalize_hostname_idn(domain_name):
+    """Make it fully-qualified, normalize, convert U-labels to A-labels"""
+    if domain_name[-1] != '.':
+        domain_name = domain_name + '.'
+    return _normalize_domain_name_idn(domain_name)
+
+
 def is_forward_record(zone, str_address):
     addr = netaddr.IPAddress(str_address)
     if addr.version == 4:
@@ -489,11 +590,12 @@ def get_reverse_zone(ipaddr, prefixlen=None):
     return revzone, revname
 
 def add_records_for_host_validation(option_name, host, domain, ip_addresses, check_forward=True, check_reverse=True):
+    domain_u_labels = _convert_domain_name_to_idn(domain)
     try:
         api.Command['dnszone_show'](domain)['result']
     except errors.NotFound:
         raise errors.NotFound(
-            reason=_('DNS zone %(zone)s not found') % dict(zone=domain)
+            reason=_('DNS zone %(zone)s not found') % dict(zone=domain_u_labels)
         )
     if not isinstance(ip_addresses, (tuple, list)):
         ip_addresses = [ip_addresses]
@@ -508,7 +610,7 @@ def add_records_for_host_validation(option_name, host, domain, ip_addresses, che
             if is_forward_record(domain, unicode(ip)):
                 raise errors.DuplicateEntry(
                         message=_(u'IP address %(ip)s is already assigned in domain %(domain)s.')\
-                            % dict(ip=str(ip), domain=domain))
+                            % dict(ip=str(ip), domain=domain_u_labels))
 
         if check_reverse:
             try:
@@ -548,6 +650,28 @@ def add_records_for_host(host, domain, ip_addresses, add_forward=True, add_rever
                 # the entry already exists and matches
                 pass
 
+def _dnszone_post_callback_idn_decode(entry_attrs):
+    #if IDN was used, show domain names in U-labels
+    try:
+        entry_attrs['idnsname'] = [ _convert_domain_name_to_idn(domain) for domain in entry_attrs['idnsname']]
+    except Exception:
+        pass
+
+    try:
+        entry_attrs['idnssoamname'] = [ _convert_domain_name_to_idn(domain) for domain in entry_attrs['idnssoamname']]
+    except Exception:
+        pass
+
+    try:
+        entry_attrs['idnssoarname'] = [ _convert_domain_name_to_idn(domain) for domain in entry_attrs['idnssoarname']]
+    except Exception:
+        pass
+
+    try:
+        entry_attrs['nsrecord'] = [ _convert_domain_name_to_idn(domain) for domain in entry_attrs['nsrecord']]
+    except Exception:
+        pass
+
 class DNSRecord(Str):
     # a list of parts that create the actual raw DNS record
     parts = None
@@ -900,8 +1024,10 @@ class AFSDBRecord(DNSRecord):
             maxvalue=65535,
         ),
         Str('hostname',
-            _bind_hostname_validator,
+            _bind_hostname_validator_idn,
             label=_('Hostname'),
+            flags=['idn_enable'],
+            normalizer=_normalize_hostname_idn,
         ),
     )
 
@@ -939,9 +1065,11 @@ class CNAMERecord(DNSRecord):
     rfc = 1035
     parts = (
         Str('hostname',
-            _bind_hostname_validator,
+            _bind_hostname_validator_idn,
             label=_('Hostname'),
             doc=_('A hostname which this alias hostname points to'),
+            flags=['idn_enable'],
+            normalizer=_normalize_domain_name_idn,
         ),
     )
 
@@ -960,8 +1088,10 @@ class DNAMERecord(DNSRecord):
     rfc = 2672
     parts = (
         Str('target',
-            _bind_hostname_validator,
+            _bind_hostname_validator_idn,
             label=_('Target'),
+            flags=['idn_enable'],
+            normalizer=_normalize_domain_name_idn,
         ),
     )
 
@@ -1039,9 +1169,11 @@ class KXRecord(DNSRecord):
             maxvalue=65535,
         ),
         Str('exchanger',
-            _bind_hostname_validator,
+            _bind_hostname_validator_idn,
             label=_('Exchanger'),
             doc=_('A host willing to act as a key exchanger'),
+            flags=['idn_enable'],
+            normalizer=_normalize_domain_name_idn,
         ),
     )
 
@@ -1180,9 +1312,11 @@ class MXRecord(DNSRecord):
             maxvalue=65535,
         ),
         Str('exchanger',
-            _bind_hostname_validator,
+            _bind_hostname_validator_idn,
             label=_('Exchanger'),
             doc=_('A host willing to act as a mail exchanger'),
+            flags=['idn_enable'],
+            normalizer=_normalize_domain_name_idn,
         ),
     )
 
@@ -1192,8 +1326,10 @@ class NSRecord(DNSRecord):
 
     parts = (
         Str('hostname',
-            _bind_hostname_validator,
+            _bind_hostname_validator_idn,
             label=_('Hostname'),
+            flags=['idn_enable'],
+            normalizer=_normalize_domain_name_idn,
         ),
     )
 
@@ -1206,8 +1342,10 @@ class NSECRecord(DNSRecord):
 
     parts = (
         Str('next',
-            _bind_hostname_validator,
+            _bind_hostname_validator_idn,
             label=_('Next Domain Name'),
+            flags=['idn_enable'],
+            normalizer=_normalize_domain_name_idn,
         ),
         StrEnum('types+',
             label=_('Type Map'),
@@ -1288,9 +1426,10 @@ class PTRRecord(DNSRecord):
     rfc = 1035
     parts = (
         Str('hostname',
-            _hostname_validator,
-            normalizer=_normalize_hostname,
+            _hostname_validator_idn,
+            normalizer=_normalize_hostname_idn,
             label=_('Hostname'),
+            flags=['idn_enable'],
             doc=_('The hostname this reverse record points to'),
         ),
     )
@@ -1306,6 +1445,13 @@ def _srv_target_validator(ugettext, value):
         return
     return _bind_hostname_validator(ugettext, value)
 
+def _srv_target_validator_idn(ugettext, value):
+    if value == u'.':
+        # service not available
+        return
+    return _bind_hostname_validator_idn(ugettext, value)
+
+
 class SRVRecord(DNSRecord):
     rrtype = 'SRV'
     rfc = 2782
@@ -1326,9 +1472,11 @@ class SRVRecord(DNSRecord):
             maxvalue=65535,
         ),
         Str('target',
-            _srv_target_validator,
+            _srv_target_validator_idn,
             label=_('Target'),
             doc=_('The domain name of the target host or \'.\' if the service is decidedly not available at this domain'),
+            flags=['idn_enable'],
+            normalizer=_normalize_domain_name_idn,
         ),
     )
 
@@ -1507,13 +1655,13 @@ def check_ns_rec_resolvable(zone, name):
         name = normalize_zone(zone)
     elif not name.endswith('.'):
         # this is a DNS name relative to the zone
-        zone = dns.name.from_text(zone)
+        zone = dns.name.from_unicode(zone)
         name = unicode(dns.name.from_text(name, origin=zone))
     try:
         return api.Command['dns_resolve'](name)
     except errors.NotFound:
         raise errors.NotFound(
-            reason=_('Nameserver \'%(host)s\' does not have a corresponding A/AAAA record') % {'host': name}
+            reason=_('Nameserver \'%(host)s\' does not have a corresponding A/AAAA record') % {'host': _convert_domain_name_to_idn(name)}
         )
 
 def dns_container_exists(ldap):
@@ -1555,12 +1703,12 @@ class dnszone(LDAPObject):
 
     takes_params = (
         Str('idnsname',
-            _domain_name_validator,
+            _domain_name_validator_idn,
             cli_name='name',
             label=_('Zone name'),
             doc=_('Zone name (FQDN)'),
             default_from=lambda name_from_ip: _reverse_zone_name(name_from_ip),
-            normalizer=lambda value: value.lower(),
+            normalizer=_normalize_domain_name_idn,
             primary_key=True,
         ),
         Str('name_from_ip?', _validate_ipnet,
@@ -1569,18 +1717,21 @@ class dnszone(LDAPObject):
             flags=('virtual_attribute',),
         ),
         Str('idnssoamname',
+            _domain_name_validator_idn,
             cli_name='name_server',
             label=_('Authoritative nameserver'),
             doc=_('Authoritative nameserver domain name'),
-            normalizer=lambda value: value.lower(),
+            normalizer=_normalize_domain_name_idn,
         ),
         Str('idnssoarname',
-            _rname_validator,
+            _rname_validator_idn,
             cli_name='admin_email',
             label=_('Administrator e-mail address'),
             doc=_('Administrator e-mail address'),
+            #FIXME conversion doesnt work here, class Param calls normalizer inside
+            #default_from=lambda idnsname: 'hostmaster.%s' % convert_domain_name_to_idn(idnsname),
             default_from=lambda idnsname: 'hostmaster.%s' % idnsname,
-            normalizer=normalize_zonemgr,
+            normalizer=normalize_zonemgr_idn,
         ),
         Int('idnssoaserial',
             cli_name='serial',
@@ -1812,7 +1963,6 @@ class dnszone_add(LDAPCreate):
 
         nameserver_ip_address = options.get('ip_address')
         normalized_zone = normalize_zone(keys[-1])
-
         if nameserver.endswith('.'):
             record_in_zone = self.obj.get_name_in_zone(keys[-1], nameserver)
         else:
@@ -1851,14 +2001,17 @@ class dnszone_add(LDAPCreate):
             add_forward_record(keys[-1],
                                dns_record,
                                nameserver_ip_address)
-
+        if not options.get('raw', False):
+            _dnszone_post_callback_idn_decode(entry_attrs)
         # Add entry to realmdomains
         # except for our own domain, forwarded zones and reverse zones
         zone = keys[0]
 
         if (zone != api.env.domain
             and not options.get('idnsforwarders')
-            and not zone_is_reverse(zone)):
+            and not zone_is_reverse(zone)
+            and not is_domain_punycoded(zone)): #Realmdomain doesnt support IDN
+
             try:
                 api.Command['realmdomains_mod'](add_domain=zone, force=True)
             except errors.EmptyModlist:
@@ -1885,7 +2038,8 @@ class dnszone_del(LDAPDelete):
         # except for our own domain
         zone = keys[0]
 
-        if zone != api.env.domain:
+        if zone != api.env.domain and not is_domain_punycoded(zone): #Realmdomain doesnt support IDN
+
             try:
                 api.Command['realmdomains_mod'](del_domain=zone, force=True)
             except errors.AttrValueNotFound:
@@ -1911,10 +2065,21 @@ class dnszone_mod(LDAPUpdate):
     def pre_callback(self, ldap, dn, entry_attrs, attrs_list,  *keys, **options):
         nameserver = entry_attrs.get('idnssoamname')
         if nameserver and nameserver != _dns_zone_record and not options['force']:
+            #convert nameserver to punycode
+            #nameserver = _normalize_domain_name_idn(nameserver)
             check_ns_rec_resolvable(keys[0], nameserver)
 
         return dn
 
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+        if options.get('raw', False):
+            #raw output dont decode punycode
+            pass
+        else:
+            _dnszone_post_callback_idn_decode(entry_attrs)
+        return dn
+
 api.register(dnszone_mod)
 
 
@@ -1951,15 +2116,60 @@ class dnszone_find(LDAPSearch):
 
     def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *args, **options):
         assert isinstance(base_dn, DN)
+
+        # Search in LDAP for internationalized domain names in punycode
+        term = args[-1]
+        if term is not None:
+            # remove first dot before converting to punycode, then insert it back,
+            # otherwise normalization raise EmptyLabel error
+            # (for example: .domain.com)
+            add_dot = u""
+            if term.startswith(r'.'):
+                term = term[1:]
+                add_dot = u"."
+
+            search_idn_kw = {}
+            # Only valid domains can be stored in LDAP, so only valid domains
+            # will be normalized for LDAP search
+            if _domain_name_validator_idn(None, term) is None: #allow search without
+                normalized_domain = _normalize_domain_name_idn(term)
+                if term == normalized_domain:
+                    #no extra filter is required
+                    pass
+                else:
+                    search_idn_kw['idnsname'] = add_dot + normalized_domain
+                    search_idn_kw['idnssoamname'] = add_dot + normalized_domain
+
+            if _rname_validator_idn(None, term) is None:
+                normalized_zonemgr = normalize_zonemgr_idn(term)
+                if term == normalized_zonemgr:
+                    #no extra filter is required
+                    pass
+                else:
+                    search_idn_kw['idnssoarname'] = add_dot + normalized_zonemgr
+
+            if len(search_idn_kw) > 0:
+                filter_idn = ldap.make_filter(search_idn_kw, exact=False)
+                filter = ldap.combine_filters((filter_idn, filter), ldap.MATCH_ANY)
+
         if options.get('forward_only', False):
             search_kw = {}
             search_kw['idnsname'] = REVERSE_DNS_ZONES.keys()
             rev_zone_filter = ldap.make_filter(search_kw, rules=ldap.MATCH_NONE, exact=False,
                     trailing_wildcard=False)
             filter = ldap.combine_filters((rev_zone_filter, filter), rules=ldap.MATCH_ALL)
-
         return (filter, base_dn, scope)
 
+    def post_callback(self, ldap, entries, truncated, *args, **options):
+        if options.get('raw', False):
+            #raw output dont decode punycode
+            pass
+        else:
+            #if IDN was used, show domain names user friendly not in punycode
+            for entry in entries:
+                _dnszone_post_callback_idn_decode(entry)
+
+        return truncated
 
 api.register(dnszone_find)
 
@@ -1969,6 +2179,16 @@ class dnszone_show(LDAPRetrieve):
 
     has_output_params = LDAPRetrieve.has_output_params + dnszone_output_params
 
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+        if options.get('raw', False):
+            #raw output dont decode punycode
+            pass
+        else:
+            _dnszone_post_callback_idn_decode(entry_attrs)
+
+        return dn
+
 api.register(dnszone_show)
 
 
@@ -2026,9 +2246,10 @@ class dnszone_add_permission(LDAPQuery):
         try:
             (dn_, entry_attrs) = ldap.get_entry(dn, ['objectclass'])
         except errors.NotFound:
-            self.obj.handle_not_found(*keys)
+            self.obj.handle_not_found(_convert_domain_name_to_idn(keys[-1]))
 
-        permission_name = self.obj.permission_name(keys[-1])
+        #leave domain name in IDN U-label format
+        permission_name = self.obj.permission_name(_convert_domain_name_to_idn(keys[-1]))
         permission = api.Command['permission_add_noaci'](permission_name,
                          permissiontype=u'SYSTEM'
                      )['result']
@@ -2062,13 +2283,14 @@ class dnszone_remove_permission(LDAPQuery):
         try:
             ldap.update_entry(dn, {'managedby': None})
         except errors.NotFound:
-            self.obj.handle_not_found(*keys)
+            self.obj.handle_not_found(_convert_domain_name_to_idn(keys[-1]))
         except errors.EmptyModlist:
             # managedBy attribute is clean, lets make sure there is also no
             # dangling DNS zone permission
             pass
 
-        permission_name = self.obj.permission_name(keys[-1])
+        #leave domain name in IDN U-label format
+        permission_name = self.obj.permission_name(_convert_domain_name_to_idn(keys[-1]))
         api.Command['permission_del'](permission_name, force=True)
 
         return dict(
@@ -2095,11 +2317,12 @@ class dnsrecord(LDAPObject):
 
     takes_params = (
         Str('idnsname',
-            _dns_record_name_validator,
+            _dns_record_name_validator_idn,
             cli_name='name',
             label=_('Record name'),
             doc=_('Record name'),
             primary_key=True,
+            normalizer=_dns_record_name_normalizer_idn,
         ),
         Int('dnsttl?',
             cli_name='ttl',
@@ -2132,7 +2355,13 @@ class dnsrecord(LDAPObject):
         ptrrecords = entry_attrs.get('ptrrecord')
         if ptrrecords is None:
             return
-        zone = keys[-2]
+
+        # convert zone name to A-labels
+        e = _domain_name_validator_idn(None, keys[-2])
+        if e is not None:
+            raise errors.ValidationError(name='ptrrecord', error=unicode(e))
+        zone = _normalize_domain_name_idn(keys[-2])
+
         if self.is_pkey_zone_record(*keys):
             addr = u''
         else:
@@ -2156,10 +2385,16 @@ class dnsrecord(LDAPObject):
                 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 zone_name_pre_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+        e = _domain_name_validator_idn(None, keys[-2])
+        if e is not None:
+            raise errors.ValidationError(name='dnszone', error=unicode(e))
+
     def run_precallback_validators(self, dn, entry_attrs, *keys, **options):
         assert isinstance(dn, DN)
         ldap = self.api.Backend.ldap2
-
+        self.zone_name_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:
@@ -2172,6 +2407,13 @@ class dnsrecord(LDAPObject):
         return False
 
     def get_dn(self, *keys, **options):
+        #normalize zonename (convert from U-label to A-label)
+        e = _domain_name_validator_idn(None, keys[-2])
+        if e is not None:
+            raise errors.ValidationError(name='dnszone', error=unicode(e))
+        keys_l = list(keys);
+        keys_l[-2] = _normalize_domain_name_idn(keys_l[-2])
+        keys = tuple(keys_l)
         if self.is_pkey_zone_record(*keys):
             dn = self.api.Object[self.parent_object].get_dn(*keys[:-1], **options)
             # zone must exist
@@ -2179,7 +2421,7 @@ class dnsrecord(LDAPObject):
             try:
                 (dn_, zone) = ldap.get_entry(dn, [])
             except errors.NotFound:
-                self.api.Object['dnszone'].handle_not_found(keys[-2])
+                self.api.Object['dnszone'].handle_not_found(_convert_domain_name_to_idn(keys[-2]))
             return self.api.Object[self.parent_object].get_dn(*keys[:-1], **options)
         return super(dnsrecord, self).get_dn(*keys, **options)
 
@@ -2230,7 +2472,53 @@ class dnsrecord(LDAPObject):
         return dict((attr, val) for attr,val in entry_attrs.iteritems() \
                     if attr in self.params and not self.params[attr].primary_key)
 
+    def _postprocess_idn_decode(self, record):
+        # convert idn parts of record to human readable form (U-labels)
+        for attr in record.keys():
+            attr = attr.lower()
+            try:
+                param = self.params[attr]
+            except KeyError:
+                continue
+
+            if not isinstance(param, DNSRecord):
+                continue
+
+            parts_params = param.get_parts()
+
+            idn_records = []
+            for dnsvalue in record[attr]:
+                values = param._get_part_values(dnsvalue)
+                if values is None:
+                    continue
+                idn_record = u""
+                for val_id, val in enumerate(values):
+                    if val is not None:
+                        if 'idn_enable' in parts_params[val_id].flags:
+                            if isinstance(val,(list, tuple)):
+                                for v in val:
+                                    idn_record = u"%s %s" % (idn_record,
+                                                             _convert_domain_name_to_idn(v))
+                            else:
+                                idn_record = u"%s %s" % (idn_record,
+                                                         _convert_domain_name_to_idn(val))
+                        else:
+                            if isinstance(val,(list, tuple)):
+                                idn_record = u"%s %s" % (idn_record, u" ".join(val))
+                            else:
+                                idn_record = u"%s %s" % (idn_record, val)
+                idn_records.append(idn_record.lstrip())
+            record[attr] = idn_records
+
     def postprocess_record(self, record, **options):
+
+        if not options.get('raw', False):
+            #idnsname is in every record
+            try:
+                record['idnsname'] = [_convert_domain_name_to_idn(domain) for domain in record['idnsname']]
+            except KeyError:
+                pass
+
         if options.get('structured', False):
             for attr in record.keys():
                 # attributes in LDAPEntry may not be normalized
@@ -2243,7 +2531,6 @@ class dnsrecord(LDAPObject):
                 if not isinstance(param, DNSRecord):
                     continue
                 parts_params = param.get_parts()
-
                 for dnsvalue in record[attr]:
                     dnsentry = {
                             u'dnstype' : unicode(param.rrtype),
@@ -2254,10 +2541,18 @@ class dnsrecord(LDAPObject):
                         continue
                     for val_id, val in enumerate(values):
                         if val is not None:
-                            dnsentry[parts_params[val_id].name] = val
+                            #decode IDN
+                            if ((not options.get('raw', False))
+                            and ('idn_enable' in parts_params[val_id].flags)):
+                                dnsentry[parts_params[val_id].name] = _convert_domain_name_to_idn(val)
+                            else:
+                                dnsentry[parts_params[val_id].name] = val
                     record.setdefault('dnsrecords', []).append(dnsentry)
                 del record[attr]
 
+        elif not options.get('raw', False):
+            self._postprocess_idn_decode(record)
+
     def get_rrparam_from_part(self, part_name):
         """
         Get an instance of DNSRecord parameter that has part_name as its part.
@@ -2531,6 +2826,14 @@ class dnsrecord_add(LDAPCreate):
 
     def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
         assert isinstance(dn, DN)
+        #normalize zonename (convert from U-label to A-label)
+        e = _domain_name_validator_idn(None, keys[-2])
+        if e is not None:
+            raise errors.ValidationError(name='dnszone', error=unicode(e))
+        keys_l = list(keys);
+        keys_l[-2] = _normalize_domain_name_idn(keys_l[-2])
+        keys = tuple(keys_l)
+
         for attr in getattr(context, 'dnsrecord_precallback_attrs', []):
             param = self.params[attr]
             param.dnsrecord_add_post_callback(ldap, dn, entry_attrs, *keys, **options)
@@ -2594,7 +2897,7 @@ class dnsrecord_mod(LDAPUpdate):
         try:
             (dn_, old_entry) = ldap.get_entry(dn, _record_attributes)
         except errors.NotFound:
-            self.obj.handle_not_found(*keys)
+            self.obj.handle_not_found(_convert_domain_name_to_idn(keys[-1]))
 
         if updated_attrs:
             for attr in updated_attrs:
@@ -2746,7 +3049,7 @@ class dnsrecord_del(LDAPUpdate):
         try:
             (dn_, old_entry) = ldap.get_entry(dn, _record_attributes)
         except errors.NotFound:
-            self.obj.handle_not_found(*keys)
+            self.obj.handle_not_found(_convert_domain_name_to_idn(keys[-1]))
 
         for attr in entry_attrs.keys():
             if attr not in _record_attributes:
@@ -2876,6 +3179,13 @@ class dnsrecord_show(LDAPRetrieve):
 
     def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
         assert isinstance(dn, DN)
+        #normalize zonename
+        e = _domain_name_validator_idn(None, keys[-2])
+        if e is not None:
+            raise errors.ValidationError(name='dnszone', error=unicode(e))
+        keys_l = list(keys);
+        keys_l[-2] = _normalize_domain_name_idn(keys_l[-2])
+        keys = tuple(keys_l)
         if self.obj.is_pkey_zone_record(*keys):
             entry_attrs[self.obj.primary_key.name] = [_dns_zone_record]
         self.obj.postprocess_record(entry_attrs, **options)
@@ -2904,9 +3214,41 @@ class dnsrecord_find(LDAPSearch):
     def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *args, **options):
         assert isinstance(base_dn, DN)
         # include zone record (root entry) in the search
+        zonename = args[-2]
+
+        if zonename is not None:
+            # remove first dot before converting to punycode, then insert it back,
+            # otherwise normalization raise EmptyLabel error
+            # (for example: .domain.com)
+            add_dot = u""
+            if zonename.startswith(r'.'):
+                zonename = zonename[1:]
+                add_dot = u"."
+
+            # Only valid domains can be stored in LDAP, so only valid domains
+            # will be normalized for LDAP search
+            e = _domain_name_validator_idn(None, zonename)
+            if e is not None:
+                raise errors.ValidationError(name='dnszone', error=unicode(e))
+            normalized_domain = _normalize_domain_name_idn(zonename)
+            if zonename == normalized_domain:
+                #no change ind base_dn is required
+                pass
+            else:
+                #change zone in base_dn to punycoded
+                normalized_domain = add_dot + normalized_domain
+                dn_list = [RDN(('idnsname', normalized_domain))] + base_dn[1:]
+                punycoded_dn = DN(*dn_list)
+                base_dn = punycoded_dn
+
         return (filter, base_dn, ldap.SCOPE_SUBTREE)
 
     def post_callback(self, ldap, entries, truncated, *args, **options):
+        #normalize zonename
+        args_l = list(args);
+        args_l[0] = _normalize_domain_name_idn(args_l[0])
+        args = tuple(args_l)
+
         if entries:
             zone_obj = self.api.Object[self.obj.parent_object]
             zone_dn = zone_obj.get_dn(args[0])
diff --git a/ipalib/util.py b/ipalib/util.py
index e14077487e979f077ddc1f9e925678884a64b5b5..7eff283b1a7e0869085a62282f5aebe3f30c1c98 100644
--- a/ipalib/util.py
+++ b/ipalib/util.py
@@ -28,6 +28,8 @@ import socket
 import re
 import decimal
 import netaddr
+import dns.name
+import encodings.idna
 from types import NoneType
 from weakref import WeakKeyDictionary
 from dns import resolver, rdatatype
@@ -194,6 +196,17 @@ def check_writable_file(filename):
     except (IOError, OSError), e:
         raise errors.FileError(reason=str(e))
 
+def is_domain_punycoded(domain):
+    """
+    Determine if the domain contains punycoded label
+    """
+    labels = domain.split(u'.')
+    for label in labels:
+        if label.startswith(u'xn--'):
+            return True
+    return False
+
+
 def normalize_zonemgr(zonemgr):
     if not zonemgr:
         # do not normalize empty or None value
@@ -209,6 +222,25 @@ def normalize_zonemgr(zonemgr):
 
     return zonemgr
 
+def normalize_zonemgr_idn(zonemgr):
+    """
+    Normalize IDN email
+    Convert from IDN U-labels to A-labels (punycode)
+    """
+    if not zonemgr:
+        # do not normalize empty or None value
+        return zonemgr
+    if '@' in zonemgr:
+        # local-part needs to be normalized
+        name, at, domain = zonemgr.partition('@')
+        name = name.replace('.', '\\.')
+        zonemgr = u''.join((name, u'.', domain))
+
+    if not zonemgr.endswith('.'):
+        zonemgr = zonemgr + u'.'
+
+    return unicode(normalize_domain_name_idn(zonemgr))
+
 def normalize_zone(zone):
     if zone[-1] != '.':
         return zone + '.'
@@ -235,6 +267,45 @@ def validate_dns_label(dns_label, allow_underscore=False):
                            'DNS label may not start or end with -') \
                            % dict(underscore=underscore_err_msg))
 
+def normalize_domain_name_idn(domain_name, compare_idn_normalized=False):
+    """
+    Normalize domain name, convert IDN domain from U-label to A-label (punycode).
+
+    @param compare_idn_normalized, if IDN domain name and normalized IDN domain
+    name do not match, then stop normalization
+    """
+    domain_n = dns.name.from_unicode(domain_name)
+    if domain_name.endswith('.'):
+        a_label = domain_n.to_text(omit_final_dot=False)
+    else:
+        a_label = domain_n.to_text(omit_final_dot=True)
+
+    #it has to be here, because normalizator is called before validator
+    #compare if IDN normalized and original domain match
+    if (compare_idn_normalized and not is_domain_punycoded(domain_name) #only U-lables can be compared
+        ):
+        normalized_domain_name = encodings.idna.nameprep(domain_name)
+        if(domain_name != normalized_domain_name):
+            raise ValueError( _("domain name '%(domain)s' and normalized domain name "
+                                "'%(normalized)s' do not match. Please use only "
+                                "normalized domains")  % {'domain':domain_name,
+                                'normalized':normalized_domain_name})
+    return unicode(a_label)
+
+def convert_domain_name_to_idn(domain_name):
+    """
+    Convert domain name from A-labels (punycode) to U-labels (UTF-8).
+    """
+    try:
+        domain_n = dns.name.from_text(domain_name)
+        domain_n = domain_n.canonicalize()
+        if domain_name.endswith('.'):
+            return unicode(domain_n.to_unicode(omit_final_dot=False))
+        else:
+            return unicode(domain_n.to_unicode(omit_final_dot=True))
+    except Exception:
+        return unicode(domain_name)
+
 def validate_domain_name(domain_name, allow_underscore=False):
     if domain_name.endswith('.'):
         domain_name = domain_name[:-1]
@@ -244,6 +315,35 @@ def validate_domain_name(domain_name, allow_underscore=False):
     # apply DNS name validator to every name part
     map(lambda label:validate_dns_label(label,allow_underscore), domain_name)
 
+def validate_domain_name_idn(domain_name, compare_idn_normalized=True):
+    try:
+        normalize_domain_name_idn(domain_name,
+                                  compare_idn_normalized=compare_idn_normalized)
+    except dns.name.BadEscape:
+        raise ValueError(_('invalid escape code in domain name'))
+    except dns.name.EmptyLabel:
+        raise ValueError(_('empty DNS label'))
+    except dns.name.NameTooLong:
+        raise ValueError(_('domain name cannot be longer than 255 characters'))
+    except dns.name.LabelTooLong:
+        raise ValueError(_('DNS label cannot be longer than 63 characters'))
+    except DNSException:
+        raise ValueError(_('domain name does not meet requirements for IDN domains'))
+    #dnspython bug?, punycode label longer than 63 returns this exception
+    #instead of LabelTooLong
+    except UnicodeError:
+        raise ValueError(_('DNS label cannot be longer than 63 characters'))
+
+#this was moved to normalizator, because normalizator is called before validator
+#    if is_domain_punycoded(a_label):
+#        #check if IDN normalized domain name is the same as domain name
+#        normalized_domain_name = convert_domain_name_to_idn(a_label)
+#        if(domain_name != normalized_domain_name):
+#            raise ValueError( _("domain name '%(domain)s' and normalized domain name "
+#                                "'%(normalized)s' do not match. Please use only "
+#                                "normalized IDN domains")  % {'domain':domain_name,
+#                                'normalized':normalized_domain_name})
+
 
 def validate_zonemgr(zonemgr):
     """ See RFC 1033, 1035 """
@@ -287,6 +387,39 @@ def validate_zonemgr(zonemgr):
                local_part.split(local_part_sep)):
         raise ValueError(local_part_errmsg)
 
+def validate_zonemgr_idn(zonemgr):
+    """
+    Validate zone manager IDN email
+    """
+
+    local_part = None
+    domain = None
+
+    if zonemgr.endswith('.'):
+        zonemgr = zonemgr[:-1]
+
+    if zonemgr.count('@') == 1:
+        local_part, dot, domain = zonemgr.partition('@')
+    elif zonemgr.count('@') > 1:
+        raise ValueError(_('too many \'@\' characters'))
+    else:
+        last_fake_sep = zonemgr.rfind('\\.')
+        if last_fake_sep != -1: # there is a 'fake' local-part/domain separator
+            sep = zonemgr.find('.', last_fake_sep+2)
+            if sep != -1:
+                local_part = zonemgr[:sep]
+                domain = zonemgr[sep+1:]
+        else:
+            local_part, dot, domain = zonemgr.partition('.')
+
+    if not domain:
+        raise ValueError(_('missing address domain'))
+
+    if not local_part:
+        raise ValueError(_('missing mail account'))
+
+    validate_domain_name_idn(local_part + '.' + domain)
+
 def validate_hostname(hostname, check_fqdn=True, allow_underscore=False):
     """ See RFC 952, 1123
 
@@ -309,6 +442,26 @@ def validate_hostname(hostname, check_fqdn=True, allow_underscore=False):
     else:
         validate_domain_name(hostname,allow_underscore)
 
+
+def validate_hostname_idn(hostname, check_fqdn=True):
+    """
+    Validate IDN hostname
+
+    :param hostname Checked value
+    :param check_fqdn Check if hostname is fully qualified
+    """
+    if hostname.endswith('.'):
+        hostname = hostname[:-1]
+
+    if '..' in hostname:
+        raise ValueError(_('hostname contains empty label (consecutive dots)'))
+
+    if '.' not in hostname:
+        if check_fqdn:
+            raise ValueError(_('not fully qualified'))
+
+    validate_domain_name_idn(hostname, compare_idn_normalized=True)
+
 def normalize_sshpubkey(value):
     return SSHPublicKey(value).openssh()
 
-- 
1.8.3.1

>From 6cbbe3c2b778899cbc02761f94db00501f52fc18 Mon Sep 17 00:00:00 2001
From: Martin Basti <mbasti redhat com>
Date: Fri, 6 Dec 2013 14:03:19 +0100
Subject: [PATCH] IDN domains updated test

ticket: https://fedorahosted.org/freeipa/ticket/3169
---
 ipatests/test_xmlrpc/test_dns_plugin.py | 691 +++++++++++++++++++++++++++++---
 1 file changed, 631 insertions(+), 60 deletions(-)

diff --git a/ipatests/test_xmlrpc/test_dns_plugin.py b/ipatests/test_xmlrpc/test_dns_plugin.py
index 1bfaee71e2e069616c3f2f58ad4d72f541cff694..8e1722fada80041abc37d99b472a18ec4ffeea61 100644
--- a/ipatests/test_xmlrpc/test_dns_plugin.py
+++ b/ipatests/test_xmlrpc/test_dns_plugin.py
@@ -57,6 +57,40 @@ dnsrescname_dn = DN(('idnsname',dnsrescname), dnszone1_dn)
 dnsresdname = u'testdns-dname'
 dnsresdname_dn = DN(('idnsname',dnsresdname), dnszone1_dn)
 
+dnszone3 = u'\u010d.test'
+dnszone3_punycoded = u'xn--bea.test'
+dnszone3_dn = DN(('idnsname',dnszone3_punycoded), api.env.container_dns, api.env.basedn)
+dnszone3_mname = u'ns1.%s.' % dnszone3
+dnszone3_mname_punycoded = u'ns1.%s.' % dnszone3_punycoded
+dnszone3_mname_dn = DN(('idnsname','ns1'), dnszone3_dn)
+dnszone3_rname = u'root.%s.' % dnszone3
+dnszone3_rname_punycoded = u'root.%s.' % dnszone3_punycoded
+revdnszone3 = u'15.168.192.in-addr.arpa.'
+revdnszone3_ip = u'192.168.15.0/24'
+revdnszone3_dn = DN(('idnsname', revdnszone3), api.env.container_dns, api.env.basedn)
+dnszone3_permission = u'Manage DNS zone %s' % dnszone3
+dnszone3_permission_dn = DN(('cn',dnszone1_permission),
+                            api.env.container_permission,api.env.basedn)
+dnsres3 = u'sk\xfa\u0161ka'
+dnsres3_punycoded = u'xn--skka-rra23d'
+dnsres3_dn = DN(('idnsname',dnsres3_punycoded), dnszone3_dn)
+dnsrescname3 = u'\u0161\u0161'
+dnsrescname3_punycoded = u'xn--pgaa'
+dnsrescname3_dn = DN(('idnsname',dnsrescname3_punycoded), dnszone3_dn)
+dnsresdname3 = u'\xe1\xe1'
+dnsresdname3_punycoded = u'xn--1caa'
+dnsresdname3_dn = DN(('idnsname',dnsresdname3_punycoded), dnszone3_dn)
+testdomain3 = u'\u010d\u010d\u010d.test'
+testdomain3_punycoded = u'xn--beaaa.test'
+dnsresnsec3 = u'sk\xfa\u0161ka-b'
+dnsresnsec3_punycoded = u'xn--skka-b-qya83f'
+dnsresnsec3_dn = DN(('idnsname',dnsresnsec3_punycoded), dnszone3_dn)
+dnsresafsdb3 = u'sk\xfa\u0161ka-c'
+dnsresafsdb3_punycoded = u'xn--skka-c-qya83f'
+dnsresafsdb3_dn = DN(('idnsname',dnsresafsdb3_punycoded), dnszone3_dn)
+dnszone3_txtrec_dn = DN(('idnsname', '_kerberos'), dnszone3_dn)
+
+
 class test_dns(Declarative):
 
     @classmethod
@@ -78,13 +112,14 @@ class test_dns(Declarative):
             pass
 
     cleanup_commands = [
-        ('dnszone_del', [dnszone1, dnszone2, revdnszone1, revdnszone2],
+        ('dnszone_del', [dnszone1, dnszone2, revdnszone1, revdnszone2,
+                         dnszone3, revdnszone3],
             {'continue': True}),
         ('dnsconfig_mod', [], {'idnsforwarders' : None,
                                'idnsforwardpolicy' : None,
                                'idnsallowsyncptr' : None,
                                }),
-        ('permission_del', [dnszone1_permission], {'force': True}),
+        ('permission_del', [dnszone1_permission, dnszone3_permission], {'force': True}),
     ]
 
     tests = [
@@ -113,19 +148,19 @@ class test_dns(Declarative):
         ),
 
 
-        dict(
-            desc='Try to create zone with invalid name',
-            command=(
-                'dnszone_add', [u'invalid zone'], {
-                    'idnssoamname': dnszone1_mname,
-                    'idnssoarname': dnszone1_rname,
-                    'ip_address' : u'1.2.3.4',
-                }
-            ),
-            expected=errors.ValidationError(name='name',
-                error=u'only letters, numbers, and - are allowed. ' +
-                    u'DNS label may not start or end with -'),
-        ),
+#        dict(
+#            desc='Try to create zone with invalid name',
+#            command=(
+#                'dnszone_add', [u'invalid zone'], {
+#                    'idnssoamname': dnszone1_mname,
+#                    'idnssoarname': dnszone1_rname,
+#                    'ip_address' : u'1.2.3.4',
+#                }
+#            ),
+#            expected=errors.ValidationError(name='name',
+#                error=u'only letters, numbers, and - are allowed. ' +
+#                    u'DNS label may not start or end with -'),
+#        ),
 
 
         dict(
@@ -496,13 +531,13 @@ class test_dns(Declarative):
         ),
 
 
-        dict(
-            desc='Try to create record with invalid name in zone %r' % dnszone1,
-            command=('dnsrecord_add', [dnszone1, u'invalid record'], {'arecord': u'127.0.0.1'}),
-            expected=errors.ValidationError(name='name',
-                error=u'only letters, numbers, _, and - are allowed. ' +
-                    u'DNS label may not start or end with -'),
-        ),
+#        dict(
+#            desc='Try to create record with invalid name in zone %r' % dnszone1,
+#            command=('dnsrecord_add', [dnszone1, u'invalid record'], {'arecord': u'127.0.0.1'}),
+#            expected=errors.ValidationError(name='name',
+#                error=u'only letters, numbers, _, and - are allowed. ' +
+#                    u'DNS label may not start or end with -'),
+#        ),
 
 
         dict(
@@ -661,24 +696,24 @@ class test_dns(Declarative):
             },
         ),
 
-        dict(
-            desc='Try to add invalid SRV record to zone %r using dnsrecord_add' % (dnszone1),
-            command=('dnsrecord_add', [dnszone1, u'_foo._tcp'], {'srvrecord': dnszone1_mname}),
-            expected=errors.ValidationError(name='srv_rec',
-                error=u'format must be specified as "PRIORITY WEIGHT PORT TARGET" ' +
-                    u' (see RFC 2782 for details)'),
-        ),
+#        dict(
+#            desc='Try to add invalid SRV record to zone %r using dnsrecord_add' % (dnszone1),
+#            command=('dnsrecord_add', [dnszone1, u'_foo._tcp'], {'srvrecord': dnszone1_mname}),
+#            expected=errors.ValidationError(name='srv_rec',
+#                error=u'format must be specified as "PRIORITY WEIGHT PORT TARGET" ' +
+#                    u' (see RFC 2782 for details)'),
+#        ),
 
-        dict(
-            desc='Try to add invalid SRV record via parts to zone %r using dnsrecord_add' % (dnszone1),
-            command=('dnsrecord_add', [dnszone1, u'_foo._tcp'], {'srv_part_priority': 0,
-                                                                 'srv_part_weight' : 0,
-                                                                 '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 -'),
-        ),
+#        dict(
+#            desc='Try to add invalid SRV record via parts to zone %r using dnsrecord_add' % (dnszone1),
+#            command=('dnsrecord_add', [dnszone1, u'_foo._tcp'], {'srv_part_priority': 0,
+#                                                                 'srv_part_weight' : 0,
+#                                                                 '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 -'),
+#        ),
 
         dict(
             desc='Try to add SRV record to zone %r both via parts and a raw value' % (dnszone1),
@@ -775,13 +810,13 @@ class test_dns(Declarative):
                       u'record (RFC 1034, section 3.6.2)'),
         ),
 
-        dict(
-            desc='Try to add invalid CNAME record %r using dnsrecord_add' % (dnsrescname),
-            command=('dnsrecord_add', [dnszone1, dnsrescname], {'cnamerecord': u'-.example.com'}),
-            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 -'),
-        ),
+#        dict(
+#            desc='Try to add invalid CNAME record %r using dnsrecord_add' % (dnsrescname),
+#            command=('dnsrecord_add', [dnszone1, dnsrescname], {'cnamerecord': u'-.example.com'}),
+#            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 -'),
+#        ),
 
         dict(
             desc='Try to add multiple CNAME record %r using dnsrecord_add' % (dnsrescname),
@@ -844,13 +879,13 @@ class test_dns(Declarative):
                 error=u'only one DNAME record is allowed per name (RFC 6672, section 2.4)'),
         ),
 
-        dict(
-            desc='Try to add invalid DNAME record %r using dnsrecord_add' % (dnsresdname),
-            command=('dnsrecord_add', [dnszone1, dnsresdname], {'dnamerecord': u'-.example.com.'}),
-            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 -'),
-        ),
+#        dict(
+#            desc='Try to add invalid DNAME record %r using dnsrecord_add' % (dnsresdname),
+#            command=('dnsrecord_add', [dnszone1, dnsresdname], {'dnamerecord': u'-.example.com.'}),
+#            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 -'),
+#        ),
 
         dict(
             desc='Add DNAME record to %r using dnsrecord_add' % (dnsresdname),
@@ -1127,13 +1162,13 @@ class test_dns(Declarative):
         ),
 
 
-        dict(
-            desc='Try to add invalid PTR %r to %r using dnsrecord_add' % (dnsrev1, revdnszone1),
-            command=('dnsrecord_add', [revdnszone1, dnsrev1], {'ptrrecord': u'-.example.com' }),
-            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 -'),
-        ),
+#        dict(
+#            desc='Try to add invalid PTR %r to %r using dnsrecord_add' % (dnsrev1, revdnszone1),
+#            command=('dnsrecord_add', [revdnszone1, dnsrev1], {'ptrrecord': u'-.example.com' }),
+#            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 -'),
+#        ),
 
         dict(
             desc='Add PTR record %r to %r using dnsrecord_add' % (dnsrev1, revdnszone1),
@@ -1507,4 +1542,540 @@ class test_dns(Declarative):
             },
         ),
 
+        dict(
+            desc='Create zone %r' % dnszone3,
+            command=(
+                'dnszone_add', [dnszone3], {
+                    'idnssoamname': dnszone3_mname,
+                    'idnssoarname': dnszone3_rname,
+                    'ip_address' : u'1.2.3.4',
+                }
+            ),
+            expected={
+                'value': dnszone3_punycoded,
+                'summary': None,
+                'result': {
+                    'dn': dnszone3_dn,
+                    'idnsname': [dnszone3],
+                    'idnszoneactive': [u'TRUE'],
+                    'idnssoamname': [dnszone3_mname],
+                    'nsrecord': [dnszone3_mname],
+                    'idnssoarname': [dnszone3_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='Retrieve zone %r' % dnszone3,
+            command=(
+                'dnszone_show', [dnszone3], {}
+            ),
+            expected={
+                'value': dnszone3_punycoded,
+                'summary': None,
+                'result': {
+                    'dn': dnszone3_dn,
+                    'idnsname': [dnszone3],
+                    'idnszoneactive': [u'TRUE'],
+                    'nsrecord': [dnszone3_mname],
+                    'idnssoamname': [dnszone3_mname],
+                    'idnssoarname': [dnszone3_rname],
+                    'idnssoaserial': [fuzzy_digits],
+                    'idnssoarefresh': [fuzzy_digits],
+                    'idnssoaretry': [fuzzy_digits],
+                    'idnssoaexpire': [fuzzy_digits],
+                    'idnssoaminimum': [fuzzy_digits],
+                    'idnsallowtransfer': [u'none;'],
+                    'idnsallowquery': [u'any;'],
+                },
+            },
+        ),
+
+        dict(
+            desc='Retrieve zone raw %r' % dnszone3,
+            command=(
+                'dnszone_show', [dnszone3], {u'raw' : True,}
+            ),
+            expected={
+                'value': dnszone3_punycoded,
+                'summary': None,
+                'result': {
+                    'dn': dnszone3_dn,
+                    'idnsname': [dnszone3_punycoded],
+                    'idnszoneactive': [u'TRUE'],
+                    'nsrecord': [dnszone3_mname_punycoded],
+                    'idnssoamname': [dnszone3_mname_punycoded],
+                    'idnssoarname': [dnszone3_rname_punycoded],
+                    'idnssoaserial': [fuzzy_digits],
+                    'idnssoarefresh': [fuzzy_digits],
+                    'idnssoaretry': [fuzzy_digits],
+                    'idnssoaexpire': [fuzzy_digits],
+                    'idnssoaminimum': [fuzzy_digits],
+                    'idnsallowtransfer': [u'none;'],
+                    'idnsallowquery': [u'any;'],
+                },
+            },
+        ),
+
+        dict(
+            desc='Update zone %r' % dnszone3,
+            command=('dnszone_mod', [dnszone3], {'idnssoarefresh': 5478}),
+            expected={
+                'value': dnszone3_punycoded,
+                'summary': None,
+                'result': {
+                    'idnsname': [dnszone3],
+                    'idnszoneactive': [u'TRUE'],
+                    'nsrecord': [dnszone3_mname],
+                    'idnssoamname': [dnszone3_mname],
+                    'idnssoarname': [dnszone3_rname],
+                    'idnssoaserial': [fuzzy_digits],
+                    'idnssoarefresh': [u'5478'],
+                    'idnssoaretry': [fuzzy_digits],
+                    'idnssoaexpire': [fuzzy_digits],
+                    'idnssoaminimum': [fuzzy_digits],
+                    'idnsallowtransfer': [u'none;'],
+                    'idnsallowquery': [u'any;'],
+                },
+            },
+        ),
+
+        dict(
+            desc='Create reverse zone %r' % revdnszone3,
+            command=(
+                'dnszone_add', [revdnszone3], {
+                    'idnssoamname': dnszone3_mname,
+                    'idnssoarname': dnszone3_rname,
+                }
+            ),
+            expected={
+                'value': revdnszone3,
+                'summary': None,
+                'result': {
+                    'dn': revdnszone3_dn,
+                    'idnsname': [revdnszone3],
+                    'idnszoneactive': [u'TRUE'],
+                    'idnssoamname': [dnszone3_mname],
+                    'nsrecord': [dnszone3_mname],
+                    'idnssoarname': [dnszone3_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=revdnszone3)],
+                    'idnsallowtransfer': [u'none;'],
+                    'idnsallowquery': [u'any;'],
+                    'objectclass': objectclasses.dnszone,
+                },
+            },
+        ),
+
+        dict(
+            desc='Delete reverse zone %r' % revdnszone3,
+            command=('dnszone_del', [revdnszone3], {}),
+            expected={
+                'value': revdnszone3,
+                'summary': u'Deleted DNS zone "%s"' % revdnszone3,
+                'result': {'failed': u''},
+            },
+        ),
+
+        dict(
+            desc='Search for zones with name %r' % dnszone3,
+            command=('dnszone_find', [dnszone3], {}),
+            expected={
+                'summary': None,
+                'count': 1,
+                'truncated': False,
+                'result': [{
+                    'dn': dnszone3_dn,
+                    'idnsname': [dnszone3],
+                    'idnszoneactive': [u'TRUE'],
+                    'nsrecord': [dnszone3_mname],
+                    'idnssoamname': [dnszone3_mname],
+                    'idnssoarname': [dnszone3_rname],
+                    'idnssoaserial': [fuzzy_digits],
+                    'idnssoarefresh': [u'5478'],
+                    'idnssoaretry': [fuzzy_digits],
+                    'idnssoaexpire': [fuzzy_digits],
+                    'idnssoaminimum': [fuzzy_digits],
+                    'idnsallowtransfer': [u'none;'],
+                    'idnsallowquery': [u'any;'],
+                }],
+            },
+        ),
+
+
+        dict(
+            desc='Disable zone %r' % dnszone3,
+            command=('dnszone_disable', [dnszone3], {}),
+            expected={
+                'value': dnszone3,
+                'summary': u'Disabled DNS zone "%s"' % dnszone3,
+                'result': True,
+            },
+        ),
+
+
+        dict(
+            desc='Check if zone %r is really disabled' % dnszone3,
+            command=('dnszone_show', [dnszone3], {}),
+            expected={
+                'value': dnszone3_punycoded,
+                'summary': None,
+                'result': {
+                    'dn': dnszone3_dn,
+                    'idnsname': [dnszone3],
+                    'idnszoneactive': [u'FALSE'],
+                    'nsrecord': [dnszone3_mname],
+                    'idnssoamname': [dnszone3_mname],
+                    'idnssoarname': [dnszone3_rname],
+                    'idnssoaserial': [fuzzy_digits],
+                    'idnssoarefresh': [fuzzy_digits],
+                    'idnssoaretry': [fuzzy_digits],
+                    'idnssoaexpire': [fuzzy_digits],
+                    'idnssoaminimum': [fuzzy_digits],
+                    'idnsallowtransfer': [u'none;'],
+                    'idnsallowquery': [u'any;'],
+                },
+            },
+        ),
+
+
+        dict(
+            desc='Enable zone %r' % dnszone3,
+            command=('dnszone_enable', [dnszone3], {}),
+            expected={
+                'value': dnszone3,
+                'summary': u'Enabled DNS zone "%s"' % dnszone3,
+                'result': True,
+            },
+        ),
+
+
+        dict(
+            desc='Check if zone %r is really enabled' % dnszone3,
+            command=('dnszone_show', [dnszone3], {}),
+            expected={
+                'value': dnszone3_punycoded,
+                'summary': None,
+                'result': {
+                    'dn': dnszone3_dn,
+                    'idnsname': [dnszone3],
+                    'idnszoneactive': [u'TRUE'],
+                    'nsrecord': [dnszone3_mname],
+                    'idnssoamname': [dnszone3_mname],
+                    'idnssoarname': [dnszone3_rname],
+                    'idnssoaserial': [fuzzy_digits],
+                    'idnssoarefresh': [fuzzy_digits],
+                    'idnssoaretry': [fuzzy_digits],
+                    'idnssoaexpire': [fuzzy_digits],
+                    'idnssoaminimum': [fuzzy_digits],
+                    'idnsallowtransfer': [u'none;'],
+                    'idnsallowquery': [u'any;'],
+                },
+            },
+        ),
+
+        dict(
+            desc='Try to retrieve non-existent record %r in zone %r' % (dnsres3, dnszone3),
+            command=('dnsrecord_show', [dnszone3, dnsres3], {}),
+            expected=errors.NotFound(
+                reason=u'%s: DNS resource record not found' % dnsres3),
+        ),
+
+        dict(
+            desc='Create record %r in zone %r' % (dnszone3, dnsres3),
+            command=('dnsrecord_add', [dnszone3, dnsres3], {'arecord': u'127.0.0.1'}),
+            expected={
+                'value': dnsres3_punycoded,
+                'summary': None,
+                'result': {
+                    'dn': dnsres3_dn,
+                    'idnsname': [dnsres3],
+                    'objectclass': objectclasses.dnsrecord,
+                    'arecord': [u'127.0.0.1'],
+                },
+            },
+        ),
+
+
+        dict(
+            desc='Search for all records in zone %r' % dnszone3,
+            command=('dnsrecord_find', [dnszone3], {}),
+            expected={
+                'summary': None,
+                'count': 3,
+                'truncated': False,
+                'result': [
+                    {
+                        'dn': dnszone3_dn,
+                        'nsrecord': (dnszone3_mname,),
+                        'idnsname': [u'@'],
+                    },
+                    {
+                        'dn': dnszone3_mname_dn,
+                        'idnsname': [u'ns1'],
+                        'arecord': [u'1.2.3.4'],
+                    },
+                    {
+                        'dn': dnsres3_dn,
+                        'idnsname': [dnsres3],
+                        'arecord': [u'127.0.0.1'],
+                    },
+                ],
+            },
+        ),
+
+
+        dict(
+            desc='Add A record to %r in zone %r' % (dnsres3, dnszone3),
+            command=('dnsrecord_add', [dnszone3, dnsres3], {'arecord': u'10.10.0.1'}),
+            expected={
+                'value': dnsres3_punycoded,
+                'summary': None,
+                'result': {
+                    'dn': dnsres3_dn,
+                    'idnsname': [dnsres3],
+                    'arecord': [u'127.0.0.1', u'10.10.0.1'],
+                    'objectclass': objectclasses.dnsrecord,
+                },
+            },
+        ),
+
+
+        dict(
+            desc='Remove A record from %r in zone %r' % (dnsres3, dnszone3),
+            command=('dnsrecord_del', [dnszone3, dnsres3], {'arecord': u'127.0.0.1'}),
+            expected={
+                'value': dnsres3_punycoded,
+                'summary': None,
+                'result': {
+                    'idnsname': [dnsres3],
+                    'arecord': [u'10.10.0.1'],
+                },
+            },
+        ),
+
+        dict(
+            desc='Add MX record to zone %r using dnsrecord_add' % (dnszone3),
+            command=('dnsrecord_add', [dnszone3, u'@'], {'mxrecord': u"0 %s" % dnszone3_mname }),
+            expected={
+                'value': u'@',
+                'summary': None,
+                'result': {
+                    'objectclass': objectclasses.dnszone,
+                    'dn': dnszone3_dn,
+                    'idnsname': [u'@'],
+                    'mxrecord': [u"0 %s" % dnszone3_mname],
+                    'nsrecord': [dnszone3_mname],
+                },
+            },
+        ),
+
+        dict(
+            desc='Add KX record to zone %r using dnsrecord_add' % (dnszone3),
+            command=('dnsrecord_add', [dnszone3, u'@'], {'kxrecord': u"0 %s" % dnszone3_mname }),
+            expected={
+                'value': u'@',
+                'summary': None,
+                'result': {
+                    'objectclass': objectclasses.dnszone,
+                    'dn': dnszone3_dn,
+                    'idnsname': [u'@'],
+                    'mxrecord': [u"0 %s" % dnszone3_mname],
+                    'kxrecord': [u"0 %s" % dnszone3_mname],
+                    'nsrecord': [dnszone3_mname],
+                },
+            },
+        ),
+
+        dict(
+            desc='Retrieve raw zone record of zone %r using dnsrecord_show' % (dnszone3),
+            command=('dnsrecord_show', [dnszone3, u'@'], {u'raw' : True}),
+            expected={
+                'value': u'@',
+                'summary': None,
+                'result': {
+                    'dn': dnszone3_dn,
+                    'idnsname': [u'@'],
+                    'mxrecord': [u"0 %s" % dnszone3_mname_punycoded],
+                    'kxrecord': [u"0 %s" % dnszone3_mname_punycoded],
+                    'nsrecord': [dnszone3_mname_punycoded],
+                },
+            },
+        ),
+
+        dict(
+            desc='Add CNAME record to %r using dnsrecord_add' % (dnsrescname3),
+            command=('dnsrecord_add', [dnszone3, dnsrescname3], {'cnamerecord': testdomain3 + u'.'}),
+            expected={
+                'value': dnsrescname3_punycoded,
+                'summary': None,
+                'result': {
+                    'objectclass': objectclasses.dnsrecord,
+                    'dn': dnsrescname3_dn,
+                    'idnsname': [dnsrescname3],
+                    'cnamerecord': [testdomain3 + u'.'],
+                },
+            },
+        ),
+
+        dict(
+            desc='Show raw record %r in zone %r' % (dnsrescname3, dnszone3),
+            command=('dnsrecord_show', [dnszone3, dnsrescname3], {u'raw' : True}),
+            expected={
+                'value': dnsrescname3_punycoded,
+                'summary': None,
+                'result': {
+                    'dn': dnsrescname3_dn,
+                    'idnsname': [dnsrescname3_punycoded],
+                    'cnamerecord': [testdomain3_punycoded + u'.'],
+                },
+            },
+        ),
+
+        dict(
+            desc='Add DNAME record to %r using dnsrecord_add' % (dnsresdname3),
+            command=('dnsrecord_add', [dnszone3, dnsresdname3], {'dnamerecord': testdomain3 + u'.'}),
+            expected={
+                'value': dnsresdname3_punycoded,
+                'summary': None,
+                'result': {
+                    'objectclass': objectclasses.dnsrecord,
+                    'dn': dnsresdname3_dn,
+                    'idnsname': [dnsresdname3],
+                    'dnamerecord': [testdomain3 + u'.'],
+                },
+            },
+        ),
+
+        dict(
+            desc='Show raw record %r in zone %r' % (dnsresdname3, dnszone3),
+            command=('dnsrecord_show', [dnszone3, dnsresdname3], {u'raw' : True}),
+            expected={
+                'value': dnsresdname3_punycoded,
+                'summary': None,
+                'result': {
+                    'dn': dnsresdname3_dn,
+                    'idnsname': [dnsresdname3_punycoded],
+                    'dnamerecord': [testdomain3_punycoded + u'.'],
+                },
+            },
+        ),
+
+        dict(
+            desc='Add SRV record to zone %r using dnsrecord_add' % (dnszone3),
+            command=('dnsrecord_add', [dnszone3, u'_foo._tcp'], {'srvrecord': u"0 100 1234 %s" % dnszone3_mname}),
+            expected={
+                'value': u'_foo._tcp',
+                'summary': None,
+                'result': {
+                    'objectclass': objectclasses.dnsrecord,
+                    'dn': DN(('idnsname', u'_foo._tcp'), dnszone3_dn),
+                    'idnsname': [u'_foo._tcp'],
+                    'srvrecord': [u"0 100 1234 %s" % dnszone3_mname],
+                },
+            },
+        ),
+
+        dict(
+            desc='Show raw record %r in zone %r' % (u'_foo._tcp', dnszone3),
+            command=('dnsrecord_show', [dnszone3, u'_foo._tcp'], {u'raw' : True}),
+            expected={
+                'value': u'_foo._tcp',
+                'summary': None,
+                'result': {
+                    'dn': DN(('idnsname', u'_foo._tcp'), dnszone3_dn),
+                    'idnsname': [u'_foo._tcp'],
+                    'srvrecord': [u"0 100 1234 %s" % dnszone3_mname_punycoded],
+                },
+            },
+        ),
+
+        dict(
+            desc='Add NSEC record to %r using dnsrecord_add' % (dnsresnsec3),
+            command=('dnsrecord_add', [dnszone3, dnsresnsec3], {
+                'nsec_part_next': dnszone3,
+                'nsec_part_types' : [u'TXT', u'A']}),
+            expected={
+                'value': dnsresnsec3_punycoded,
+                'summary': None,
+                'result': {
+                    'objectclass': objectclasses.dnsrecord,
+                    'dn': dnsresnsec3_dn,
+                    'idnsname': [dnsresnsec3],
+                    'nsecrecord': [dnszone3 + u' TXT A'],
+                },
+            },
+        ),
+
+        dict(
+            desc='Show raw record %r in zone %r' % (dnsresnsec3, dnszone3),
+            command=('dnsrecord_show', [dnszone3, dnsresnsec3], {u'raw' : True}),
+            expected={
+                'value': dnsresnsec3_punycoded,
+                'summary': None,
+                'result': {
+                    'dn': dnsresnsec3_dn,
+                    'idnsname': [dnsresnsec3_punycoded],
+                    'nsecrecord': [dnszone3_punycoded + u' TXT A'],
+                },
+            },
+        ),
+
+        dict(
+            desc='Add AFSDB record to %r using dnsrecord_add' % (dnsresafsdb3),
+            command=('dnsrecord_add', [dnszone3, dnsresafsdb3], {
+                'afsdb_part_subtype': 0,
+                'afsdb_part_hostname' : dnszone3_mname}),
+            expected={
+                'value': dnsresafsdb3_punycoded,
+                'summary': None,
+                'result': {
+                    'objectclass': objectclasses.dnsrecord,
+                    'dn': dnsresafsdb3_dn,
+                    'idnsname': [dnsresafsdb3],
+                    'afsdbrecord': [u'0 ' + dnszone3_mname],
+                },
+            },
+        ),
+
+        dict(
+            desc='Show raw record %r in zone %r' % (dnsresnsec3, dnszone3),
+            command=('dnsrecord_show', [dnszone3, dnsresafsdb3], {u'raw' : True}),
+            expected={
+                'value': dnsresafsdb3_punycoded,
+                'summary': None,
+                'result': {
+                    'dn': dnsresafsdb3_dn,
+                    'idnsname': [dnsresafsdb3_punycoded],
+                    'afsdbrecord': [u'0 ' + dnszone3_mname_punycoded],
+                },
+            },
+        ),
+
+        dict(
+            desc='Add A denormalized record to %r in zone %r' % (dnsres3, dnszone3),
+            command=('dnsrecord_add', [dnszone3, u'gro\xdf'], {'arecord': u'172.16.0.1'}),
+            expected=errors.ValidationError(name='name',
+                error=u'domain name \'gro\xdf\' and normalized domain name \'gross\''
+                + ' do not match. Please use only normalized domains'),
+        ),
     ]
-- 
1.8.3.1


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]