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