summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ipalib/plugins/trust.py51
-rw-r--r--ipapython/ipaldap.py12
-rw-r--r--ipapython/ipautil.py10
-rw-r--r--ipaserver/dcerpc.py230
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+<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.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)