From 6556b4fc4261f8051e13e0f7dd81481b78fa33d1 Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Wed, 17 Jul 2013 15:55:36 +0200 Subject: Gain information from AD --- ipalib/plugins/trust.py | 51 +++++++++-- ipapython/ipaldap.py | 12 ++- ipapython/ipautil.py | 10 ++- ipaserver/dcerpc.py | 230 +++++++++++++++++++++++++++++++++++++++++------- 4 files changed, 259 insertions(+), 44 deletions(-) diff --git a/ipalib/plugins/trust.py b/ipalib/plugins/trust.py index 965ff76bb..87a1adbdb 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 @@ -313,7 +317,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 +422,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,7 +470,10 @@ sides. return old_range, range_name, dom_sid - def add_range(self, range_name, dom_sid, **options): + def add_range(self, range_name, dom_sid, *keys, **options): + # Sleep for 10 seconds, to make sure KDC contains refreshed data + sleep(10) + base_id = options.get('base_id') if not base_id: base_id = DEFAULT_RANGE_SIZE + ( @@ -478,6 +483,42 @@ sides. ) % 10000 ) * DEFAULT_RANGE_SIZE + # Get information about ID space from AD + domain = keys[-1] + + # Get the base dn + 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')) + + for retry in range(10): + # Get the info from AD + info = domain_validator.search_in_gc(domain, + info_filter, + None, + SCOPE_SUBTREE, + basedn=info_dn, + use_http=True) + + if info is not None: + break + else: + sleep(2) + + self.log.info('result: %s' % info) + # Add new ID range api.Command['idrange_add'](range_name, ipabaseid=base_id, diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py index 6873511c4..9ee0ab5f5 100644 --- a/ipapython/ipaldap.py +++ b/ipapython/ipaldap.py @@ -1164,7 +1164,7 @@ class LDAPClient(object): ) return self.combine_filters(flts, rules) - def get_entries(self, base_dn, scope=None, filter=None, attrs_list=None): + def get_entries(self, base_dn, scope=None, filter=None, attrs_list=None, debug=False): """Return a list of matching entries. Raises an error if the list is truncated by the server @@ -1177,14 +1177,14 @@ class LDAPClient(object): Use the find_entries method for more options. """ entries, truncated = self.find_entries( - base_dn=base_dn, scope=scope, filter=filter, attrs_list=attrs_list) + base_dn=base_dn, scope=scope, filter=filter, attrs_list=attrs_list, debug=debug) if truncated: raise errors.LimitsExceeded() return entries def find_entries(self, filter=None, attrs_list=None, base_dn=None, scope=ldap.SCOPE_SUBTREE, time_limit=None, - size_limit=None, search_refs=False): + size_limit=None, search_refs=False, debug=False): """ Return a list of entries and indication of whether the results were truncated ([(dn, entry_attrs)], truncated) matching specified search @@ -1232,14 +1232,20 @@ class LDAPClient(object): base_dn, scope, filter, attrs_list, timeout=time_limit, sizelimit=size_limit ) + if debug: + self.log.info("id: %s" % id) while True: (objtype, res_list) = self.conn.result(id, 0) + if debug: + self.log.info("res_list: %s" % res_list) if not res_list: break if (objtype == ldap.RES_SEARCH_ENTRY or (search_refs and objtype == ldap.RES_SEARCH_REFERENCE)): res.append(res_list[0]) + if debug: + self.log.info('appending to res: %s' % res_list[0]) except (ldap.ADMINLIMIT_EXCEEDED, ldap.TIMELIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED), e: truncated = True diff --git a/ipapython/ipautil.py b/ipapython/ipautil.py index 92569c3b4..e00f3ef3f 100644 --- a/ipapython/ipautil.py +++ b/ipapython/ipautil.py @@ -246,7 +246,7 @@ def shell_quote(string): return "'" + string.replace("'", "'\\''") + "'" def run(args, stdin=None, raiseonerr=True, - nolog=(), env=None, capture_output=True, skip_output=False, cwd=None): + nolog=(), env=None, capture_output=True, skip_output=False, cwd=None, debug=False): """ Execute a command and return stdin, stdout and the process return code. @@ -295,8 +295,9 @@ def run(args, stdin=None, raiseonerr=True, p_err = subprocess.PIPE arg_string = nolog_replace(' '.join(args), nolog) - root_logger.debug('Starting external process') - root_logger.debug('args=%s' % arg_string) + if debug: + root_logger.info('Starting external process') + root_logger.info('args=%s' % arg_string) try: p = subprocess.Popen(args, stdin=p_in, stdout=p_out, stderr=p_err, @@ -314,7 +315,8 @@ def run(args, stdin=None, raiseonerr=True, if skip_output: p_out.close() # pylint: disable=E1103 - root_logger.debug('Process finished, return code=%s', p.returncode) + if debug: + root_logger.info('Process finished, return code=%s', p.returncode) # The command and its output may include passwords that we don't want # to log. Replace those. diff --git a/ipaserver/dcerpc.py b/ipaserver/dcerpc.py index 0f98ce83c..a3b1a444b 100644 --- a/ipaserver/dcerpc.py +++ b/ipaserver/dcerpc.py @@ -53,6 +53,7 @@ from ipapython.ipaldap import IPAdmin from ipalib.session import krbccache_dir, krbccache_prefix from dns import resolver, rdatatype from dns.exception import DNSException +from time import sleep __doc__ = _(""" Classes to manage trust joins using DCE-RPC calls @@ -61,6 +62,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 +71,55 @@ def is_sid_valid(sid): else: return True + +def kinit_as_http(domain): + """ + Initializes ccache with http service credentials. + + Applies session code defaults for ccache directory and naming prefix. + Session code uses krbccache_prefix+, we use + krbccache_prefix++ 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.info('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, debug=True) + + root_logger.warning('kdestroy: %s' % stdout) + root_logger.warning('kdestroy: %s' % stderr) + root_logger.warning('kdestroy: %s' % str(returncode)) + + # Destroy the contents of the ccache + root_logger.info('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, debug=True) + + if returncode == 0: + return (ccache_path, principal) + else: + return (None, None) + access_denied_error = errors.ACIError(info=_('CIFS server denied your credentials')) dcerpc_error_codes = { -1073741823: @@ -113,6 +164,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 +236,28 @@ 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_basedn(self, domain): + info = self.__retrieve_trusted_domain_gc_list(domain) + + if not info: + raise errors.ValidationError(name=_('Trust setup'), + error=_('Cannot retrieve trusted domain GC list')) + + basedn = DN(*map(lambda p: ('dc', p), info['dns_domain'].split('.'))) + return basedn + 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 +274,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 +503,143 @@ 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 search_in_gc(self, domain, filter, attrs, scope, basedn=None, + use_http=False): """ Perform LDAP search in a trusted domain `domain' Global Catalog. - Returns resulting entries or None + 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_gc(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_gc(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) + + if use_http: + (ccache_name, principal) = 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: + old_ccache = os.environ.get('KRB5CCNAME') + os.environ["KRB5CCNAME"] = ccache_name + + # Printing irrelevant debugging info + root_logger.info('Printing irrelevant (so far) debug info:') + root_logger.info('ccache: %s' % ccache_name) + root_logger.info('host: %s' % host) + root_logger.info('port: %s' % port) + root_logger.info('filter: %s' % filter) + root_logger.info('attrs: %s' % attrs) + + # 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 = ipautil.realm_to_suffix(info['dns_domain']) + + # Let's get some debugging going + root_logger.info('Printing contents of the ccache:') + o, e, r = ipautil.run(['klist'], debug=True) + root_logger.info("klist: %s" % o) + root_logger.info("klist error: %s" % e) + + # How big are the keytabs? + root_logger.info('The size of /etc/httpd/conf/ipa.keytab:') + o, e, r = ipautil.run(['du', '-h', '/etc/httpd/conf/ipa.keytab'], + debug=True) + root_logger.info("du: %s" % o) + root_logger.info("du error: %s" % e) + + root_logger.info('The size of /etc/dirsrv/ds.keytab:') + o, e, r = ipautil.run(['du', '-h', '/etc/dirsrv/ds.keytab'], + debug=True) + root_logger.info("du: %s" % o) + root_logger.info("du error: %s" % e) + + # How big is the ccache? + root_logger.info('The size of ccache %s' % ccache_name) + o, e, r = ipautil.run(['du', '-h', ccache_name], + debug=True) + root_logger.info("du: %s" % o) + root_logger.info("du error: %s" % e) + + # Run the ldapsearch from within the framework + root_logger.info('Conducting ldapsearch:') + o, e, r = ipautil.run( + ['ldapsearch', + '-Y', 'GSSAPI', + '-U', 'HTTP/{local}@{dom}'.format(local=api.env.host, + dom=api.env.realm), + '-H', 'ldap://%s' % host, + '-b', str(basedn), + filter], + raiseonerr=False, debug=True, + env={'KRB5CCNAME': ccache_name}) + + root_logger.info("ldapsearch: %s" % o) + root_logger.warning("ldapsearch error: %s" % e) + + result = 'nothing' + result2= 'nothing' + try: + connection = _ldap.initialize('ldap://{host}'.format(host=host)) + auth2 = _ldap.sasl.gssapi('') + connection.sasl_interactive_bind_s('', auth2) + result = connection.search_s(str(basedn), _ldap.SCOPE_SUBTREE, + filter) + result2 = connection.search_ext(str(basedn), _ldap.SCOPE_SUBTREE, + filter) + except Exception, e: + root_logger.error('direct ldap error: %s' % str(e)) + + root_logger.error('direct ldap result: %s' % result) + root_logger.error('direct ldap result2: %s' % result2) + + try: + entries = conn.get_entries(basedn, scope, filter, attrs, debug=True) + except: + root_logger.error('framework ldap error: %s' % str(e)) + entries = None + finally: os.environ["KRB5CCNAME"] = old_ccache - return entries + + root_logger.error('framework ldap result: %s' % entries) + + return entries def __retrieve_trusted_domain_gc_list(self, domain): """ @@ -508,9 +670,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) -- cgit