[Freeipa-devel] [PATCH] 0207, 0218-0219 Solving trust conflicts and external trust topology fixes

Alexander Bokovoy abokovoy at redhat.com
Mon Aug 15 16:06:48 UTC 2016


On Mon, 15 Aug 2016, Alexander Bokovoy wrote:
>Hi!
>
>Attached are trust-related patches.
>
>0207 is a pre-requisite. I did send it before, it is re-formatting of
>the ipaserver/dcerpc.py to be close to PEP8 requirements.
>
>0218 is an automated trust topology conflict resolver for DNS namespace
>conflicts. Read the commit message for details, and also comments in the
>code. With this patch FreeIPA becomes more smart than Windows Server
>which doesn't have automated trust topology conflict resolution. ;)
>
>0219 fixes issue of topology details leaking through external trust. The
>problem is only in presentation as we store more data than needed -- it
>is impossible to cross external trust boundary anyway as it is handled
>by AD DC side, but one important consequence is that we need to store
>UPN suffixes before we start storing information about child domains.
>Again, read the commit message.
>
>These patches also are on top of my previously sent patches 0215-0216.
Patches attached.

-- 
/ Alexander Bokovoy
-------------- next part --------------
From 356f5b81af065ee808ab953c7103ae536cd0136f Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <abokovoy at redhat.com>
Date: Tue, 7 Jun 2016 22:41:10 +0300
Subject: [PATCH 07/10] ipaserver/dcerpc: reformat to make the code closer to
 pep8

Because Samba Python bindings provide long-named methods and constants,
sometimes it is impossible to fit into 80 columns without causing
damage to readability of the code. This patchset attempts to reduce
pep8 complaints to a minimum.
---
 ipaserver/dcerpc.py | 473 +++++++++++++++++++++++++++++++++-------------------
 1 file changed, 298 insertions(+), 175 deletions(-)

diff --git a/ipaserver/dcerpc.py b/ipaserver/dcerpc.py
index 21ac89d..19be6bf 100644
--- a/ipaserver/dcerpc.py
+++ b/ipaserver/dcerpc.py
@@ -44,10 +44,12 @@ import samba
 import random
 from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
 from cryptography.hazmat.backends import default_backend
+# pylint: disable=F0401
 try:
-    from ldap.controls import RequestControl as LDAPControl #pylint: disable=F0401
+    from ldap.controls import RequestControl as LDAPControl
 except ImportError:
-    from ldap.controls import LDAPControl as LDAPControl    #pylint: disable=F0401
+    from ldap.controls import LDAPControl as LDAPControl
+# pylint: enable=F0401
 import ldap as _ldap
 from ipapython.ipaldap import IPAdmin
 from ipaserver.session import krbccache_dir, krbccache_prefix
@@ -74,7 +76,7 @@ and Samba4 python bindings.
 
 # Both constants can be used as masks against trust direction
 # because bi-directional has two lower bits set.
-TRUST_ONEWAY        = 1
+TRUST_ONEWAY = 1
 TRUST_BIDIRECTIONAL = 3
 
 # Trust join behavior
@@ -91,31 +93,44 @@ def is_sid_valid(sid):
         return True
 
 
-access_denied_error =  errors.ACIError(info=_('CIFS server denied your credentials'))
+access_denied_error = errors.ACIError(
+                          info=_('CIFS server denied your credentials'))
 dcerpc_error_codes = {
     -1073741823:
-        errors.RemoteRetrieveError(reason=_('communication with CIFS server was unsuccessful')),
+        errors.RemoteRetrieveError(
+            reason=_('communication with CIFS server was unsuccessful')),
     -1073741790: access_denied_error,
     -1073741715: access_denied_error,
     -1073741614: access_denied_error,
     -1073741603:
-        errors.ValidationError(name=_('AD domain controller'), error=_('unsupported functional level')),
-    -1073741811: # NT_STATUS_INVALID_PARAMETER
+        errors.ValidationError(
+            name=_('AD domain controller'),
+            error=_('unsupported functional level')),
+    -1073741811:  # NT_STATUS_INVALID_PARAMETER
         errors.RemoteRetrieveError(
-            reason=_('AD domain controller complains about communication sequence. It may mean unsynchronized time on both sides, for example')),
-    -1073741776: # NT_STATUS_INVALID_PARAMETER_MIX, we simply will skip the binding
+            reason=_('AD domain controller complains about communication '
+                     'sequence. It may mean unsynchronized time on both '
+                     'sides, for example')),
+    -1073741776:  # NT_STATUS_INVALID_PARAMETER_MIX,
+                  # we simply will skip the binding
         access_denied_error,
-    -1073741772: # NT_STATUS_OBJECT_NAME_NOT_FOUND
-        errors.RemoteRetrieveError(reason=_('CIFS server configuration does not allow access to \\\\pipe\\lsarpc')),
+    -1073741772:  # NT_STATUS_OBJECT_NAME_NOT_FOUND
+        errors.RemoteRetrieveError(
+            reason=_('CIFS server configuration does not allow '
+                     'access to \\\\pipe\\lsarpc')),
 }
 
 dcerpc_error_messages = {
     "NT_STATUS_OBJECT_NAME_NOT_FOUND":
-         errors.NotFound(reason=_('Cannot find specified domain or server name')),
+        errors.NotFound(
+            reason=_('Cannot find specified domain or server name')),
     "WERR_NO_LOGON_SERVERS":
-         errors.RemoteRetrieveError(reason=_('AD DC was unable to reach any IPA domain controller. Most likely it is a DNS or firewall issue')),
+        errors.RemoteRetrieveError(
+            reason=_('AD DC was unable to reach any IPA domain controller. '
+                     'Most likely it is a DNS or firewall issue')),
     "NT_STATUS_INVALID_PARAMETER_MIX":
-         errors.RequirementError(name=_('At least the domain or IP address should be specified')),
+        errors.RequirementError(
+            name=_('At least the domain or IP address should be specified')),
 }
 
 pysss_type_key_translation_dict = {
@@ -126,7 +141,7 @@ pysss_type_key_translation_dict = {
 }
 
 
-def assess_dcerpc_exception(num=None,message=None):
+def assess_dcerpc_exception(num=None, message=None):
     """
     Takes error returned by Samba bindings and converts it into
     an IPA error class.
@@ -135,8 +150,9 @@ def assess_dcerpc_exception(num=None,message=None):
         return dcerpc_error_codes[num]
     if message and message in dcerpc_error_messages:
         return dcerpc_error_messages[message]
-    reason = _('''CIFS server communication error: code "%(num)s",
-                  message "%(message)s" (both may be "None")''') % dict(num=num, message=message)
+    reason = _('CIFS server communication error: code "%(num)s", '
+               'message "%(message)s" (both may be "None")') % \
+        dict(num=num, message=message)
     return errors.RemoteRetrieveError(reason=reason)
 
 
@@ -182,9 +198,13 @@ class DomainValidator(object):
         self._parm = None
 
     def is_configured(self):
-        cn_trust_local = DN(('cn', self.api.env.domain), self.api.env.container_cifsdomains, self.api.env.basedn)
+        cn_trust_local = DN(('cn', self.api.env.domain),
+                            self.api.env.container_cifsdomains,
+                            self.api.env.basedn)
         try:
-            entry_attrs = self.ldap.get_entry(cn_trust_local, [self.ATTR_FLATNAME, self.ATTR_SID])
+            entry_attrs = self.ldap.get_entry(cn_trust_local,
+                                              [self.ATTR_FLATNAME,
+                                               self.ATTR_SID])
             self.flatname = entry_attrs[self.ATTR_FLATNAME][0]
             self.sid = entry_attrs[self.ATTR_SID][0]
             self.dn = entry_attrs.dn
@@ -203,7 +223,8 @@ class DomainValidator(object):
 
         try:
             search_kw = {'objectClass': 'ipaNTTrustedDomain'}
-            filter = self.ldap.make_filter(search_kw, rules=self.ldap.MATCH_ALL)
+            filter = self.ldap.make_filter(search_kw,
+                                           rules=self.ldap.MATCH_ALL)
             (entries, truncated) = self.ldap.find_entries(
                 filter=filter,
                 base_dn=cn_trust,
@@ -216,22 +237,22 @@ class DomainValidator(object):
             # domain names as keys and those are generally case-insensitive
             result = ipautil.CIDict()
 
-            for entry in entries:
+            for e in entries:
                 try:
-                    trust_partner = entry[self.ATTR_TRUST_PARTNER][0]
-                    flatname_normalized = entry[self.ATTR_FLATNAME][0].lower()
-                    trusted_sid = entry[self.ATTR_TRUSTED_SID][0]
-                except KeyError as e:
+                    t_partner = e.single_value.get(self.ATTR_TRUST_PARTNER)
+                    fname_norm = e.single_value.get(self.ATTR_FLATNAME).lower()
+                    trusted_sid = e.single_value.get(self.ATTR_TRUSTED_SID)
+                except KeyError as exc:
                     # Some piece of trusted domain info in LDAP is missing
                     # Skip the domain, but leave log entry for investigation
                     api.log.warning("Trusted domain '%s' entry misses an "
-                                 "attribute: %s", entry.dn, e)
+                                    "attribute: %s", e.dn, exc)
                     continue
 
-                result[trust_partner] = (flatname_normalized,
-                                         security.dom_sid(trusted_sid))
+                result[t_partner] = (fname_norm,
+                                     security.dom_sid(trusted_sid))
             return result
-        except errors.NotFound as e:
+        except errors.NotFound as exc:
             return []
 
     def set_trusted_domains(self):
@@ -244,21 +265,22 @@ class DomainValidator(object):
             # This means we can't check the correctness of a trusted
             # domain SIDs
             raise errors.ValidationError(name='sid',
-                  error=_('no trusted domain is configured'))
+                                         error=_('no trusted domain '
+                                                 'is configured'))
 
     def get_domain_by_sid(self, sid, exact_match=False):
         if not self.domain:
             # our domain is not configured or self.is_configured() never run
             # reject SIDs as we can't check correctness of them
             raise errors.ValidationError(name='sid',
-                  error=_('domain is not configured'))
+                                         error=_('domain is not configured'))
 
         # Parse sid string to see if it is really in a SID format
         try:
             test_sid = security.dom_sid(sid)
         except TypeError:
             raise errors.ValidationError(name='sid',
-                  error=_('SID is not valid'))
+                                         error=_('SID is not valid'))
 
         # At this point we have SID_NT_AUTHORITY family SID and really need to
         # check it against prefixes of domain SIDs we trust to
@@ -314,30 +336,34 @@ class DomainValidator(object):
             return None
 
     def get_trusted_domain_objects(self, domain=None, flatname=None, filter="",
-            attrs=None, scope=_ldap.SCOPE_SUBTREE, basedn=None):
+                                   attrs=None, scope=_ldap.SCOPE_SUBTREE,
+                                   basedn=None):
         """
-        Search for LDAP objects in a trusted domain specified either by `domain'
-        or `flatname'. The actual LDAP search is specified by `filter', `attrs',
-        `scope' and `basedn'. When `basedn' is empty, database root DN is used.
+        Search for LDAP objects in a trusted domain specified either by
+        `domain' or `flatname'. The actual LDAP search is specified by
+        `filter', `attrs', `scope' and `basedn'. When `basedn' is empty,
+        database root DN is used.
         """
         assert domain is not None or flatname is not None
         """Returns SID for the trusted domain object (user or group only)"""
         if not self.domain:
             # our domain is not configured or self.is_configured() never run
             raise errors.ValidationError(name=_('Trust setup'),
-                error=_('Our domain is not configured'))
+                                         error=_('Our domain is '
+                                                 'not configured'))
         if not self._domains:
             self._domains = self.get_trusted_domains()
         if len(self._domains) == 0:
             # Our domain is configured but no trusted domains are configured
             raise errors.ValidationError(name=_('Trust setup'),
-                error=_('No trusted domain is not configured'))
+                                         error=_('No trusted domain is '
+                                                 'not configured'))
 
         entries = None
         if domain is not None:
             if domain not in self._domains:
                 raise errors.ValidationError(name=_('trusted domain object'),
-                   error= _('domain is not trusted'))
+                                             error=_('domain is not trusted'))
             # Now we have a name to check against our list of trusted domains
             entries = self.search_in_dc(domain, filter, attrs, scope, basedn)
         elif flatname is not None:
@@ -347,53 +373,65 @@ class DomainValidator(object):
             for domain in self._domains:
                 if self._domains[domain][0] == flatname:
                     found_flatname = True
-                    entries = self.search_in_dc(domain, filter, attrs, scope, basedn)
+                    entries = self.search_in_dc(domain, filter,
+                                                attrs, scope, basedn)
                     if entries:
                         break
             if not found_flatname:
                 raise errors.ValidationError(name=_('trusted domain object'),
-                        error= _('no trusted domain matched the specified flat name'))
+                                             error=_('no trusted domain '
+                                                     'matched the specified '
+                                                     'flat name'))
         if not entries:
             raise errors.NotFound(reason=_('trusted domain object not found'))
 
         return entries
 
-    def get_trusted_domain_object_sid(self, object_name, fallback_to_ldap=True):
+    def get_trusted_domain_object_sid(self, object_name,
+                                      fallback_to_ldap=True):
         result = pysss_nss_idmap.getsidbyname(object_name)
-        if object_name in result and (pysss_nss_idmap.SID_KEY in result[object_name]):
+        if object_name in result and \
+           (pysss_nss_idmap.SID_KEY in result[object_name]):
             object_sid = result[object_name][pysss_nss_idmap.SID_KEY]
             return object_sid
 
         # If fallback to AD DC LDAP is not allowed, bail out
         if not fallback_to_ldap:
             raise errors.ValidationError(name=_('trusted domain object'),
-               error= _('SSSD was unable to resolve the object to a valid SID'))
+                                         error=_('SSSD was unable to resolve '
+                                                 'the object to a valid SID'))
 
         # Else, we are going to contact AD DC LDAP
         components = normalize_name(object_name)
         if not ('domain' in components or 'flatname' in components):
             # No domain or realm specified, ambiguous search
-             raise errors.ValidationError(name=_('trusted domain object'),
-                   error= _('Ambiguous search, user domain was not specified'))
+            raise errors.ValidationError(name=_('trusted domain object'),
+                                         error=_('Ambiguous search, user '
+                                                 'domain was not specified'))
 
         attrs = ['objectSid']
-        filter = '(&(sAMAccountName=%(name)s)(|(objectClass=user)(objectClass=group)))' \
-                % dict(name=components['name'])
+        filter = '(&(sAMAccountName=%(name)s)' \
+                 '(|(objectClass=user)(objectClass=group)))' \
+                 % dict(name=components['name'])
         scope = _ldap.SCOPE_SUBTREE
         entries = self.get_trusted_domain_objects(components.get('domain'),
-                components.get('flatname'), filter, attrs, scope)
+                                                  components.get('flatname'),
+                                                  filter, attrs, scope)
 
         if len(entries) > 1:
             # Treat non-unique entries as invalid
             raise errors.ValidationError(name=_('trusted domain object'),
-               error= _('Trusted domain did not return a unique object'))
+                                         error=_('Trusted domain did not '
+                                                 'return a unique object'))
         sid = self.__sid_to_str(entries[0]['objectSid'][0])
         try:
             test_sid = security.dom_sid(sid)
             return unicode(test_sid)
         except TypeError as e:
             raise errors.ValidationError(name=_('trusted domain object'),
-               error= _('Trusted domain did not return a valid SID for the object'))
+                                         error=_('Trusted domain did not '
+                                                 'return a valid SID for '
+                                                 'the object'))
 
     def get_trusted_domain_object_type(self, name_or_sid):
         """
@@ -443,7 +481,8 @@ class DomainValidator(object):
         )
 
         attrs = ['sAMAccountName']
-        filter = (r'(&(objectSid=%(sid)s)(|(objectClass=user)(objectClass=group)))'
+        filter = (r'(&(objectSid=%(sid)s)'
+                  '(|(objectClass=user)(objectClass=group)))'
                   % dict(sid=escaped_sid))  # sid in binary
         domain = self.get_domain_by_sid(sid)
 
@@ -454,7 +493,8 @@ class DomainValidator(object):
         if len(entries) > 1:
             # Treat non-unique entries as invalid
             raise errors.ValidationError(name=_('trusted domain object'),
-               error=_('Trusted domain did not return a unique object'))
+                                         error=_('Trusted domain did not '
+                                                 'return a unique object'))
 
         object_name = (
             "%s@%s" % (entries[0].single_value['sAMAccountName'].lower(),
@@ -486,27 +526,31 @@ class DomainValidator(object):
             # Now search a trusted domain for a user with this SID
             attrs = ['cn']
             filter = '(&(objectClass=user)(objectSid=%(sid)s))' \
-                    % dict(sid=object_name)
+                % dict(sid=object_name)
             try:
-                entries = self.get_trusted_domain_objects(domain=domain, filter=filter,
-                        attrs=attrs, scope=_ldap.SCOPE_SUBTREE)
+                entries = self.get_trusted_domain_objects(domain=domain,
+                                                          filter=filter,
+                                                          attrs=attrs,
+                                                          scope=_ldap.SCOPE_SUBTREE)
             except errors.NotFound:
                 raise errors.NotFound(reason=_('trusted domain user not found'))
             user_dn = entries[0].dn
         elif domain or flatname:
             attrs = ['cn']
             filter = '(&(sAMAccountName=%(name)s)(objectClass=user))' \
-                    % dict(name=name)
+                     % dict(name=name)
             try:
                 entries = self.get_trusted_domain_objects(domain,
-                        flatname, filter, attrs, _ldap.SCOPE_SUBTREE)
+                                                          flatname, filter, attrs,
+                                                          _ldap.SCOPE_SUBTREE)
             except errors.NotFound:
                 raise errors.NotFound(reason=_('trusted domain user not found'))
             user_dn = entries[0].dn
         else:
             # No domain or realm specified, ambiguous search
             raise errors.ValidationError(name=_('trusted domain object'),
-                   error= _('Ambiguous search, user domain was not specified'))
+                                         error=_('Ambiguous search, '
+                                                 'user domain was not specified'))
 
         # Get SIDs of user object and it's groups
         # tokenGroups attribute must be read with a scope BASE for a known user
@@ -514,9 +558,11 @@ class DomainValidator(object):
         attrs = ['objectSID', 'tokenGroups']
         filter = "(objectClass=user)"
         entries = self.get_trusted_domain_objects(domain,
-            flatname, filter, attrs, _ldap.SCOPE_BASE, user_dn)
+                                                  flatname, filter, attrs,
+                                                  _ldap.SCOPE_BASE, user_dn)
         object_sid = self.__sid_to_str(entries[0]['objectSid'][0])
-        group_sids = [self.__sid_to_str(sid) for sid in entries[0]['tokenGroups']]
+        group_sids = [self.__sid_to_str(sid)
+                      for sid in entries[0]['tokenGroups']]
         return (object_sid, group_sids)
 
     def get_trusted_domain_user_and_groups(self, object_name):
@@ -540,11 +586,14 @@ class DomainValidator(object):
         if is_valid_sid:
             object_sid = object_name
             result = pysss_nss_idmap.getnamebysid(object_name)
-            if object_name in result and (pysss_nss_idmap.NAME_KEY in result[object_name]):
-                group_list = pysss.getgrouplist(result[object_name][pysss_nss_idmap.NAME_KEY])
+            if object_name in result and \
+               (pysss_nss_idmap.NAME_KEY in result[object_name]):
+                group_list = pysss.getgrouplist(
+                                 result[object_name][pysss_nss_idmap.NAME_KEY])
         else:
             result = pysss_nss_idmap.getsidbyname(object_name)
-            if object_name in result and (pysss_nss_idmap.SID_KEY in result[object_name]):
+            if object_name in result and \
+               (pysss_nss_idmap.SID_KEY in result[object_name]):
                 object_sid = result[object_name][pysss_nss_idmap.SID_KEY]
                 group_list = pysss.getgrouplist(object_name)
 
@@ -552,7 +601,10 @@ class DomainValidator(object):
             return self.__get_trusted_domain_user_and_groups(object_name)
 
         group_sids = pysss_nss_idmap.getsidbyname(group_list)
-        return (object_sid, [el[1][pysss_nss_idmap.SID_KEY] for el in group_sids.items()])
+        return (
+                object_sid,
+                [el[1][pysss_nss_idmap.SID_KEY] for el in group_sids.items()]
+               )
 
     def __sid_to_str(self, sid):
         """
@@ -561,12 +613,13 @@ class DomainValidator(object):
         """
         sid_rev_num = ord(sid[0])
         number_sub_id = ord(sid[1])
-        ia = struct.unpack('!Q','\x00\x00'+sid[2:8])[0]
+        ia = struct.unpack('!Q', '\x00\x00'+sid[2:8])[0]
         subs = [
-            struct.unpack('<I',sid[8+4*i:12+4*i])[0]
+            struct.unpack('<I', sid[8+4*i:12+4*i])[0]
             for i in range(number_sub_id)
         ]
-        return u'S-%d-%d-%s' % ( sid_rev_num, ia, '-'.join([str(s) for s in subs]),)
+        return u'S-%d-%d-%s' % (sid_rev_num, ia,
+                                '-'.join([str(s) for s in subs]),)
 
     def kinit_as_http(self, domain):
         """
@@ -624,7 +677,7 @@ class DomainValidator(object):
         error on ccache initialization
         """
 
-        if self._admin_creds == None:
+        if self._admin_creds is None:
             return (None, None)
 
         domain_suffix = domain.replace('.', '-')
@@ -691,7 +744,8 @@ class DomainValidator(object):
         ccache_name = None
 
         if self._admin_creds:
-            (ccache_name, principal) = self.kinit_as_administrator(info['dns_domain'])
+            (ccache_name,
+             principal) = self.kinit_as_administrator(info['dns_domain'])
 
         if ccache_name:
             with ipautil.private_ccache(path=ccache_name):
@@ -736,7 +790,7 @@ class DomainValidator(object):
 
         if not self._creds:
             self._parm = param.LoadParm()
-            self._parm.load(os.path.join(ipautil.SHARE_DIR,"smb.conf.empty"))
+            self._parm.load(os.path.join(ipautil.SHARE_DIR, "smb.conf.empty"))
             self._parm.set('netbios name', self.flatname)
             self._creds = credentials.Credentials()
             self._creds.set_kerberos_state(credentials.MUST_USE_KERBEROS)
@@ -746,12 +800,14 @@ class DomainValidator(object):
         netrc = net.Net(creds=self._creds, lp=self._parm)
         finddc_error = None
         result = None
+        flags = nbt.NBT_SERVER_LDAP | nbt.NBT_SERVER_GC | nbt.NBT_SERVER_CLOSEST
         try:
-            result = netrc.finddc(domain=domain, flags=nbt.NBT_SERVER_LDAP | nbt.NBT_SERVER_GC | nbt.NBT_SERVER_CLOSEST)
+            result = netrc.finddc(domain=domain, flags=flags)
         except RuntimeError as e:
             try:
                 # If search of closest GC failed, attempt to find any one
-                result = netrc.finddc(domain=domain, flags=nbt.NBT_SERVER_LDAP | nbt.NBT_SERVER_GC)
+                flags = nbt.NBT_SERVER_LDAP | nbt.NBT_SERVER_GC
+                result = netrc.finddc(domain=domain, flags=flags)
             except RuntimeError as e:
                 finddc_error = e
 
@@ -789,6 +845,7 @@ class DomainValidator(object):
         self._info[domain] = info
         return info
 
+
 def string_to_array(what):
     return [ord(v) for v in what]
 
@@ -797,7 +854,7 @@ class TrustDomainInstance(object):
 
     def __init__(self, hostname, creds=None):
         self.parm = param.LoadParm()
-        self.parm.load(os.path.join(ipautil.SHARE_DIR,"smb.conf.empty"))
+        self.parm.load(os.path.join(ipautil.SHARE_DIR, "smb.conf.empty"))
         if len(hostname) > 0:
             self.parm.set('netbios name', hostname)
         self.creds = creds
@@ -810,14 +867,14 @@ class TrustDomainInstance(object):
         self.validation_attempts = 0
 
     def __gen_lsa_connection(self, binding):
-       if self.creds is None:
-           raise errors.RequirementError(name=_('CIFS credentials object'))
-       try:
-           result = lsa.lsarpc(binding, self.parm, self.creds)
-           return result
-       except RuntimeError as e:
-           num, message = e.args  # pylint: disable=unpacking-non-sequence
-           raise assess_dcerpc_exception(num=num, message=message)
+        if self.creds is None:
+            raise errors.RequirementError(name=_('CIFS credentials object'))
+        try:
+            result = lsa.lsarpc(binding, self.parm, self.creds)
+            return result
+        except RuntimeError as e:
+            num, message = e.args  # pylint: disable=unpacking-non-sequence
+            raise assess_dcerpc_exception(num=num, message=message)
 
     def init_lsa_pipe(self, remote_host):
         """
@@ -847,30 +904,35 @@ class TrustDomainInstance(object):
                 # When session key is not available, we just skip this binding
                 session_attempts = session_attempts + 1
 
-        if self._pipe is None and (attempts + session_attempts) == len(bindings):
+        if self._pipe is None and \
+           (attempts + session_attempts) == len(bindings):
             raise errors.ACIError(
-                info=_('CIFS server %(host)s denied your credentials') % dict(host=remote_host))
+                      info=_('CIFS server %(host)s denied your credentials')
+                      % dict(host=remote_host))
 
         if self._pipe is None:
             raise errors.RemoteRetrieveError(
-                reason=_('Cannot establish LSA connection to %(host)s. Is CIFS server running?') % dict(host=remote_host))
+                    reason=_('Cannot establish LSA connection to %(host)s. '
+                             'Is CIFS server running?') % dict(host=remote_host))
         self.binding = binding
         self.session_key = self._pipe.session_key
 
     def __gen_lsa_bindings(self, remote_host):
         """
-        There are multiple transports to issue LSA calls. However, depending on a
-        system in use they may be blocked by local operating system policies.
+        There are multiple transports to issue LSA calls. However, depending on
+        a system in use they may be blocked by local operating system policies.
         Generate all we can use. init_lsa_pipe() will try them one by one until
         there is one working.
 
-        We try NCACN_NP before NCACN_IP_TCP and use SMB2 before SMB1 or defaults.
+        We try NCACN_NP before NCACN_IP_TCP and use SMB2 before SMB1.
         """
         transports = (u'ncacn_np', u'ncacn_ip_tcp')
-        options = ( u'smb2,print', u'print')
-        return [u'%s:%s[%s]' % (t, remote_host, o) for t in transports for o in options]
+        options = (u'smb2,print', u'print')
+        return [u'%s:%s[%s]' % (t, remote_host, o)
+                for t in transports for o in options]
 
-    def retrieve_anonymously(self, remote_host, discover_srv=False, search_pdc=False):
+    def retrieve_anonymously(self, remote_host,
+                             discover_srv=False, search_pdc=False):
         """
         When retrieving DC information anonymously, we can't get SID of the domain
         """
@@ -896,7 +958,8 @@ class TrustDomainInstance(object):
         self.info['is_pdc'] = (result.server_type & nbt.NBT_SERVER_PDC) != 0
 
         # Netlogon response doesn't contain SID of the domain.
-        # We need to do rootDSE search with LDAP_SERVER_EXTENDED_DN_OID control to reveal the SID
+        # We need to do rootDSE search with LDAP_SERVER_EXTENDED_DN_OID
+        # control to reveal the SID
         ldap_uri = 'ldap://%s' % (result.pdc_dns_name)
         conn = _ldap.initialize(ldap_uri)
         conn.set_option(_ldap.OPT_SERVER_CONTROLS, [ExtendedDNControl()])
@@ -908,7 +971,7 @@ class TrustDomainInstance(object):
         except _ldap.LDAPError as e:
             root_logger.error(
                 "LDAP error when connecting to %(host)s: %(error)s" %
-                    dict(host=unicode(result.pdc_name), error=str(e)))
+                dict(host=unicode(result.pdc_name), error=str(e)))
         except KeyError as e:
             root_logger.error("KeyError: {err}, LDAP entry from {host} "
                               "returned malformed. Your DNS might be "
@@ -930,8 +993,11 @@ class TrustDomainInstance(object):
         objectAttribute = lsa.ObjectAttribute()
         objectAttribute.sec_qos = lsa.QosInfo()
         try:
-            self._policy_handle = self._pipe.OpenPolicy2(u"", objectAttribute, security.SEC_FLAG_MAXIMUM_ALLOWED)
-            result = self._pipe.QueryInfoPolicy2(self._policy_handle, lsa.LSA_POLICY_INFO_DNS)
+            self._policy_handle = \
+                self._pipe.OpenPolicy2(u"", objectAttribute,
+                                       security.SEC_FLAG_MAXIMUM_ALLOWED)
+            result = self._pipe.QueryInfoPolicy2(self._policy_handle,
+                                                 lsa.LSA_POLICY_INFO_DNS)
         except RuntimeError as e:
             num, message = e.args  # pylint: disable=unpacking-non-sequence
             raise assess_dcerpc_exception(num=num, message=message)
@@ -944,7 +1010,8 @@ class TrustDomainInstance(object):
         self.info['dc'] = remote_host
 
         try:
-            result = self._pipe.QueryInfoPolicy2(self._policy_handle, lsa.LSA_POLICY_INFO_ROLE)
+            result = self._pipe.QueryInfoPolicy2(self._policy_handle,
+                                                 lsa.LSA_POLICY_INFO_ROLE)
         except RuntimeError as e:
             num, message = e.args  # pylint: disable=unpacking-non-sequence
             raise assess_dcerpc_exception(num=num, message=message)
@@ -958,18 +1025,18 @@ class TrustDomainInstance(object):
         clear_value.size = len(password_blob)
         clear_value.password = password_blob
 
-        clear_authentication_information = drsblobs.AuthenticationInformation()
-        clear_authentication_information.LastUpdateTime = samba.unix2nttime(int(time.time()))
-        clear_authentication_information.AuthType = lsa.TRUST_AUTH_TYPE_CLEAR
-        clear_authentication_information.AuthInfo = clear_value
+        clear_authinfo = drsblobs.AuthenticationInformation()
+        clear_authinfo.LastUpdateTime = samba.unix2nttime(int(time.time()))
+        clear_authinfo.AuthType = lsa.TRUST_AUTH_TYPE_CLEAR
+        clear_authinfo.AuthInfo = clear_value
 
-        authentication_information_array = drsblobs.AuthenticationInformationArray()
-        authentication_information_array.count = 1
-        authentication_information_array.array = [clear_authentication_information]
+        authinfo_array = drsblobs.AuthenticationInformationArray()
+        authinfo_array.count = 1
+        authinfo_array.array = [clear_authinfo]
 
         outgoing = drsblobs.trustAuthInOutBlob()
         outgoing.count = 1
-        outgoing.current = authentication_information_array
+        outgoing.current = authinfo_array
 
         confounder = [3]*512
         for i in range(512):
@@ -983,7 +1050,8 @@ class TrustDomainInstance(object):
 
         trustpass_blob = ndr_pack(trustpass)
 
-        encrypted_trustpass = arcfour_encrypt(self._pipe.session_key, trustpass_blob)
+        encrypted_trustpass = arcfour_encrypt(self._pipe.session_key,
+                                              trustpass_blob)
 
         auth_blob = lsa.DATA_BUF2()
         auth_blob.size = len(encrypted_trustpass)
@@ -993,7 +1061,6 @@ class TrustDomainInstance(object):
         auth_info.auth_blob = auth_blob
         self.auth_info = auth_info
 
-
     def generate_ftinfo(self, another_domain):
         """
         Generates TrustDomainInfoFullInfo2Internal structure
@@ -1032,27 +1099,38 @@ class TrustDomainInstance(object):
                 # smbd already has the information about itself
                 ldname = lsa.StringLarge()
                 ldname.string = another_domain.info['dns_domain']
-                collision_info = self._pipe.lsaRSetForestTrustInformation(self._policy_handle,
-                                                                          ldname,
-                                                                          lsa.LSA_FOREST_TRUST_DOMAIN_INFO,
-                                                                          ftinfo, 0)
-                if collision_info:
-                    root_logger.error("When setting forest trust information, got collision info back:\n%s" % (ndr_print(collision_info)))
+                ftlevel = lsa.LSA_FOREST_TRUST_DOMAIN_INFO
+                # RSetForestTrustInformation returns collision information
+                # for trust topology
+                cinfo = self._pipe.lsaRSetForestTrustInformation(
+                            self._policy_handle,
+                            ldname,
+                            ftlevel,
+                            ftinfo, 0)
+                if cinfo:
+                    root_logger.error("When setting forest trust information, "
+                                      "got collision info back:\n%s"
+                                      % (ndr_print(cinfo)))
         except RuntimeError as e:
-            # We can ignore the error here -- setting up name suffix routes may fail
+            # We can ignore the error here --
+            # setting up name suffix routes may fail
             pass
 
-    def establish_trust(self, another_domain, trustdom_secret, trust_type='bidirectional', trust_external=False):
+    def establish_trust(self, another_domain, trustdom_secret,
+                        trust_type='bidirectional', trust_external=False):
         """
         Establishes trust between our and another domain
-        Input: another_domain -- instance of TrustDomainInstance, initialized with #retrieve call
+        Input: another_domain -- instance of TrustDomainInstance,
+                                 initialized with #retrieve call
                trustdom_secret -- shared secred used for the trust
         """
         if self.info['name'] == another_domain.info['name']:
             # Check that NetBIOS names do not clash
             raise errors.ValidationError(name=u'AD Trust Setup',
-                    error=_('the IPA server and the remote domain cannot share the same '
-                            'NetBIOS name: %s') % self.info['name'])
+                                         error=_('the IPA server and the '
+                                                 'remote domain cannot share '
+                                                 'the same NetBIOS name: %s')
+                                         % self.info['name'])
 
         self.generate_auth(trustdom_secret)
 
@@ -1071,8 +1149,12 @@ class TrustDomainInstance(object):
         try:
             dname = lsa.String()
             dname.string = another_domain.info['dns_domain']
-            res = self._pipe.QueryTrustedDomainInfoByName(self._policy_handle, dname, lsa.LSA_TRUSTED_DOMAIN_INFO_FULL_INFO)
-            self._pipe.DeleteTrustedDomain(self._policy_handle, res.info_ex.sid)
+            res = self._pipe.QueryTrustedDomainInfoByName(
+                                self._policy_handle,
+                                dname,
+                                lsa.LSA_TRUSTED_DOMAIN_INFO_FULL_INFO)
+            self._pipe.DeleteTrustedDomain(self._policy_handle,
+                                           res.info_ex.sid)
         except RuntimeError as e:
             num, message = e.args  # pylint: disable=unpacking-non-sequence
             # Ignore anything but access denied (NT_STATUS_ACCESS_DENIED)
@@ -1080,7 +1162,10 @@ class TrustDomainInstance(object):
                 raise access_denied_error
 
         try:
-            trustdom_handle = self._pipe.CreateTrustedDomainEx2(self._policy_handle, info, self.auth_info, security.SEC_STD_DELETE)
+            trustdom_handle = self._pipe.CreateTrustedDomainEx2(
+                                           self._policy_handle,
+                                           info, self.auth_info,
+                                           security.SEC_STD_DELETE)
         except RuntimeError as e:
             num, message = e.args  # pylint: disable=unpacking-non-sequence
             raise assess_dcerpc_exception(num=num, message=message)
@@ -1089,13 +1174,19 @@ class TrustDomainInstance(object):
         # trust settings. Samba insists this has to be done with LSA
         # OpenTrustedDomain* calls, it is not enough to have a handle
         # returned by the CreateTrustedDomainEx2 call.
-        trustdom_handle = self._pipe.OpenTrustedDomainByName(self._policy_handle, dname, security.SEC_FLAG_MAXIMUM_ALLOWED)
+        trustdom_handle = self._pipe.OpenTrustedDomainByName(
+                                         self._policy_handle,
+                                         dname,
+                                         security.SEC_FLAG_MAXIMUM_ALLOWED)
         try:
-            infoclass = lsa.TrustDomainInfoSupportedEncTypes()
-            infoclass.enc_types = security.KERB_ENCTYPE_RC4_HMAC_MD5
-            infoclass.enc_types |= security.KERB_ENCTYPE_AES128_CTS_HMAC_SHA1_96
-            infoclass.enc_types |= security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96
-            self._pipe.SetInformationTrustedDomain(trustdom_handle, lsa.LSA_TRUSTED_DOMAIN_SUPPORTED_ENCRYPTION_TYPES, infoclass)
+            infocls = lsa.TrustDomainInfoSupportedEncTypes()
+            infocls.enc_types = security.KERB_ENCTYPE_RC4_HMAC_MD5
+            infocls.enc_types |= security.KERB_ENCTYPE_AES128_CTS_HMAC_SHA1_96
+            infocls.enc_types |= security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96
+            self._pipe.SetInformationTrustedDomain(
+                           trustdom_handle,
+                           lsa.LSA_TRUSTED_DOMAIN_SUPPORTED_ENCRYPTION_TYPES,
+                           infocls)
         except RuntimeError as e:
             # We can ignore the error here -- changing enctypes is for
             # improved security but the trust will work with default values as
@@ -1105,13 +1196,16 @@ class TrustDomainInstance(object):
 
         if not trust_external:
             try:
-                info = self._pipe.QueryTrustedDomainInfo(trustdom_handle,
-                                                         lsa.LSA_TRUSTED_DOMAIN_INFO_INFO_EX)
+                info = self._pipe.QueryTrustedDomainInfo(
+                                      trustdom_handle,
+                                      lsa.LSA_TRUSTED_DOMAIN_INFO_INFO_EX)
                 info.trust_attributes |= lsa.LSA_TRUST_ATTRIBUTE_FOREST_TRANSITIVE
-                self._pipe.SetInformationTrustedDomain(trustdom_handle,
-                                                       lsa.LSA_TRUSTED_DOMAIN_INFO_INFO_EX, info)
+                self._pipe.SetInformationTrustedDomain(
+                                      trustdom_handle,
+                                      lsa.LSA_TRUSTED_DOMAIN_INFO_INFO_EX, info)
             except RuntimeError as e:
-                root_logger.error('unable to set trust transitivity status: %s' % (str(e)))
+                root_logger.error(
+                      'unable to set trust transitivity status: %s' % (str(e)))
 
         if self.info['is_pdc'] or trust_external:
             self.update_ftinfo(another_domain)
@@ -1119,12 +1213,13 @@ class TrustDomainInstance(object):
     def verify_trust(self, another_domain):
         def retrieve_netlogon_info_2(logon_server, domain, function_code, data):
             try:
-                netr_pipe = netlogon.netlogon(domain.binding, domain.parm, domain.creds)
-                result = netr_pipe.netr_LogonControl2Ex(logon_server=logon_server,
+                netr_pipe = netlogon.netlogon(domain.binding,
+                                              domain.parm, domain.creds)
+                result = netr_pipe.netr_LogonControl2Ex(
+                                           logon_server=logon_server,
                                            function_code=function_code,
                                            level=2,
-                                           data=data
-                                           )
+                                           data=data)
                 return result
             except RuntimeError as e:
                 num, message = e.args  # pylint: disable=unpacking-non-sequence
@@ -1135,9 +1230,11 @@ class TrustDomainInstance(object):
                                           another_domain.info['dns_domain'])
 
         if result and result.flags and netlogon.NETLOGON_VERIFY_STATUS_RETURNED:
-            if result.pdc_connection_status[0] != 0 and result.tc_connection_status[0] != 0:
+            if result.pdc_connection_status[0] != 0 and \
+               result.tc_connection_status[0] != 0:
                 if result.pdc_connection_status[1] == "WERR_ACCESS_DENIED":
-                    # Most likely AD DC hit another IPA replica which yet has no trust secret replicated
+                    # Most likely AD DC hit another IPA replica which
+                    # yet has no trust secret replicated
 
                     # Sleep and repeat again
                     self.validation_attempts += 1
@@ -1176,23 +1273,23 @@ class TrustDomainInstance(object):
 
 def fetch_domains(api, mydomain, trustdomain, creds=None, server=None):
     trust_flags = dict(
-                NETR_TRUST_FLAG_IN_FOREST = 0x00000001,
-                NETR_TRUST_FLAG_OUTBOUND  = 0x00000002,
-                NETR_TRUST_FLAG_TREEROOT  = 0x00000004,
-                NETR_TRUST_FLAG_PRIMARY   = 0x00000008,
-                NETR_TRUST_FLAG_NATIVE    = 0x00000010,
-                NETR_TRUST_FLAG_INBOUND   = 0x00000020,
-                NETR_TRUST_FLAG_MIT_KRB5  = 0x00000080,
-                NETR_TRUST_FLAG_AES       = 0x00000100)
+                NETR_TRUST_FLAG_IN_FOREST=0x00000001,
+                NETR_TRUST_FLAG_OUTBOUND=0x00000002,
+                NETR_TRUST_FLAG_TREEROOT=0x00000004,
+                NETR_TRUST_FLAG_PRIMARY=0x00000008,
+                NETR_TRUST_FLAG_NATIVE=0x00000010,
+                NETR_TRUST_FLAG_INBOUND=0x00000020,
+                NETR_TRUST_FLAG_MIT_KRB5=0x00000080,
+                NETR_TRUST_FLAG_AES=0x00000100)
 
     trust_attributes = dict(
-                NETR_TRUST_ATTRIBUTE_NON_TRANSITIVE     = 0x00000001,
-                NETR_TRUST_ATTRIBUTE_UPLEVEL_ONLY       = 0x00000002,
-                NETR_TRUST_ATTRIBUTE_QUARANTINED_DOMAIN = 0x00000004,
-                NETR_TRUST_ATTRIBUTE_FOREST_TRANSITIVE  = 0x00000008,
-                NETR_TRUST_ATTRIBUTE_CROSS_ORGANIZATION = 0x00000010,
-                NETR_TRUST_ATTRIBUTE_WITHIN_FOREST      = 0x00000020,
-                NETR_TRUST_ATTRIBUTE_TREAT_AS_EXTERNAL  = 0x00000040)
+                NETR_TRUST_ATTRIBUTE_NON_TRANSITIVE=0x00000001,
+                NETR_TRUST_ATTRIBUTE_UPLEVEL_ONLY=0x00000002,
+                NETR_TRUST_ATTRIBUTE_QUARANTINED_DOMAIN=0x00000004,
+                NETR_TRUST_ATTRIBUTE_FOREST_TRANSITIVE=0x00000008,
+                NETR_TRUST_ATTRIBUTE_CROSS_ORGANIZATION=0x00000010,
+                NETR_TRUST_ATTRIBUTE_WITHIN_FOREST=0x00000020,
+                NETR_TRUST_ATTRIBUTE_TREAT_AS_EXTERNAL=0x00000040)
 
     def communicate(td):
         td.init_lsa_pipe(td.info['dc'])
@@ -1314,7 +1411,8 @@ class TrustDomainJoins(object):
         ld.retrieve(installutils.get_fqdn())
         self.local_domain = ld
 
-    def populate_remote_domain(self, realm, realm_server=None, realm_admin=None, realm_passwd=None):
+    def populate_remote_domain(self, realm, realm_server=None,
+                               realm_admin=None, realm_passwd=None):
         def get_instance(self):
             # Fetch data from foreign domain using password only
             rd = TrustDomainInstance('')
@@ -1330,7 +1428,8 @@ class TrustDomainJoins(object):
         if realm_server is None:
             rd.retrieve_anonymously(realm, discover_srv=True, search_pdc=True)
         else:
-            rd.retrieve_anonymously(realm_server, discover_srv=False, search_pdc=True)
+            rd.retrieve_anonymously(realm_server,
+                                    discover_srv=False, search_pdc=True)
         rd.read_only = True
         if realm_admin and realm_passwd:
             if 'name' in rd.info:
@@ -1339,12 +1438,14 @@ class TrustDomainJoins(object):
                     # realm admin is in DOMAIN\user format
                     # strip DOMAIN part as we'll enforce the one discovered
                     realm_admin = names[-1]
-                auth_string = u"%s\%s%%%s" % (rd.info['name'], realm_admin, realm_passwd)
+                auth_string = u"%s\%s%%%s" \
+                              % (rd.info['name'], realm_admin, realm_passwd)
                 td = get_instance(self)
                 td.creds.parse_string(auth_string)
                 td.creds.set_workstation(self.local_domain.hostname)
                 if realm_server is None:
-                    # we must have rd.info['dns_hostname'] then, part of anonymous discovery
+                    # we must have rd.info['dns_hostname'] then
+                    # as it is part of the anonymous discovery
                     td.retrieve(rd.info['dns_hostname'])
                 else:
                     td.retrieve(realm_server)
@@ -1358,8 +1459,8 @@ class TrustDomainJoins(object):
         """
         Generate list of records for forest trust information about
         our realm domains. Note that the list generated currently
-        includes only top level domains, no exclusion domains, and no TDO objects
-        as we handle the latter in a separate way
+        includes only top level domains, no exclusion domains, and
+        no TDO objects as we handle the latter in a separate way
         """
         if self.local_domain.read_only:
             return
@@ -1367,10 +1468,15 @@ class TrustDomainJoins(object):
         self.local_domain.ftinfo_records = []
 
         realm_domains = self.api.Command.realmdomains_show()['result']
-        # Use realmdomains' modification timestamp to judge records last update time
-        entry = self.api.Backend.ldap2.get_entry(realm_domains['dn'], ['modifyTimestamp'])
+        # Use realmdomains' modification timestamp
+        # to judge records' last update time
+        entry = self.api.Backend.ldap2.get_entry(
+                    realm_domains['dn'], ['modifyTimestamp'])
         # Convert the timestamp to Windows 64-bit timestamp format
-        trust_timestamp = long(time.mktime(entry['modifytimestamp'][0].timetuple())*1e7+116444736000000000)
+        trust_timestamp = long(
+                time.mktime(
+                     entry.single_value.get('modifytimestamp').timetuple()
+                )*1e7+116444736000000000)
 
         for dom in realm_domains['associateddomain']:
             ftinfo = dict()
@@ -1379,7 +1485,8 @@ class TrustDomainJoins(object):
             ftinfo['rec_type'] = lsa.LSA_FOREST_TRUST_TOP_LEVEL_NAME
             self.local_domain.ftinfo_records.append(ftinfo)
 
-    def join_ad_full_credentials(self, realm, realm_server, realm_admin, realm_passwd, trust_type):
+    def join_ad_full_credentials(self, realm, realm_server, realm_admin,
+                                 realm_passwd, trust_type):
         if not self.configured:
             return None
 
@@ -1392,24 +1499,33 @@ class TrustDomainJoins(object):
             )
 
         trust_external = bool(self.__allow_behavior & TRUST_JOIN_EXTERNAL)
-        if self.remote_domain.info['dns_domain'] != self.remote_domain.info['dns_forest']:
+        if self.remote_domain.info['dns_domain'] != \
+           self.remote_domain.info['dns_forest']:
             if not trust_external:
-                raise errors.NotAForestRootError(forest=self.remote_domain.info['dns_forest'],
-                                                 domain=self.remote_domain.info['dns_domain'])
+                raise errors.NotAForestRootError(
+                          forest=self.remote_domain.info['dns_forest'],
+                          domain=self.remote_domain.info['dns_domain'])
 
         if not self.remote_domain.read_only:
             trustdom_pass = samba.generate_random_password(128, 128)
             self.get_realmdomains()
             self.remote_domain.establish_trust(self.local_domain,
-                                               trustdom_pass, trust_type, trust_external)
+                                               trustdom_pass,
+                                               trust_type, trust_external)
             self.local_domain.establish_trust(self.remote_domain,
-                                              trustdom_pass, trust_type, trust_external)
-            # if trust is inbound, we don't need to verify it because AD DC will respond
-            # with WERR_NO_SUCH_DOMAIN -- in only does verification for outbound trusts.
+                                              trustdom_pass,
+                                              trust_type, trust_external)
+            # if trust is inbound, we don't need to verify it because
+            # AD DC will respond with WERR_NO_SUCH_DOMAIN --
+            # it only does verification for outbound trusts.
             result = True
             if trust_type == TRUST_BIDIRECTIONAL:
                 result = self.remote_domain.verify_trust(self.local_domain)
-            return dict(local=self.local_domain, remote=self.remote_domain, verified=result)
+            return dict(
+                        local=self.local_domain,
+                        remote=self.remote_domain,
+                        verified=result
+                       )
         return None
 
     def join_ad_ipa_half(self, realm, realm_server, trustdom_passwd, trust_type):
@@ -1420,11 +1536,18 @@ class TrustDomainJoins(object):
             self.populate_remote_domain(realm, realm_server, realm_passwd=None)
 
         trust_external = bool(self.__allow_behavior & TRUST_JOIN_EXTERNAL)
-        if self.remote_domain.info['dns_domain'] != self.remote_domain.info['dns_forest']:
+        if self.remote_domain.info['dns_domain'] != \
+           self.remote_domain.info['dns_forest']:
             if not trust_external:
-                raise errors.NotAForestRootError(forest=self.remote_domain.info['dns_forest'],
-                                                 domain=self.remote_domain.info['dns_domain'])
+                raise errors.NotAForestRootError(
+                          forest=self.remote_domain.info['dns_forest'],
+                          domain=self.remote_domain.info['dns_domain'])
 
         self.local_domain.establish_trust(self.remote_domain,
-                                          trustdom_passwd, trust_type, trust_external)
-        return dict(local=self.local_domain, remote=self.remote_domain, verified=False)
+                                          trustdom_passwd,
+                                          trust_type, trust_external)
+        return dict(
+                    local=self.local_domain,
+                    remote=self.remote_domain,
+                    verified=False
+                   )
-- 
2.7.4

-------------- next part --------------
From 90b9e338994255155294614e8c21ca070c59e1a6 Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <abokovoy at redhat.com>
Date: Mon, 15 Aug 2016 18:14:00 +0300
Subject: [PATCH 09/10] trust: automatically resolve DNS trust conflicts for
 triangle trusts

For configuration where:
  - AD example.com trusts IPA at ipa.example.com
  - AD example.org trusts AD example.com
  - a trust is tried to be established between ipa.example.com and
    example.org,

there will be a trust topology conflict detected by example.org domain
controller because ipa.example.com DNS namespace overlaps with
example.com DNS namespace.

This type of trust topology conflict is documented in MS-ADTS 6.1.6.9.3.2
"Building Well-Formed msDS-TrustForestTrustInfo Message". A similar
conflict can arise for SID and NetBIOS namespaces. However, unlike SID
and NetBIOS namespaces, we can solve DNS namespace conflict
automatically if there are administrative credentials for example.org
available.

A manual sequence to solve the DNS namespace conflict is described in
https://msdn.microsoft.com/it-it/library/cc786254%28v=ws.10%29.aspx.
This sequence boils down to the following steps:

   1. As an administrator of the example.org, you need to add an
exclusion entry for ipa.example.com in the properties of the trust to
example.com
   2. Establish trust between ipa.example.com and example.org

It is important to add the exclusion entry before step 4 or there will
be conflict recorded which cannot be cleared easily right now due to a
combination of bugs in both IPA and Active Directory.

This patchset implements automated solution for the case when we have
access to the example.org's administrator credentials:

   1. Attempt to establish trust and update trust topology information.
   2. If trust topology conflict is detected as result of (1):
   2.1. Fetch trust topology infromation for the conflicting forest
        trust
   2.2. Add exclusion entry to our domain to the trust topology obtained
        in (2.1)
   2.3. Update trust topology for the conflicting forest trust
   3. Re-establish trust between ipa.example.com and example.org

We cannot do the same for shared secret trust and for external trust,
though:

   1. For shared secret trust we don't have administrative credentials
      in the forest reporting the conflict

   2. For the external trust we cannot set topology information due to
      MS-LSAD 3.1.4.7.16 because external trust is non-transitive by
      definition and thus setting topology information will fail.

To test this logic one can use two Samba AD forests with FreeIPA
using a sub-domain of one of them.

Fixes: https://fedorahosted.org/freeipa/ticket/6076
---
 ipalib/errors.py    |  34 +++++++++
 ipaserver/dcerpc.py | 214 +++++++++++++++++++++++++++++++++++++++++++++-------
 2 files changed, 220 insertions(+), 28 deletions(-)

diff --git a/ipalib/errors.py b/ipalib/errors.py
index 7b4f15d..0e351b5 100644
--- a/ipalib/errors.py
+++ b/ipalib/errors.py
@@ -866,6 +866,40 @@ class NotAForestRootError(InvocationError):
     errno = 3016
     format = _("Domain '%(domain)s' is not a root domain for forest '%(forest)s'")
 
+class TrustTopologyConflictError(InvocationError):
+    """
+    **3017** Raised when an attempt to establish trust fails with a topology
+             conflict against another forest the target forest trusts
+
+    For example:
+
+    >>> raise TrustTopologyConflictError(forest='example.test',
+                                        conflict='my.ad.test',
+                                        domains=['ad.test'])
+    Traceback (most recent call last):
+      ...
+    TrustTopologyConflictError: Forest 'example.test' has existing trust to forest(s) ['ad.test'] which prevents a trust to 'my.ad.test'
+    """
+
+    errno = 3017
+    format = _("Forest '%(forest)s' has existing trust to forest(s) %(domains)s which prevents a trust to '%(conflict)s'")
+
+class TrustTopologyConflictSolved(InvocationError):
+    """
+    **3018** Raised when an attempt to establish trust fails with a topology
+             conflict against another forest the target forest trusts
+
+    For example:
+
+    >>> raise TrustTopologyConflictError(forest='example.test',
+                                        conflict='my.ad.test')
+    Traceback (most recent call last):
+      ...
+    TrustTopologyConflictSolved: When adding forest trust between 'my.ad.test' and 'example.test', a topology conflict was solved
+    """
+
+    errno = 3018
+    format = _("When adding forest trust between '%(conflict)s' and '%(forest)s', a topology conflict was solved")
 
 ##############################################################################
 # 4000 - 4999: Execution errors
diff --git a/ipaserver/dcerpc.py b/ipaserver/dcerpc.py
index 19be6bf..e9e9e1e 100644
--- a/ipaserver/dcerpc.py
+++ b/ipaserver/dcerpc.py
@@ -1,7 +1,7 @@
 # Authors:
 #     Alexander Bokovoy <abokovoy at redhat.com>
 #
-# Copyright (C) 2011  Red Hat
+# Copyright (C) 2011-2016  Red Hat
 # see file 'COPYING' for use and warranty information
 #
 # Portions (C) Andrew Tridgell, Andrew Bartlett
@@ -1087,34 +1087,168 @@ class TrustDomainInstance(object):
         info.entries = ftinfo_records
         return info
 
+    def clear_ftinfo_conflict(self, another_domain, cinfo):
+        """
+        Attempt to clean up the forest trust collisions
+
+        This code tries to perform intelligent job of going
+        over individual collisions and making exclusion entries
+        for affected IPA namespaces.
+
+        There are three possible conflict configurations:
+          - conflict of DNS namespace (TLN conflict, LSA_TLN_DISABLED_CONFLICT)
+          - conflict of SID namespace (LSA_SID_DISABLED_CONFLICT)
+          - conflict of NetBIOS namespace (LSA_NB_DISABLED_CONFLICT)
+
+        we only can handle TLN conflicts because (a) excluding SID namespace
+        is not possible and (b) excluding NetBIOS namespace not possible.
+        These two types of conflicts should result in trust-add CLI error
+
+        These conflicts can come from external source (another forest) or
+        from internal source (another domain in the same forest). We only
+        can fix the problems with another forest.
+
+        To resolve TLN conflict we need to do following:
+          1. Retrieve forest trust information for the forest we conflict on
+          2. Add an exclusion entry for IPA DNS namespace to it
+          3. Set forest trust information for the forest we conflict on
+          4. Re-try establishing trust to the original forest
+
+        This all can only be done under privileges of Active Directory admin
+        that can change forest trusts. If we cannot have those privileges,
+        the work has to be done manually in the Windows UI for
+        'Active Directory Domains and Trusts' by the administrator of the
+        original forest.
+        """
+
+        # Number of conflicts, entries for unsolved conflicts
+        result = [cinfo.count, []]
+
+        # In the code below:
+        # self -- the forest we establish trust to
+        # another_domain -- a forest that establishes trust to 'self'
+        # cinfo -- lsa_ForestTrustCollisionInfo structure that contains
+        #          set of of lsa_ForestTrustCollisionRecord structures
+        trust_timestamp = long(time.time()*1e7+116444736000000000)
+
+        # Collision information contains entries for specific trusted domains
+        # we collide with. Look into TLN collisions and add a TLN exclusion
+        # entry to the specific domain trust.
+        root_logger.error("Attempt to solve forest trust topology conflicts")
+        for rec in cinfo.entries:
+            if rec.type == lsa.LSA_FOREST_TRUST_COLLISION_TDO:
+                dominfo = self._pipe.lsaRQueryForestTrustInformation(
+                                 self._policy_handle,
+                                 rec.name,
+                                 lsa.LSA_FOREST_TRUST_DOMAIN_INFO)
+
+                # Oops, we were unable to retrieve trust topology for this
+                # trusted domain (forest).
+                if not dominfo:
+                    result[1].append(rec)
+                    root_logger.error("Unable to resolve conflict for "
+                                      "DNS domain %s in the forest %s "
+                                      "for domain trust %s. Trust cannot "
+                                      "be established unless this conflict "
+                                      "is fixed manually."
+                                      % (another_domain.info['dns_domain'],
+                                         self.info['dns_domain'],
+                                         rec.name.string))
+                    continue
+
+                # Copy over the entries, extend with TLN exclusion
+                entries = []
+                for e in dominfo.entries:
+                    e1 = lsa.ForestTrustRecord()
+                    e1.type = e.type
+                    e1.flags = e.flags
+                    e1.time = e.time
+                    e1.forest_trust_data = e.forest_trust_data
+                    entries.append(e1)
+
+                # Create TLN exclusion record
+                record = lsa.ForestTrustRecord()
+                record.type = lsa.LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX
+                record.flags = 0
+                record.time = trust_timestamp
+                record.forest_trust_data.string = \
+                    another_domain.info['dns_domain']
+                entries.append(record)
+
+                fti = lsa.ForestTrustInformation()
+                fti.count = len(entries)
+                fti.entries = entries
+
+                # Update the forest trust information now
+                ldname = lsa.StringLarge()
+                ldname.string = rec.name.string
+                cninfo = self._pipe.lsaRSetForestTrustInformation(
+                             self._policy_handle,
+                             ldname,
+                             lsa.LSA_FOREST_TRUST_DOMAIN_INFO,
+                             fti, 0)
+                if cninfo:
+                    result[1].append(rec)
+                    root_logger.error("When defining exception for DNS "
+                                      "domain %s in forest %s for "
+                                      "trusted forest %s, "
+                                      "got collision info back:\n%s"
+                                      % (another_domain.info['dns_domain'],
+                                         self.info['dns_domain'],
+                                         rec.name.string,
+                                         ndr_print(cninfo)))
+            else:
+                result[1].append(rec)
+                root_logger.error("Unable to resolve conflict for "
+                                  "DNS domain %s in the forest %s "
+                                  "for in-forest domain %s. Trust cannot "
+                                  "be established unless this conflict "
+                                  "is fixed manually."
+                                  % (conflict_type[rec.type],
+                                     another_domain.info['dns_domain'],
+                                     self.info['dns_domain'],
+                                     rec.name.string))
+
+        if len(result[1]) == 0:
+            root_logger.error("Successfully solved all conflicts")
+
+        return result
+
+
     def update_ftinfo(self, another_domain):
         """
         Updates forest trust information in this forest corresponding
         to the another domain's information.
         """
-        try:
-            if another_domain.ftinfo_records:
-                ftinfo = self.generate_ftinfo(another_domain)
-                # Set forest trust information -- we do it only against AD DC as
-                # smbd already has the information about itself
-                ldname = lsa.StringLarge()
-                ldname.string = another_domain.info['dns_domain']
-                ftlevel = lsa.LSA_FOREST_TRUST_DOMAIN_INFO
-                # RSetForestTrustInformation returns collision information
-                # for trust topology
-                cinfo = self._pipe.lsaRSetForestTrustInformation(
-                            self._policy_handle,
-                            ldname,
-                            ftlevel,
-                            ftinfo, 0)
-                if cinfo:
-                    root_logger.error("When setting forest trust information, "
-                                      "got collision info back:\n%s"
-                                      % (ndr_print(cinfo)))
-        except RuntimeError as e:
-            # We can ignore the error here --
-            # setting up name suffix routes may fail
-            pass
+        if another_domain.ftinfo_records:
+            ftinfo = self.generate_ftinfo(another_domain)
+            # Set forest trust information -- we do it only against AD DC as
+            # smbd already has the information about itself
+            ldname = lsa.StringLarge()
+            ldname.string = another_domain.info['dns_domain']
+            ftlevel = lsa.LSA_FOREST_TRUST_DOMAIN_INFO
+            # RSetForestTrustInformation returns collision information
+            # for trust topology
+            cinfo = self._pipe.lsaRSetForestTrustInformation(
+                        self._policy_handle,
+                        ldname,
+                        ftlevel,
+                        ftinfo, 0)
+            if cinfo:
+                root_logger.error("When setting forest trust information, "
+                                  "got collision info back:\n%s"
+                                  % (ndr_print(cinfo)))
+                res = self.clear_ftinfo_conflict(another_domain, cinfo)
+                if len(res[1]) != 0:
+                    domains = [x.name.string for x in res[1]]
+                    raise errors.TrustTopologyConflictError(
+                              target=self.info['dns_domain'],
+                              conflict=another_domain.info['dns_domain'],
+                              domains=domains)
+                else:
+                    raise errors.TrustTopologyConflictSolved(
+                              target=self.info['dns_domain'],
+                              conflict=another_domain.info['dns_domain'])
 
     def establish_trust(self, another_domain, trustdom_secret,
                         trust_type='bidirectional', trust_external=False):
@@ -1207,7 +1341,19 @@ class TrustDomainInstance(object):
                 root_logger.error(
                       'unable to set trust transitivity status: %s' % (str(e)))
 
-        if self.info['is_pdc'] or trust_external:
+        # Updating forest trust info may fail
+        # If it failed due to topology conflict, it may be fixed automatically
+        # update_ftinfo() will through exceptions in that case
+        # Note that MS-LSAD 3.1.4.7.16 says:
+        # -------------------------
+        # The server MUST also make sure that the trust attributes associated
+        # with the trusted domain object referenced by the TrustedDomainName
+        # parameter has the TRUST_ATTRIBUTE_FOREST_TRANSITIVE set.
+        # If the attribute is not present, the server MUST return
+        # STATUS_INVALID_PARAMETER.
+        # -------------------------
+        # Thus, we must not update forest trust info for the external trust
+        if self.info['is_pdc'] and not trust_external:
             self.update_ftinfo(another_domain)
 
     def verify_trust(self, another_domain):
@@ -1509,9 +1655,21 @@ class TrustDomainJoins(object):
         if not self.remote_domain.read_only:
             trustdom_pass = samba.generate_random_password(128, 128)
             self.get_realmdomains()
-            self.remote_domain.establish_trust(self.local_domain,
-                                               trustdom_pass,
-                                               trust_type, trust_external)
+
+            # Establishing trust may throw an exception for topology
+            # conflict. If it was solved, re-establish the trust again
+            # Otherwise let the CLI to display a message about the conflict
+            try:
+                self.remote_domain.establish_trust(self.local_domain,
+                                                   trustdom_pass,
+                                                   trust_type, trust_external)
+            except errors.TrustTopologyConflictSolved as e:
+                # we solved topology conflict, retry again
+                self.remote_domain.establish_trust(self.local_domain,
+                                                   trustdom_pass,
+                                                   trust_type, trust_external)
+
+            # For local domain we don't set topology information
             self.local_domain.establish_trust(self.remote_domain,
                                               trustdom_pass,
                                               trust_type, trust_external)
-- 
2.7.4

-------------- next part --------------
From 08dd95a7ff9c39ae2bfe167b51934666b550b57c Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <abokovoy at redhat.com>
Date: Mon, 15 Aug 2016 18:32:25 +0300
Subject: [PATCH 10/10] trust: make sure external trust topology is correctly
 rendered

When external trust is established, it is by definition is
non-transitive: it is not possible to obtain Kerberos tickets to any
service outside the trusted domain.

Reflect this reality by only accepting UPN suffixes from the external
trust -- since the trusted domain is a part of another forest and UPN
suffixes are forest-wide, there are could be user accounts in the
trusted domain that use forest-wide UPN suffix but it will be impossible
to reach the forest root via the externally trusted domain.

https://fedorahosted.org/freeipa/ticket/6021
---
 ipaserver/dcerpc.py        |  2 +-
 ipaserver/plugins/trust.py | 28 +++++++++++++++++-----------
 2 files changed, 18 insertions(+), 12 deletions(-)

diff --git a/ipaserver/dcerpc.py b/ipaserver/dcerpc.py
index e9e9e1e..777c311 100644
--- a/ipaserver/dcerpc.py
+++ b/ipaserver/dcerpc.py
@@ -1443,7 +1443,7 @@ def fetch_domains(api, mydomain, trustdomain, creds=None, server=None):
         # Older FreeIPA versions used netr_DsrEnumerateDomainTrusts call
         # but it doesn't provide information about non-domain UPNs associated
         # with the forest, thus we have to use netr_DsRGetForestTrustInformation
-        domains = netr_pipe.netr_DsRGetForestTrustInformation(td.info['dc'], '', 0)
+        domains = netr_pipe.netr_DsRGetForestTrustInformation(td.info['dc'], None, 0)
         return domains
 
     domains = None
diff --git a/ipaserver/plugins/trust.py b/ipaserver/plugins/trust.py
index f90d9c1..b9d9b12 100644
--- a/ipaserver/plugins/trust.py
+++ b/ipaserver/plugins/trust.py
@@ -1663,6 +1663,23 @@ def add_new_domains_from_trust(myapi, trustinstance, trust_entry, domains, **opt
                     for x, y in six.iteritems(domains['suffixes'])
                     if x not in domains['domains'])
 
+    try:
+        dn = myapi.Object.trust.get_dn(trust_name, trust_type=u'ad')
+        ldap = myapi.Backend.ldap2
+        entry = ldap.get_entry(dn)
+        tlns = entry.get('ipantadditionalsuffixes', [])
+        tlns.extend(x for x in suffixes if x not in tlns)
+        entry['ipantadditionalsuffixes'] = tlns
+        ldap.update_entry(entry)
+    except errors.EmptyModlist:
+        pass
+
+    is_nontransitive = int(trust_entry.get('ipanttrustattributes',
+                           [0])[0]) & LSA_TRUST_ATTRIBUTE_NON_TRANSITIVE
+
+    if is_nontransitive:
+        return result
+
     for dom in six.itervalues(domains['domains']):
         dom['trust_type'] = u'ad'
         try:
@@ -1690,17 +1707,6 @@ def add_new_domains_from_trust(myapi, trustinstance, trust_entry, domains, **opt
             # Ignore updating duplicate entries
             pass
 
-    try:
-        dn = myapi.Object.trust.get_dn(trust_name, trust_type=u'ad')
-        ldap = myapi.Backend.ldap2
-        entry = ldap.get_entry(dn)
-        tlns = entry.get('ipantadditionalsuffixes', [])
-        tlns.extend(x for x in suffixes if x not in tlns)
-        entry['ipantadditionalsuffixes'] = tlns
-        ldap.update_entry(entry)
-    except errors.EmptyModlist:
-        pass
-
     return result
 
 
-- 
2.7.4



More information about the Freeipa-devel mailing list