mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
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
This commit is contained in:
parent
84b2269589
commit
17c7d46c25
2
API.txt
2
API.txt
@ -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')
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
"""
|
||||
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, quiet=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,
|
||||
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)
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user