403b09
From 79bcdeb76d51fec5e8eab08f7642e7910e925bb4 Mon Sep 17 00:00:00 2001
403b09
From: Alexander Bokovoy <abokovoy@redhat.com>
403b09
Date: Mon, 15 Aug 2016 18:14:00 +0300
403b09
Subject: [PATCH] trust: automatically resolve DNS trust conflicts for triangle
403b09
 trusts
403b09
403b09
For configuration where:
403b09
  - AD example.com trusts IPA at ipa.example.com
403b09
  - AD example.org trusts AD example.com
403b09
  - a trust is tried to be established between ipa.example.com and
403b09
    example.org,
403b09
403b09
there will be a trust topology conflict detected by example.org domain
403b09
controller because ipa.example.com DNS namespace overlaps with
403b09
example.com DNS namespace.
403b09
403b09
This type of trust topology conflict is documented in MS-ADTS 6.1.6.9.3.2
403b09
"Building Well-Formed msDS-TrustForestTrustInfo Message". A similar
403b09
conflict can arise for SID and NetBIOS namespaces. However, unlike SID
403b09
and NetBIOS namespaces, we can solve DNS namespace conflict
403b09
automatically if there are administrative credentials for example.org
403b09
available.
403b09
403b09
A manual sequence to solve the DNS namespace conflict is described in
403b09
https://msdn.microsoft.com/it-it/library/cc786254%28v=ws.10%29.aspx.
403b09
This sequence boils down to the following steps:
403b09
403b09
   1. As an administrator of the example.org, you need to add an
403b09
exclusion entry for ipa.example.com in the properties of the trust to
403b09
example.com
403b09
   2. Establish trust between ipa.example.com and example.org
403b09
403b09
It is important to add the exclusion entry before step 4 or there will
403b09
be conflict recorded which cannot be cleared easily right now due to a
403b09
combination of bugs in both IPA and Active Directory.
403b09
403b09
This patchset implements automated solution for the case when we have
403b09
access to the example.org's administrator credentials:
403b09
403b09
   1. Attempt to establish trust and update trust topology information.
403b09
   2. If trust topology conflict is detected as result of (1):
403b09
   2.1. Fetch trust topology infromation for the conflicting forest
403b09
        trust
403b09
   2.2. Add exclusion entry to our domain to the trust topology obtained
403b09
        in (2.1)
403b09
   2.3. Update trust topology for the conflicting forest trust
403b09
   3. Re-establish trust between ipa.example.com and example.org
403b09
403b09
We cannot do the same for shared secret trust and for external trust,
403b09
though:
403b09
403b09
   1. For shared secret trust we don't have administrative credentials
403b09
      in the forest reporting the conflict
403b09
403b09
   2. For the external trust we cannot set topology information due to
403b09
      MS-LSAD 3.1.4.7.16 because external trust is non-transitive by
403b09
      definition and thus setting topology information will fail.
403b09
403b09
To test this logic one can use two Samba AD forests with FreeIPA
403b09
using a sub-domain of one of them.
403b09
403b09
Fixes: https://fedorahosted.org/freeipa/ticket/6076
403b09
Reviewed-By: Martin Babinsky <mbabinsk@redhat.com>
403b09
---
403b09
 ipalib/errors.py    |  29 ++++++-
403b09
 ipaserver/dcerpc.py | 220 +++++++++++++++++++++++++++++++++++++++++++++-------
403b09
 2 files changed, 220 insertions(+), 29 deletions(-)
403b09
403b09
diff --git a/ipalib/errors.py b/ipalib/errors.py
403b09
index 7b4f15dd60ee80719195ba1b9b85d075b10bdf4f..4cc4455b0abf7d2b1366e1ce6dbb3762bc551cc6 100644
403b09
--- a/ipalib/errors.py
403b09
+++ b/ipalib/errors.py
403b09
@@ -866,7 +866,6 @@ class NotAForestRootError(InvocationError):
403b09
     errno = 3016
403b09
     format = _("Domain '%(domain)s' is not a root domain for forest '%(forest)s'")
403b09
 
403b09
-
403b09
 ##############################################################################
403b09
 # 4000 - 4999: Execution errors
403b09
 
403b09
@@ -1908,6 +1907,34 @@ class DNSResolverError(DNSError):
403b09
     errno = 4401
403b09
     format = _('%(exception)s')
403b09
 
403b09
+class TrustError(ExecutionError):
403b09
+    """
403b09
+    **4500** Base class for trust execution errors (*4500 - 4599*).
403b09
+    These are typically instantiated when there is an error in establishing or
403b09
+    modifying a trust to another forest.
403b09
+    """
403b09
+
403b09
+    errno = 4500
403b09
+
403b09
+class TrustTopologyConflictError(TrustError):
403b09
+    """
403b09
+    **4501** Raised when an attempt to establish trust fails with a topology
403b09
+             conflict against another forest the target forest trusts
403b09
+
403b09
+    For example:
403b09
+
403b09
+    >>> raise TrustTopologyConflictError(forest='example.test',
403b09
+                                         conflict='my.ad.test',
403b09
+                                         domains=['ad.test'])
403b09
+    Traceback (most recent call last):
403b09
+      ...
403b09
+    TrustTopologyConflictError: Forest 'example.test' has existing trust to forest(s) ['ad.test'] which prevents a trust to 'my.ad.test'
403b09
+    """
403b09
+
403b09
+    errno = 4501
403b09
+    format = _("Forest '%(forest)s' has existing trust to forest(s) "
403b09
+               "%(domains)s which prevents a trust to '%(conflict)s'")
403b09
+
403b09
 
403b09
 ##############################################################################
403b09
 # 5000 - 5999: Generic errors
403b09
diff --git a/ipaserver/dcerpc.py b/ipaserver/dcerpc.py
403b09
index 19be6bf7a3617a3b867d51a9358c9926e91049a7..a1c12f16a655493808d50e6adb95e618a664a98c 100644
403b09
--- a/ipaserver/dcerpc.py
403b09
+++ b/ipaserver/dcerpc.py
403b09
@@ -1,7 +1,7 @@
403b09
 # Authors:
403b09
 #     Alexander Bokovoy <abokovoy@redhat.com>
403b09
 #
403b09
-# Copyright (C) 2011  Red Hat
403b09
+# Copyright (C) 2011-2016  Red Hat
403b09
 # see file 'COPYING' for use and warranty information
403b09
 #
403b09
 # Portions (C) Andrew Tridgell, Andrew Bartlett
403b09
@@ -140,6 +140,15 @@ pysss_type_key_translation_dict = {
403b09
     pysss_nss_idmap.ID_BOTH: 'both',
403b09
 }
403b09
 
403b09
+class TrustTopologyConflictSolved(Exception):
403b09
+    """
403b09
+    Internal trust error: raised when previously detected
403b09
+    trust topology conflict is automatically solved.
403b09
+
403b09
+    No separate errno is assigned as this error should
403b09
+    not be visible outside the dcerpc.py code.
403b09
+    """
403b09
+    pass
403b09
 
403b09
 def assess_dcerpc_exception(num=None, message=None):
403b09
     """
403b09
@@ -1087,34 +1096,165 @@ class TrustDomainInstance(object):
403b09
         info.entries = ftinfo_records
403b09
         return info
403b09
 
403b09
+    def clear_ftinfo_conflict(self, another_domain, cinfo):
403b09
+        """
403b09
+        Attempt to clean up the forest trust collisions
403b09
+
403b09
+        :param self: the forest we establish trust to
403b09
+        :param another_domain: a forest that establishes trust to 'self'
403b09
+        :param cinfo: lsa_ForestTrustCollisionInfo structure that contain
403b09
+                      set of of lsa_ForestTrustCollisionRecord structures
403b09
+        :raises: TrustTopologyConflictSolved, TrustTopologyConflictError
403b09
+
403b09
+        This code tries to perform intelligent job of going
403b09
+        over individual collisions and making exclusion entries
403b09
+        for affected IPA namespaces.
403b09
+
403b09
+        There are three possible conflict configurations:
403b09
+          - conflict of DNS namespace (TLN conflict, LSA_TLN_DISABLED_CONFLICT)
403b09
+          - conflict of SID namespace (LSA_SID_DISABLED_CONFLICT)
403b09
+          - conflict of NetBIOS namespace (LSA_NB_DISABLED_CONFLICT)
403b09
+
403b09
+        we only can handle TLN conflicts because (a) excluding SID namespace
403b09
+        is not possible and (b) excluding NetBIOS namespace not possible.
403b09
+        These two types of conflicts should result in trust-add CLI error
403b09
+
403b09
+        These conflicts can come from external source (another forest) or
403b09
+        from internal source (another domain in the same forest). We only
403b09
+        can fix the problems with another forest.
403b09
+
403b09
+        To resolve TLN conflict we need to do following:
403b09
+          1. Retrieve forest trust information for the forest we conflict on
403b09
+          2. Add an exclusion entry for IPA DNS namespace to it
403b09
+          3. Set forest trust information for the forest we conflict on
403b09
+          4. Re-try establishing trust to the original forest
403b09
+
403b09
+        This all can only be done under privileges of Active Directory admin
403b09
+        that can change forest trusts. If we cannot have those privileges,
403b09
+        the work has to be done manually in the Windows UI for
403b09
+        'Active Directory Domains and Trusts' by the administrator of the
403b09
+        original forest.
403b09
+        """
403b09
+
403b09
+        # List of entries for unsolved conflicts
403b09
+        result = []
403b09
+
403b09
+        trust_timestamp = long(time.time()*1e7+116444736000000000)
403b09
+
403b09
+        # Collision information contains entries for specific trusted domains
403b09
+        # we collide with. Look into TLN collisions and add a TLN exclusion
403b09
+        # entry to the specific domain trust.
403b09
+        root_logger.error("Attempt to solve forest trust topology conflicts")
403b09
+        for rec in cinfo.entries:
403b09
+            if rec.type == lsa.LSA_FOREST_TRUST_COLLISION_TDO:
403b09
+                dominfo = self._pipe.lsaRQueryForestTrustInformation(
403b09
+                                 self._policy_handle,
403b09
+                                 rec.name,
403b09
+                                 lsa.LSA_FOREST_TRUST_DOMAIN_INFO)
403b09
+
403b09
+                # Oops, we were unable to retrieve trust topology for this
403b09
+                # trusted domain (forest).
403b09
+                if not dominfo:
403b09
+                    result.append(rec)
403b09
+                    root_logger.error("Unable to resolve conflict for "
403b09
+                                      "DNS domain %s in the forest %s "
403b09
+                                      "for domain trust %s. Trust cannot "
403b09
+                                      "be established unless this conflict "
403b09
+                                      "is fixed manually."
403b09
+                                      % (another_domain.info['dns_domain'],
403b09
+                                         self.info['dns_domain'],
403b09
+                                         rec.name.string))
403b09
+                    continue
403b09
+
403b09
+                # Copy over the entries, extend with TLN exclusion
403b09
+                entries = []
403b09
+                for e in dominfo.entries:
403b09
+                    e1 = lsa.ForestTrustRecord()
403b09
+                    e1.type = e.type
403b09
+                    e1.flags = e.flags
403b09
+                    e1.time = e.time
403b09
+                    e1.forest_trust_data = e.forest_trust_data
403b09
+                    entries.append(e1)
403b09
+
403b09
+                # Create TLN exclusion record
403b09
+                record = lsa.ForestTrustRecord()
403b09
+                record.type = lsa.LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX
403b09
+                record.flags = 0
403b09
+                record.time = trust_timestamp
403b09
+                record.forest_trust_data.string = \
403b09
+                    another_domain.info['dns_domain']
403b09
+                entries.append(record)
403b09
+
403b09
+                fti = lsa.ForestTrustInformation()
403b09
+                fti.count = len(entries)
403b09
+                fti.entries = entries
403b09
+
403b09
+                # Update the forest trust information now
403b09
+                ldname = lsa.StringLarge()
403b09
+                ldname.string = rec.name.string
403b09
+                cninfo = self._pipe.lsaRSetForestTrustInformation(
403b09
+                             self._policy_handle,
403b09
+                             ldname,
403b09
+                             lsa.LSA_FOREST_TRUST_DOMAIN_INFO,
403b09
+                             fti, 0)
403b09
+                if cninfo:
403b09
+                    result.append(rec)
403b09
+                    root_logger.error("When defining exception for DNS "
403b09
+                                      "domain %s in forest %s for "
403b09
+                                      "trusted forest %s, "
403b09
+                                      "got collision info back:\n%s"
403b09
+                                      % (another_domain.info['dns_domain'],
403b09
+                                         self.info['dns_domain'],
403b09
+                                         rec.name.string,
403b09
+                                         ndr_print(cninfo)))
403b09
+            else:
403b09
+                result.append(rec)
403b09
+                root_logger.error("Unable to resolve conflict for "
403b09
+                                  "DNS domain %s in the forest %s "
403b09
+                                  "for in-forest domain %s. Trust cannot "
403b09
+                                  "be established unless this conflict "
403b09
+                                  "is fixed manually."
403b09
+                                  % (another_domain.info['dns_domain'],
403b09
+                                     self.info['dns_domain'],
403b09
+                                     rec.name.string))
403b09
+
403b09
+        if len(result) == 0:
403b09
+            root_logger.error("Successfully solved all conflicts")
403b09
+            raise TrustTopologyConflictSolved()
403b09
+
403b09
+        # Otherwise, raise TrustTopologyConflictError() exception
403b09
+        domains = [x.name.string for x in result]
403b09
+        raise errors.TrustTopologyConflictError(
403b09
+                              target=self.info['dns_domain'],
403b09
+                              conflict=another_domain.info['dns_domain'],
403b09
+                              domains=domains)
403b09
+
403b09
+
403b09
+
403b09
     def update_ftinfo(self, another_domain):
403b09
         """
403b09
         Updates forest trust information in this forest corresponding
403b09
         to the another domain's information.
403b09
         """
403b09
-        try:
403b09
-            if another_domain.ftinfo_records:
403b09
-                ftinfo = self.generate_ftinfo(another_domain)
403b09
-                # Set forest trust information -- we do it only against AD DC as
403b09
-                # smbd already has the information about itself
403b09
-                ldname = lsa.StringLarge()
403b09
-                ldname.string = another_domain.info['dns_domain']
403b09
-                ftlevel = lsa.LSA_FOREST_TRUST_DOMAIN_INFO
403b09
-                # RSetForestTrustInformation returns collision information
403b09
-                # for trust topology
403b09
-                cinfo = self._pipe.lsaRSetForestTrustInformation(
403b09
-                            self._policy_handle,
403b09
-                            ldname,
403b09
-                            ftlevel,
403b09
-                            ftinfo, 0)
403b09
-                if cinfo:
403b09
-                    root_logger.error("When setting forest trust information, "
403b09
-                                      "got collision info back:\n%s"
403b09
-                                      % (ndr_print(cinfo)))
403b09
-        except RuntimeError as e:
403b09
-            # We can ignore the error here --
403b09
-            # setting up name suffix routes may fail
403b09
-            pass
403b09
+        if another_domain.ftinfo_records:
403b09
+            ftinfo = self.generate_ftinfo(another_domain)
403b09
+            # Set forest trust information -- we do it only against AD DC as
403b09
+            # smbd already has the information about itself
403b09
+            ldname = lsa.StringLarge()
403b09
+            ldname.string = another_domain.info['dns_domain']
403b09
+            ftlevel = lsa.LSA_FOREST_TRUST_DOMAIN_INFO
403b09
+            # RSetForestTrustInformation returns collision information
403b09
+            # for trust topology
403b09
+            cinfo = self._pipe.lsaRSetForestTrustInformation(
403b09
+                        self._policy_handle,
403b09
+                        ldname,
403b09
+                        ftlevel,
403b09
+                        ftinfo, 0)
403b09
+            if cinfo:
403b09
+                root_logger.error("When setting forest trust information, "
403b09
+                                  "got collision info back:\n%s"
403b09
+                                  % (ndr_print(cinfo)))
403b09
+                self.clear_ftinfo_conflict(another_domain, cinfo)
403b09
 
403b09
     def establish_trust(self, another_domain, trustdom_secret,
403b09
                         trust_type='bidirectional', trust_external=False):
403b09
@@ -1207,7 +1347,19 @@ class TrustDomainInstance(object):
403b09
                 root_logger.error(
403b09
                       'unable to set trust transitivity status: %s' % (str(e)))
403b09
 
403b09
-        if self.info['is_pdc'] or trust_external:
403b09
+        # Updating forest trust info may fail
403b09
+        # If it failed due to topology conflict, it may be fixed automatically
403b09
+        # update_ftinfo() will through exceptions in that case
403b09
+        # Note that MS-LSAD 3.1.4.7.16 says:
403b09
+        # -------------------------
403b09
+        # The server MUST also make sure that the trust attributes associated
403b09
+        # with the trusted domain object referenced by the TrustedDomainName
403b09
+        # parameter has the TRUST_ATTRIBUTE_FOREST_TRANSITIVE set.
403b09
+        # If the attribute is not present, the server MUST return
403b09
+        # STATUS_INVALID_PARAMETER.
403b09
+        # -------------------------
403b09
+        # Thus, we must not update forest trust info for the external trust
403b09
+        if self.info['is_pdc'] and not trust_external:
403b09
             self.update_ftinfo(another_domain)
403b09
 
403b09
     def verify_trust(self, another_domain):
403b09
@@ -1509,9 +1661,21 @@ class TrustDomainJoins(object):
403b09
         if not self.remote_domain.read_only:
403b09
             trustdom_pass = samba.generate_random_password(128, 128)
403b09
             self.get_realmdomains()
403b09
-            self.remote_domain.establish_trust(self.local_domain,
403b09
-                                               trustdom_pass,
403b09
-                                               trust_type, trust_external)
403b09
+
403b09
+            # Establishing trust may throw an exception for topology
403b09
+            # conflict. If it was solved, re-establish the trust again
403b09
+            # Otherwise let the CLI to display a message about the conflict
403b09
+            try:
403b09
+                self.remote_domain.establish_trust(self.local_domain,
403b09
+                                                   trustdom_pass,
403b09
+                                                   trust_type, trust_external)
403b09
+            except TrustTopologyConflictSolved as e:
403b09
+                # we solved topology conflict, retry again
403b09
+                self.remote_domain.establish_trust(self.local_domain,
403b09
+                                                   trustdom_pass,
403b09
+                                                   trust_type, trust_external)
403b09
+
403b09
+            # For local domain we don't set topology information
403b09
             self.local_domain.establish_trust(self.remote_domain,
403b09
                                               trustdom_pass,
403b09
                                               trust_type, trust_external)
403b09
-- 
403b09
2.7.4
403b09