On Friday 19 of July 2013 12:32:04 Tomas Babej wrote:
> 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

This version fixes few small build nitpicks.

Tomas
>From c7dbbf5523176b713fd651c6c817c46806afbfc2 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

When creating a trusted domain ID range, probe AD DC to get
information about ID space leveraged by POSIX users already
defined in AD, and create an ID range with according parameters.

For more details:
http://www.freeipa.org/page/V3/Use_posix_attributes_defined_in_AD
https://fedorahosted.org/freeipa/ticket/3649
---
 API.txt                           |   2 +-
 VERSION                           |   2 +-
 ipalib/plugins/trust.py           | 105 +++++++++++++++++++++---
 ipaserver/dcerpc.py               | 163 +++++++++++++++++++++++++++++---------
 ipaserver/install/installutils.py |   7 +-
 5 files changed, 225 insertions(+), 54 deletions(-)

diff --git a/API.txt b/API.txt
index 44b3dd444964c8dac595177f8601c82d0235eabe..2773f3d5c88ffa05ab7587dd9f0df97b350e45ca 100644
--- a/API.txt
+++ b/API.txt
@@ -3283,7 +3283,7 @@ arg: Str('cn', attribute=True, cli_name='realm', multivalue=False, primary_key=T
 option: Str('addattr*', cli_name='addattr', exclude='webui')
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
 option: Int('base_id?', cli_name='base_id')
-option: Int('range_size?', autofill=True, cli_name='range_size', default=200000)
+option: Int('range_size?', cli_name='range_size')
 option: StrEnum('range_type?', cli_name='range_type', values=(u'ipa-ad-trust-posix', u'ipa-ad-trust'))
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Str('realm_admin?', cli_name='admin')
diff --git a/VERSION b/VERSION
index 8606d724e6c8c785ba9d554ed3effa905573e25f..8a36c6304d7cf7777e0452eae5dbdc7a5d2951ab 100644
--- a/VERSION
+++ b/VERSION
@@ -89,4 +89,4 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=61
+IPA_API_VERSION_MINOR=62
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..10efc0ca70869964ec0222eb629035e88de7e4b0 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
@@ -284,7 +292,7 @@ class DomainValidator(object):
                 raise errors.ValidationError(name=_('trusted domain object'),
                    error= _('domain is not trusted'))
             # Now we have a name to check against our list of trusted domains
-            entries = self.search_in_gc(domain, filter, attrs, scope, basedn)
+            entries = self.search_in_dc(domain, filter, attrs, scope, basedn)
         elif flatname is not None:
             # Flatname was specified, traverse through the list of trusted
             # domains first to find the proper one
@@ -292,7 +300,7 @@ class DomainValidator(object):
             for domain in self._domains:
                 if self._domains[domain][0] == flatname:
                     found_flatname = True
-                    entries = self.search_in_gc(domain, filter, attrs, scope, basedn)
+                    entries = self.search_in_dc(domain, filter, attrs, scope, basedn)
                     if entries:
                         break
             if not found_flatname:
@@ -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