diff options
-rw-r--r-- | API.txt | 2 | ||||
-rw-r--r-- | ipalib/plugins/trust.py | 111 | ||||
-rw-r--r-- | ipaserver/dcerpc.py | 164 | ||||
-rw-r--r-- | ipaserver/install/installutils.py | 7 |
4 files changed, 231 insertions, 53 deletions
@@ -3394,7 +3394,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/ipalib/plugins/trust.py b/ipalib/plugins/trust.py index 965ff76bb..b19a27eca 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,96 @@ 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 + # However, we skip this step if other than ipa-ad-trust-posix + # range type is enforced + + if options.get('range_type', None) in (None, u'ipa-ad-trust-posix'): + + # 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, + quiet=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 = (1 + (max_id - base_id) / DEFAULT_RANGE_SIZE)\ + * DEFAULT_RANGE_SIZE + + # 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 +565,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 0f98ce83c..88ad928eb 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,126 @@ 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): + """ + 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, quiet=False): """ - Perform LDAP search in a trusted domain `domain' Global Catalog. - Returns resulting entries or None + 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, + quiet=quiet) 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, quiet=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: + msg = "Search on AD DC {host}:{port} failed with: {err}"\ + .format(host=host, port=str(port), err=str(e)) + if quiet: + root_logger.debug(msg) + else: + root_logger.warning(msg) + finally: + return entries def __retrieve_trusted_domain_gc_list(self, domain): """ @@ -508,9 +594,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 d23f9b57f..e6f50a52c 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) |