Hi,

I still need to test this with AD 2008. Sending so that code can get some 
review before that.

https://fedorahosted.org/freeipa/ticket/3649

Tomas
>From 592f5ad6e6f9e5241f99dc0909948991d8ea7848 Mon Sep 17 00:00:00 2001
From: Tomas Babej <tba...@redhat.com>
Date: Wed, 17 Jul 2013 15:55:36 +0200
Subject: [PATCH] Use AD LDAP probing to create trusted domain ID range

TODO

https://fedorahosted.org/freeipa/ticket/3649
---
 ipalib/plugins/trust.py           | 105 ++++++++++++++++++++++---
 ipaserver/dcerpc.py               | 159 +++++++++++++++++++++++++++++---------
 ipaserver/install/installutils.py |   7 +-
 3 files changed, 221 insertions(+), 50 deletions(-)

diff --git a/ipalib/plugins/trust.py b/ipalib/plugins/trust.py
index 965ff76bb7968a8d2784e67478eb824dc3f0621b..40432c6074bb19e1a31c21e7e32d5b85c12d7210 100644
--- a/ipalib/plugins/trust.py
+++ b/ipalib/plugins/trust.py
@@ -20,9 +20,13 @@
 
 from ipalib.plugins.baseldap import *
 from ipalib.plugins.dns import dns_container_exists
+from ipapython.ipautil import realm_to_suffix
 from ipalib import api, Str, StrEnum, Password, _, ngettext
 from ipalib import Command
 from ipalib import errors
+from ldap import SCOPE_SUBTREE
+from time import sleep
+
 try:
     import pysss_murmur #pylint: disable=F0401
     _murmur_installed = True
@@ -292,8 +296,6 @@ sides.
         Int('range_size?',
             cli_name='range_size',
             label=_('Size of the ID range reserved for the trusted domain'),
-            default=DEFAULT_RANGE_SIZE,
-            autofill=True
         ),
         StrEnum('range_type?',
             label=_('Range type'),
@@ -313,7 +315,7 @@ sides.
         result = self.execute_ad(full_join, *keys, **options)
 
         if not old_range:
-            self.add_range(range_name, dom_sid, **options)
+            self.add_range(range_name, dom_sid, *keys, **options)
 
         trust_filter = "cn=%s" % result['value']
         ldap = self.obj.backend
@@ -418,9 +420,7 @@ sides.
                         'Only the ipa-ad-trust and ipa-ad-trust-posix are '
                         'allowed values for --range-type when adding an AD '
                         'trust.'
-                    )
-
-)
+                    ))
 
         base_id = options.get('base_id')
         range_size = options.get('range_size') != DEFAULT_RANGE_SIZE
@@ -468,9 +468,90 @@ sides.
 
         return old_range, range_name, dom_sid
 
-    def add_range(self, range_name, dom_sid, **options):
-        base_id = options.get('base_id')
-        if not base_id:
+    def add_range(self, range_name, dom_sid, *keys, **options):
+        """
+        First, we try to derive the parameters of the ID range based on the
+        information contained in the Active Directory.
+
+        If that was not successful, we go for our usual defaults (random base,
+        range size 200 000, ipa-ad-trust range type).
+
+        Any of these can be overriden by passing appropriate CLI options
+        to the trust-add command.
+        """
+
+        range_size = None
+        range_type = None
+        base_id = None
+
+        # First, get information about ID space from AD
+
+        # Get the base dn
+        domain = keys[-1]
+        basedn = realm_to_suffix(domain)
+
+        # Search for information contained in
+        # CN=ypservers,CN=ypServ30,CN=RpcServices,CN=System
+        info_filter = '(objectClass=msSFU30DomainInfo)'
+        info_dn = DN('CN=ypservers,CN=ypServ30,CN=RpcServices,CN=System')\
+                  + basedn
+
+        # Get the domain validator
+        domain_validator = ipaserver.dcerpc.DomainValidator(self.api)
+        if not domain_validator.is_configured():
+            raise errors.NotFound(
+                reason=_('Cannot search in trusted domains without own domain '
+                         'configured. Make sure you have run ipa-adtrust-'
+                         'install on the IPA server first'))
+
+        # KDC might not get refreshed data at the first time,
+        # retry several times
+        for retry in range(10):
+            info_list = domain_validator.search_in_dc(domain,
+                                                      info_filter,
+                                                      None,
+                                                      SCOPE_SUBTREE,
+                                                      basedn=info_dn,
+                                                      use_http=True)
+
+            if info_list:
+                info = info_list[0]
+                break
+            else:
+                sleep(2)
+
+        required_msSFU_attrs = ['msSFU30MaxUidNumber', 'msSFU30OrderNumber']
+
+        if not info_list:
+            # We were unable to gain UNIX specific info from the AD
+            self.log.debug("Unable to gain POSIX info from the AD")
+        else:
+            if all(attr in info for attr in required_msSFU_attrs):
+                self.log.debug("Able to gain POSIX info from the AD")
+                range_type = u'ipa-ad-trust-posix'
+
+                max_uid = info.get('msSFU30MaxUidNumber')
+                max_gid = info.get('msSFU30MaxGidNumber', None)
+                max_id = int(max(max_uid, max_gid)[0])
+
+                base_id = int(info.get('msSFU30OrderNumber')[0])
+                range_size = max_id - base_id
+
+        # Second, options given via the CLI options take precedence to discovery
+        if options.get('range_type', None):
+            range_type = options.get('range_type', None)
+        elif not range_type:
+            range_type = u'ipa-ad-trust'
+
+        if options.get('range_size', None):
+            range_size = options.get('range_size', None)
+        elif not range_size:
+            range_size = DEFAULT_RANGE_SIZE
+
+        if options.get('base_id', None):
+            base_id = options.get('base_id', None)
+        elif not base_id:
+            # Generate random base_id if not discovered nor given via CLI
             base_id = DEFAULT_RANGE_SIZE + (
                 pysss_murmur.murmurhash3(
                     dom_sid,
@@ -478,12 +559,12 @@ sides.
                 ) % 10000
             ) * DEFAULT_RANGE_SIZE
 
-        # Add new ID range
+        # Finally, add new ID range
         api.Command['idrange_add'](range_name,
                                    ipabaseid=base_id,
-                                   ipaidrangesize=options['range_size'],
+                                   ipaidrangesize=range_size,
                                    ipabaserid=0,
-                                   iparangetype=options.get('range_type'),
+                                   iparangetype=range_type,
                                    ipanttrusteddomainsid=dom_sid)
 
     def execute_ad(self, full_join, *keys, **options):
diff --git a/ipaserver/dcerpc.py b/ipaserver/dcerpc.py
index 0f98ce83cc17715317ef33f0076e2bc802a23df7..1c487355cdc178aa44dbcdfaec2da56c719ef4a6 100644
--- a/ipaserver/dcerpc.py
+++ b/ipaserver/dcerpc.py
@@ -61,6 +61,7 @@ The code in this module relies heavily on samba4-python package
 and Samba4 python bindings.
 """)
 
+
 def is_sid_valid(sid):
     try:
         security.dom_sid(sid)
@@ -69,6 +70,7 @@ def is_sid_valid(sid):
     else:
         return True
 
+
 access_denied_error =  errors.ACIError(info=_('CIFS server denied your credentials'))
 dcerpc_error_codes = {
     -1073741823:
@@ -113,6 +115,7 @@ class ExtendedDNControl(LDAPControl):
     def encodeControlValue(self, value=None):
         return '0\x03\x02\x01\x01'
 
+
 class DomainValidator(object):
     ATTR_FLATNAME = 'ipantflatname'
     ATTR_SID = 'ipantsecurityidentifier'
@@ -184,6 +187,18 @@ class DomainValidator(object):
         except errors.NotFound, e:
             return []
 
+    def set_trusted_domains(self):
+        # At this point we have SID_NT_AUTHORITY family SID and really need to
+        # check it against prefixes of domain SIDs we trust to
+        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
+            # This means we can't check the correctness of a trusted
+            # domain SIDs
+            raise errors.ValidationError(name='sid',
+                  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
@@ -200,14 +215,7 @@ class DomainValidator(object):
 
         # At this point we have SID_NT_AUTHORITY family SID and really need to
         # check it against prefixes of domain SIDs we trust to
-        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
-            # This means we can't check the correctness of a trusted
-            # domain SIDs
-            raise errors.ValidationError(name='sid',
-                  error=_('no trusted domain is configured'))
+        self.set_trusted_domains()
 
         # We have non-zero list of trusted domains and have to go through
         # them one by one and check their sids as prefixes / exact match
@@ -436,48 +444,125 @@ class DomainValidator(object):
                         dict(domain=info['dns_domain'],message=stderr.strip()))
             return (None, None)
 
-    def search_in_gc(self, domain, filter, attrs, scope, basedn=None):
+    def kinit_as_http(self, domain):
         """
-        Perform LDAP search in a trusted domain `domain' Global Catalog.
-        Returns resulting entries or None
+        Initializes ccache with http service credentials.
+
+        Applies session code defaults for ccache directory and naming prefix.
+        Session code uses krbccache_prefix+<pid>, we use
+        krbccache_prefix+<TD>+<domain netbios name> so there is no clash.
+
+        Returns tuple (ccache path, principal) where (None, None) signifes an
+        error on ccache initialization
         """
+
+        domain_suffix = domain.replace('.', '-')
+
+        ccache_name = "%sTD%s" % (krbccache_prefix, domain_suffix)
+        ccache_path = os.path.join(krbccache_dir, ccache_name)
+
+        realm = api.env.realm
+        hostname = api.env.host
+        principal = 'HTTP/%s@%s' % (hostname, realm)
+        keytab = '/etc/httpd/conf/ipa.keytab'
+
+        # Destroy the contents of the ccache
+        root_logger.debug('Destroying the contents of the separate ccache')
+
+        (stdout, stderr, returncode) = ipautil.run(
+            ['/usr/bin/kdestroy', '-A', '-c', ccache_path],
+            env={'KRB5CCNAME': ccache_path},
+            raiseonerr=False)
+
+        # Destroy the contents of the ccache
+        root_logger.debug('Running kinit from ipa.keytab to obtain HTTP '
+                          'service principal with MS-PAC attached.')
+
+        (stdout, stderr, returncode) = ipautil.run(
+            ['/usr/bin/kinit', '-kt', keytab, principal],
+            env={'KRB5CCNAME': ccache_path},
+            raiseonerr=False)
+
+        if returncode == 0:
+            return (ccache_path, principal)
+        else:
+            return (None, None)
+
+    def search_in_dc(self, domain, filter, attrs, scope, basedn=None,
+                     use_http=False):
+        """
+        Perform LDAP search in a trusted domain `domain' Domain Controller.
+        Returns resulting entries or None.
+
+        If use_http is set to True, the search is conducted using
+        HTTP service credentials.
+        """
+
         entries = None
-        sid = None
+
         info = self.__retrieve_trusted_domain_gc_list(domain)
+
         if not info:
-             raise errors.ValidationError(name=_('Trust setup'),
+            raise errors.ValidationError(
+                name=_('Trust setup'),
                 error=_('Cannot retrieve trusted domain GC list'))
+
         for (host, port) in info['gc']:
-            entries = self.__search_in_gc(info, host, port, filter, attrs, scope, basedn)
+            entries = self.__search_in_dc(info, host, port, filter, attrs,
+                                          scope, basedn=basedn,
+                                          use_http=use_http)
             if entries:
                 break
 
         return entries
 
-    def __search_in_gc(self, info, host, port, filter, attrs, scope, basedn=None):
+    def __search_in_dc(self, info, host, port, filter, attrs, scope,
+                       basedn=None, use_http=False):
         """
         Actual search in AD LDAP server, using SASL GSSAPI authentication
-        Returns LDAP result or None
+        Returns LDAP result or None.
         """
-        conn = IPAdmin(host=host, port=port, no_schema=True, decode_attrs=False)
-        auth = self.__extract_trusted_auth(info)
-        if attrs is None:
-            attrs = []
-        if auth:
-            (ccache_name, principal) = self.__kinit_as_trusted_account(info, auth)
-            if ccache_name:
-                old_ccache = os.environ.get('KRB5CCNAME')
-                os.environ["KRB5CCNAME"] = ccache_name
-                # OPT_X_SASL_NOCANON is used to avoid hard requirement for PTR
-                # records pointing back to the same host name
-                conn.set_option(_ldap.OPT_X_SASL_NOCANON, _ldap.OPT_ON)
-                conn.do_sasl_gssapi_bind()
-                if basedn is None:
-                    # Use domain root base DN
-                    basedn = DN(*map(lambda p: ('dc', p), info['dns_domain'].split('.')))
-                entries = conn.get_entries(basedn, scope, filter, attrs)
-                os.environ["KRB5CCNAME"] = old_ccache
-                return entries
+
+        if use_http:
+            (ccache_name, principal) = self.kinit_as_http(info['dns_domain'])
+        else:
+            auth = self.__extract_trusted_auth(info)
+
+            if not auth:
+                return None
+
+            (ccache_name, principal) = self.__kinit_as_trusted_account(info,
+                                                                       auth)
+
+        if ccache_name:
+            with installutils.private_ccache(path=ccache_name):
+                entries = None
+
+                try:
+                    conn = IPAdmin(host=host,
+                                   port=389,  # query the AD DC
+                                   no_schema=True,
+                                   decode_attrs=False,
+                                   sasl_nocanon=True)
+                    # sasl_nocanon used to avoid hard requirement for PTR
+                    # records pointing back to the same host name
+
+                    conn.do_sasl_gssapi_bind()
+
+                    if basedn is None:
+                        # Use domain root base DN
+                        basedn = ipautil.realm_to_suffix(info['dns_domain'])
+
+                    entries = conn.get_entries(basedn, scope, filter, attrs)
+                except Exception, e:
+                    root_logger.warning("Search on AD DC {host}:{port} "
+                                        "failed with: {err}"
+                                        .format(host=host,
+                                                port=str(port),
+                                                err=str(e)
+                                                ))
+                finally:
+                    return entries
 
     def __retrieve_trusted_domain_gc_list(self, domain):
         """
@@ -508,9 +593,13 @@ class DomainValidator(object):
         except RuntimeError, e:
             finddc_error = e
 
+        if not self._domains:
+            self._domains = self.get_trusted_domains()
+
         info = dict()
         info['auth'] = self._domains[domain][2]
         servers = []
+
         if result:
             info['name'] = unicode(result.domain_name)
             info['dns_domain'] = unicode(result.dns_domain)
diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py
index d23f9b57ff3a31ab17ed126c504c8075f30de642..e6f50a52c3272a10231815f8f0052a4600d22b21 100644
--- a/ipaserver/install/installutils.py
+++ b/ipaserver/install/installutils.py
@@ -766,10 +766,11 @@ def check_pkcs12(pkcs12_info, ca_file, hostname):
 
 
 @contextmanager
-def private_ccache():
+def private_ccache(path=None):
 
-    (desc, path) = tempfile.mkstemp(prefix='krbcc')
-    os.close(desc)
+    if path is None:
+        (desc, path) = tempfile.mkstemp(prefix='krbcc')
+        os.close(desc)
 
     original_value = os.environ.get('KRB5CCNAME', None)
 
-- 
1.8.3.1

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to